คำแนะนำเกี่ยวกับการจัดการข้อผิดพลาด 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 สำหรับการดึงวัตถุที่เป็นตัวแทนของนก มีคุณลักษณะที่อธิบายไว้ในบทความนี้และตัวอย่างเพิ่มเติมบางส่วนเกี่ยวกับสถานการณ์การจัดการข้อผิดพลาด ต่อไปนี้คือข้อมูลสรุปของปลายทางที่ใช้งานในแอปพลิเคชันนั้น:
GET /birds/{birdId} | รับข้อมูลเกี่ยวกับนกและแสดงข้อยกเว้นหากไม่พบ |
GET /birds/noexception/{birdId} | การโทรนี้ยังได้รับข้อมูลเกี่ยวกับนกด้วย ยกเว้นในกรณีที่ไม่พบนกจะไม่มีข้อยกเว้น | POST /birds | สร้างนก | </tr>
โมดูล 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 เริ่มต้น
การจัดการข้อยกเว้น
ขั้นตอนต่อไปคือการสร้างคลาสที่จะจัดการกับข้อยกเว้น เพื่อความง่าย เรากำลังเรียกมันว่า 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 ต่างกัน
ที่กล่าวว่ามาสร้าง 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
