คำแนะนำเกี่ยวกับการจัดการข้อผิดพลาด Spring Boot REST API

เผยแพร่แล้ว: 2022-03-11

การจัดการข้อผิดพลาดอย่างถูกต้องใน API ในขณะที่แสดงข้อความแสดงข้อผิดพลาดที่สื่อความหมายเป็นคุณลักษณะที่ต้องการอย่างมาก เนื่องจากสามารถช่วยให้ไคลเอ็นต์ API ตอบสนองต่อปัญหาได้อย่างเหมาะสม ลักษณะการทำงานเริ่มต้นมักจะส่งคืนสแต็กเทรซที่เข้าใจยากและท้ายที่สุดก็ไร้ประโยชน์สำหรับไคลเอ็นต์ API การแบ่งพาร์ติชั่นข้อมูลข้อผิดพลาดลงในฟิลด์ยังช่วยให้ไคลเอ็นต์ API สามารถแยกวิเคราะห์และแสดงข้อความแสดงข้อผิดพลาดแก่ผู้ใช้ได้ดียิ่งขึ้น ในบทความนี้ เราจะพูดถึงวิธีจัดการข้อผิดพลาดที่เหมาะสมเมื่อสร้าง REST API ด้วย Spring Boot

บุคคลสับสนเกี่ยวกับข้อความแสดงข้อผิดพลาดที่คลุมเครือและยาว

การสร้าง REST API ด้วย Spring กลายเป็นแนวทางมาตรฐานสำหรับนักพัฒนา Java ในช่วงสองสามปีที่ผ่านมา การใช้ Spring Boot ช่วยได้อย่างมาก เนื่องจากจะลบโค้ดสำเร็จรูปจำนวนมาก และเปิดใช้งานการกำหนดค่าอัตโนมัติของส่วนประกอบต่างๆ เราจะถือว่าคุณคุ้นเคยกับพื้นฐานของการพัฒนา API กับเทคโนโลยีเหล่านั้นก่อนที่จะใช้ความรู้ที่อธิบายไว้ที่นี่ หากคุณยังไม่แน่ใจเกี่ยวกับวิธีการพัฒนา REST API พื้นฐาน คุณควรเริ่มด้วยบทความเกี่ยวกับ Spring MVC หรือบทความอื่นเกี่ยวกับการสร้าง Spring REST Service

ทำให้การตอบสนองต่อข้อผิดพลาดชัดเจนขึ้น

ตลอดบทความนี้ เราจะใช้ซอร์สโค้ดที่โฮสต์บน GitHub ของแอปพลิเคชันที่ใช้ REST API สำหรับการดึงวัตถุที่เป็นตัวแทนของนก มีคุณลักษณะที่อธิบายไว้ในบทความนี้และตัวอย่างเพิ่มเติมบางส่วนเกี่ยวกับสถานการณ์การจัดการข้อผิดพลาด ต่อไปนี้คือข้อมูลสรุปของปลายทางที่ใช้งานในแอปพลิเคชันนั้น:

</tr>
GET /birds/{birdId} รับข้อมูลเกี่ยวกับนกและแสดงข้อยกเว้นหากไม่พบ
GET /birds/noexception/{birdId} การโทรนี้ยังได้รับข้อมูลเกี่ยวกับนกด้วย ยกเว้นในกรณีที่ไม่พบนกจะไม่มีข้อยกเว้น
POST /birds สร้างนก

โมดูล Spring framework MVC มาพร้อมกับคุณสมบัติที่ยอดเยี่ยมเพื่อช่วยในการจัดการข้อผิดพลาด แต่นักพัฒนาจะใช้คุณสมบัติเหล่านั้นเพื่อจัดการกับข้อยกเว้นและส่งคืนการตอบกลับที่มีความหมายไปยังไคลเอนต์ API

ลองดูตัวอย่างของคำตอบเริ่มต้นของ Spring Boot เมื่อเราออก HTTP POST ไปยังปลายทาง /birds ด้วยอ็อบเจ็กต์ JSON ต่อไปนี้ ซึ่งมีสตริง "aaa" บนฟิลด์ "mass" ซึ่งควรคาดหวังให้เป็นจำนวนเต็ม:

 { "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }

คำตอบเริ่มต้นของ Spring Boot โดยไม่มีการจัดการข้อผิดพลาดที่เหมาะสม:

 { "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }

อืม… ข้อความตอบกลับมีบางช่องที่ดี แต่เน้นมากเกินไปกับข้อยกเว้น อย่างไรก็ตาม นี่คือคลาส DefaultErrorAttributes จาก Spring Boot ฟิลด์การ timestamp เป็นตัวเลขจำนวนเต็มที่ไม่มีแม้กระทั่งข้อมูลของหน่วยการวัดที่มีการประทับเวลา ฟิลด์ exception นั้นน่าสนใจสำหรับนักพัฒนา Java เท่านั้นและข้อความจะทำให้ผู้บริโภค API สูญเสียรายละเอียดการใช้งานทั้งหมดที่ไม่เกี่ยวข้องกับพวกเขา . แล้วถ้ามีรายละเอียดเพิ่มเติมที่เราสามารถดึงออกมาจากข้อยกเว้นที่เกิดข้อผิดพลาดได้ล่ะ? มาเรียนรู้วิธีจัดการกับข้อยกเว้นเหล่านั้นอย่างเหมาะสมและรวมไว้เป็นการแสดง JSON ที่ดีกว่าเพื่อทำให้ชีวิตง่ายขึ้นสำหรับไคลเอ็นต์ API ของเรา

เนื่องจากเราจะใช้คลาสวันที่และเวลาของ Java 8 ก่อนอื่นเราต้องเพิ่มการพึ่งพา Maven สำหรับตัวแปลง Jackson JSR310 พวกเขาดูแลการแปลงคลาสวันที่และเวลาของ Java 8 เป็นการแสดง JSON โดยใช้คำอธิบายประกอบ @JsonFormat :

 <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>

ตกลง มากำหนดคลาสสำหรับแสดงข้อผิดพลาด API เราจะสร้างคลาสที่เรียกว่า ApiError ซึ่งมีฟิลด์เพียงพอสำหรับเก็บข้อมูลที่เกี่ยวข้องเกี่ยวกับข้อผิดพลาดที่เกิดขึ้นระหว่างการโทร REST

 class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
  • คุณสมบัติ status เก็บสถานะการเรียกการดำเนินการ มันจะเป็นอะไรก็ได้ตั้งแต่ 4xx เพื่อส่งสัญญาณข้อผิดพลาดของไคลเอนต์หรือ 5xx หมายถึงข้อผิดพลาดของเซิร์ฟเวอร์ สถานการณ์สมมติทั่วไปคือรหัส http 400 ซึ่งหมายถึง BAD_REQUEST เมื่อไคลเอ็นต์ส่งฟิลด์ที่มีรูปแบบไม่ถูกต้อง เช่น ที่อยู่อีเมลที่ไม่ถูกต้อง

  • คุณสมบัติการ timestamp จะเก็บอินสแตนซ์วันที่-เวลาเมื่อเกิดข้อผิดพลาดขึ้น

  • คุณสมบัติ message เก็บข้อความที่เป็นมิตรต่อผู้ใช้เกี่ยวกับข้อผิดพลาด

  • คุณสมบัติ debugMessage เก็บข้อความระบบที่อธิบายข้อผิดพลาดโดยละเอียด

  • คุณสมบัติ subErrors เก็บอาร์เรย์ของข้อผิดพลาดย่อยที่เกิดขึ้น ใช้สำหรับแสดงข้อผิดพลาดหลายรายการในการโทรครั้งเดียว ตัวอย่างจะเป็นข้อผิดพลาดในการตรวจสอบซึ่งหลายฟิลด์ล้มเหลวในการตรวจสอบ คลาส ApiSubError ใช้เพื่อสรุปสิ่งเหล่านั้น

 abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }

ดังนั้น ApiValidationError จึงเป็นคลาสที่ขยาย ApiSubError และแสดงปัญหาการตรวจสอบความถูกต้องที่พบในระหว่างการโทร REST

ด้านล่างนี้ คุณจะเห็นตัวอย่างบางส่วนของการตอบสนอง JSON ที่สร้างขึ้นหลังจากที่เราได้ใช้การปรับปรุงที่อธิบายไว้ที่นี่ เพื่อให้เข้าใจถึงสิ่งที่เราจะได้รับในตอนท้ายของบทความนี้

นี่คือตัวอย่างของ JSON ที่ส่งคืนเมื่อไม่พบเอนทิตีขณะเรียกปลายทาง GET /birds/2 :

 { "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }

นี่เป็นอีกตัวอย่างหนึ่งของ JSON ที่ส่งคืนเมื่อออก POST /birds call ด้วยค่าที่ไม่ถูกต้องสำหรับมวลของนก:

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }

การจัดการข้อผิดพลาดของ Spring Boot

มาสำรวจคำอธิบายประกอบ Spring บางส่วนที่จะใช้ในการจัดการข้อยกเว้น

RestController เป็นคำอธิบายประกอบพื้นฐานสำหรับคลาสที่จัดการการดำเนินการ REST

ExceptionHandler เป็นคำอธิบายประกอบ Spring ที่มีกลไกในการจัดการข้อยกเว้นที่เกิดขึ้นระหว่างการดำเนินการของตัวจัดการ (การดำเนินการของตัวควบคุม) หมายเหตุประกอบนี้ หากใช้กับเมธอดของคลาสคอนโทรลเลอร์ จะทำหน้าที่เป็นจุดเริ่มต้นสำหรับการจัดการข้อยกเว้นภายในคอนโทรลเลอร์นี้เท่านั้น โดยรวมแล้ว วิธีที่พบบ่อยที่สุดคือการใช้ @ExceptionHandler กับเมธอดของคลาส @ControllerAdvice เพื่อให้การจัดการข้อยกเว้นถูกนำไปใช้ทั่วโลกหรือกับชุดย่อยของคอนโทรลเลอร์

ControllerAdvice เป็นคำอธิบายประกอบที่นำมาใช้ใน Spring 3.2 และตามชื่อที่แนะนำคือ "คำแนะนำ" สำหรับคอนโทรลเลอร์หลายตัว มันถูกใช้เพื่อเปิดใช้งาน ExceptionHandler เดียวเพื่อนำไปใช้กับคอนโทรลเลอร์หลายตัว วิธีนี้เราสามารถกำหนดวิธีจัดการกับข้อยกเว้นดังกล่าวได้ในที่เดียว และตัวจัดการนี้จะถูกเรียกเมื่อมีการส่งข้อยกเว้นออกจากคลาสที่ครอบคลุมโดย ControllerAdvice นี้ เซ็ตย่อยของคอนโทรลเลอร์ที่ได้รับผลกระทบสามารถกำหนดได้โดยใช้ตัวเลือกต่อไปนี้บน @ControllerAdvice : annotations() , basePackageClasses() และ basePackages() หากไม่มีตัวเลือก ControllerAdvice จะถูกนำไปใช้กับคอนโทรลเลอร์ทั้งหมดทั่วโลก

ดังนั้นโดยใช้ @ExceptionHandler และ @ControllerAdvice เราสามารถกำหนดจุดศูนย์กลางสำหรับการรักษาข้อยกเว้นและรวมไว้ในวัตถุ ApiError ที่มีการจัดระเบียบที่ดีกว่ากลไกการจัดการข้อผิดพลาด Spring Boot เริ่มต้น

การจัดการข้อยกเว้น

การแสดงสิ่งที่เกิดขึ้นกับการเรียกไคลเอ็นต์ REST ที่สำเร็จและล้มเหลว

ขั้นตอนต่อไปคือการสร้างคลาสที่จะจัดการกับข้อยกเว้น เพื่อความง่าย เรากำลังเรียกมันว่า RestExceptionHandler และต้องขยายจาก ResponseEntityExceptionHandler ของ Spring Boot เราจะขยาย ResponseEntityExceptionHandler เนื่องจากมีการจัดการข้อยกเว้น Spring MVC ขั้นพื้นฐานอยู่แล้ว ดังนั้นเราจะเพิ่มตัวจัดการสำหรับข้อยกเว้นใหม่ในขณะที่ปรับปรุงข้อยกเว้นที่มีอยู่

การเอาชนะข้อยกเว้นที่จัดการใน ResponseEntityExceptionHandler

หากคุณดูซอร์สโค้ดของ ResponseEntityExceptionHandler คุณจะเห็นวิธีการมากมายที่เรียกว่า handle******() เช่น handleHttpMessageNotReadable() หรือ handleHttpMessageNotWritable() ก่อนอื่นมาดูว่าเราจะสามารถขยาย handleHttpMessageNotReadable() เพื่อจัดการกับข้อยกเว้น HttpMessageNotReadableException ได้อย่างไร เราแค่ต้องแทนที่เมธอด handleHttpMessageNotReadable() ในคลาส RestExceptionHandler ของเรา:

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }

เราได้ประกาศว่าในกรณีที่มีการ HttpMessageNotReadableException ข้อความแสดงข้อผิดพลาดจะเป็น "คำขอ JSON ที่มีรูปแบบไม่ถูกต้อง" และข้อผิดพลาดจะถูกห่อหุ้มไว้ภายในวัตถุ ApiError ด้านล่างนี้ เราจะเห็นคำตอบของการเรียก REST ด้วยวิธีใหม่นี้ที่ถูกแทนที่:

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }

การจัดการข้อยกเว้นที่กำหนดเอง

ตอนนี้เราจะมาดูวิธีสร้างวิธีการที่จัดการกับข้อยกเว้นที่ยังไม่ได้ประกาศไว้ใน ResponseEntityExceptionHandler ของ Spring Boot

สถานการณ์สมมติทั่วไปสำหรับแอปพลิเคชัน Spring ที่จัดการการเรียกฐานข้อมูลคือการเรียกเพื่อค้นหาบันทึกโดยใช้ ID โดยใช้คลาสที่เก็บ แต่ถ้าเราดู CrudRepository.findOne() เราจะเห็นว่ามันคืน null หากไม่พบอ็อบเจกต์ นั่นหมายความว่าหากบริการของเราเรียกใช้เมธอดนี้และส่งคืนโดยตรงไปยังคอนโทรลเลอร์ เราจะได้รับรหัส HTTP 200 (OK) แม้ว่าจะไม่พบทรัพยากรก็ตาม อันที่จริง วิธีการที่เหมาะสมคือการส่งคืนรหัส HTTP 404 (ไม่พบ) ตามที่ระบุไว้ในข้อมูลจำเพาะ HTTP/1.1

เพื่อจัดการกับกรณีนี้ เราจะสร้างข้อยกเว้นแบบกำหนดเองที่เรียกว่า EntityNotFoundException นี่เป็นข้อยกเว้นที่สร้างขึ้นเองและแตกต่างจาก javax.persistence.EntityNotFoundException เนื่องจากมีตัวสร้างบางตัวที่ช่วยให้การสร้างวัตถุง่ายขึ้น และอาจเลือกที่จะจัดการกับข้อยกเว้น javax.persistence ต่างกัน

ตัวอย่างของการเรียก REST ที่ล้มเหลว

ที่กล่าวว่ามาสร้าง ExceptionHandler สำหรับ EntityNotFoundException ที่สร้างขึ้นใหม่นี้ในคลาส RestExceptionHandler ของเรา ในการทำเช่นนั้น ให้สร้างวิธีการที่เรียกว่า handleEntityNotFound() และใส่คำอธิบายประกอบด้วย @ExceptionHandler โดยส่งผ่านวัตถุคลาส EntityNotFoundException.class ไป สิ่งนี้ส่งสัญญาณให้ Spring ว่าทุกครั้งที่ EntityNotFoundException ออกไป Spring ควรเรียกวิธีนี้เพื่อจัดการกับมัน เมื่อใส่คำอธิบายประกอบวิธีการด้วย @ExceptionHandler จะยอมรับพารามิเตอร์ที่ฉีดอัตโนมัติจำนวนมาก เช่น WebRequest , Locale และอื่นๆ ตามที่อธิบายไว้ที่นี่ เราจะระบุข้อยกเว้น EntityNotFoundException เองเป็นพารามิเตอร์สำหรับเมธอด handleEntityNotFound นี้

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }

ยอดเยี่ยม! ใน handleEntityNotFound() เรากำลังตั้งค่ารหัสสถานะ HTTP เป็น NOT_FOUND และใช้ข้อความแสดงข้อยกเว้นใหม่ นี่คือลักษณะการตอบสนองสำหรับปลายทาง GET /birds/2 ในตอนนี้:

 { "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }

บทสรุป

สิ่งสำคัญคือต้องควบคุมการจัดการข้อยกเว้น เพื่อให้เราสามารถแมปข้อยกเว้นเหล่านั้นกับอ็อบเจ็กต์ ApiError ได้อย่างถูกต้อง และให้ข้อมูลสำคัญที่ช่วยให้ไคลเอ็นต์ API รู้ว่าเกิดอะไรขึ้น ขั้นตอนต่อไปจากที่นี่คือการสร้างเมธอดตัวจัดการเพิ่มเติม (วิธีที่มี @ExceptionHandler) สำหรับข้อยกเว้นที่เกิดขึ้นภายในโค้ดแอปพลิเคชัน มีตัวอย่างเพิ่มเติมสำหรับข้อยกเว้นทั่วไปอื่นๆ เช่น MethodArgumentTypeMismatchException , ConstraintViolationException และอื่นๆ ในโค้ด GitHub

ต่อไปนี้เป็นแหล่งข้อมูลเพิ่มเติมที่ช่วยในองค์ประกอบของบทความนี้:

  • Baeldung - ข้อผิดพลาดในการจัดการ REST ด้วย Spring

  • Spring Blog - การจัดการข้อยกเว้นใน Spring MVC