การจัดการข้อผิดพลาดอย่างถูกต้องใน API ในขณะที่ให้ข้อความแสดงข้อผิดพลาดที่มีความหมายเป็นคุณลักษณะที่พึงปรารถนามากเนื่องจากสามารถช่วยไคลเอ็นต์ API ตอบสนองต่อปัญหาได้อย่างเหมาะสม พฤติกรรมเริ่มต้นมีแนวโน้มที่จะส่งคืนสแต็กเทรซที่ยากที่จะเข้าใจและในที่สุดก็ไม่มีประโยชน์สำหรับไคลเอ็นต์ API การแบ่งพาร์ติชันข้อมูลข้อผิดพลาดลงในฟิลด์ยังช่วยให้ไคลเอนต์ API สามารถแยกวิเคราะห์และให้ข้อความแสดงข้อผิดพลาดที่ดีขึ้นแก่ผู้ใช้ ในบทความนี้เราจะพูดถึงวิธีจัดการข้อผิดพลาดที่เหมาะสมเมื่อสร้าง REST API ด้วย สปริงบูต .
การสร้าง REST API ด้วย Spring กลายเป็นแนวทางมาตรฐานสำหรับนักพัฒนา Java ในช่วงสองสามปีที่ผ่านมา การใช้ Spring Boot ช่วยได้มากเนื่องจากจะลบรหัสสำเร็จรูปจำนวนมากและเปิดใช้งานการกำหนดค่าส่วนประกอบต่างๆโดยอัตโนมัติ เราจะถือว่าคุณคุ้นเคยกับพื้นฐานของการพัฒนา API ด้วยเทคโนโลยีเหล่านั้นก่อนที่จะใช้ความรู้ที่อธิบายไว้ที่นี่ หากคุณยังไม่แน่ใจเกี่ยวกับวิธีการพัฒนา REST API พื้นฐานคุณควรเริ่มจากบทความเกี่ยวกับ MVC ฤดูใบไม้ผลิ หรืออีกเรื่องหนึ่งเกี่ยวกับการสร้างก บริการ Spring REST .
ตลอดบทความนี้เราจะใช้ไฟล์ ซอร์สโค้ดโฮสต์บน GitHub ของแอปพลิเคชันที่ใช้ REST API สำหรับการดึงวัตถุที่แสดงถึงนก มีคุณสมบัติที่อธิบายไว้ในบทความนี้และอีกสองสามตัวอย่างของสถานการณ์การจัดการข้อผิดพลาด ข้อมูลสรุปของจุดสิ้นสุดที่ใช้งานในแอปพลิเคชันนั้นมีดังนี้
GET /birds/{birdId} | รับข้อมูลเกี่ยวกับนกและโยนข้อยกเว้นหากไม่พบ |
GET /birds/noexception/{birdId} | การโทรนี้ได้รับข้อมูลเกี่ยวกับนกด้วยเช่นกันยกเว้นว่าจะไม่มีข้อยกเว้นในกรณีที่ไม่พบนก | POST /birds | สร้างนก |
โมดูล MVC ของ Spring framework มาพร้อมกับคุณสมบัติที่ยอดเยี่ยมเพื่อช่วยในการจัดการข้อผิดพลาด แต่นักพัฒนาซอฟต์แวร์ต้องใช้คุณลักษณะเหล่านั้นเพื่อปฏิบัติต่อข้อยกเว้นและส่งคืนการตอบสนองที่มีความหมายไปยังไคลเอ็นต์ API
แปลงวันที่เป็นมิลลิวินาที javascript
ลองดูตัวอย่างคำตอบ Spring Boot เริ่มต้นเมื่อเราออก HTTP POST ไปที่ /birds
จุดสิ้นสุดด้วยออบเจ็กต์ JSON ต่อไปนี้ซึ่งมีสตริง“ aaa” บนฟิลด์“ มวล” ซึ่งควรคาดหวังจำนวนเต็ม:
{ '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')
at [Source: [email protected] ; line: 4, column: 17]', 'path': '/birds' }
อืม ... ข้อความตอบกลับมีช่องที่ดี แต่เน้นมากเกินไปว่าข้อยกเว้นคืออะไร ยังไงก็ตามนี่คือคลาส DefaultErrorAttributes
จาก Spring Boot timestamp
ฟิลด์คือตัวเลขจำนวนเต็มที่ไม่มีแม้แต่ข้อมูลว่าการประทับเวลาอยู่ในหน่วยการวัดใด exception
ฟิลด์เป็นสิ่งที่น่าสนใจสำหรับนักพัฒนา Java เท่านั้นและข้อความดังกล่าวทำให้ผู้ใช้ API สูญหายในรายละเอียดการใช้งานทั้งหมดที่ไม่เกี่ยวข้องกับพวกเขา และจะเกิดอะไรขึ้นถ้ามีรายละเอียดเพิ่มเติมที่เราสามารถดึงออกจากข้อยกเว้นที่เกิดจากข้อผิดพลาด? ดังนั้นเรามาเรียนรู้วิธีปฏิบัติตามข้อยกเว้นเหล่านั้นอย่างเหมาะสมและรวมเข้ากับการแสดง JSON ที่ดีกว่าเดิมเพื่อทำให้ชีวิตง่ายขึ้นสำหรับไคลเอนต์ API ของเรา
เนื่องจากเราจะใช้คลาสวันที่และเวลา Java 8 อันดับแรกเราต้องเพิ่มการอ้างอิง Maven สำหรับตัวแปลง Jackson JSR310 พวกเขาดูแลการแปลงคลาสวันที่และเวลา Java 8 เป็นการแสดง JSON โดยใช้ @JsonFormat
คำอธิบายประกอบ:
com.fasterxml.jackson.datatype jackson-datatype-jsr310
เอาล่ะมากำหนดคลาสสำหรับแสดงข้อผิดพลาดของ 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 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
คุณสมบัติเก็บอินสแตนซ์วันที่ - เวลาเมื่อเกิดข้อผิดพลาด
ส่วนแบ่งการตลาด uber vs ลิฟท์
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 ที่ส่งคืนเมื่อไม่พบเอนทิตีในขณะที่เรียก endpoint 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
โทรด้วยค่าที่ไม่ถูกต้องสำหรับมวลของนก:
{ '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 บางส่วนที่จะใช้ในการจัดการข้อยกเว้น
RestController
เป็นคำอธิบายประกอบพื้นฐานสำหรับคลาสที่จัดการการดำเนินการ REST
ExceptionHandler
เป็นคำอธิบายประกอบแบบ Spring ที่มีกลไกในการปฏิบัติต่อข้อยกเว้นที่เกิดขึ้นระหว่างการดำเนินการของตัวจัดการ (การดำเนินการของคอนโทรลเลอร์) คำอธิบายประกอบนี้หากใช้กับวิธีการของคลาสคอนโทรลเลอร์จะทำหน้าที่เป็นจุดเริ่มต้นสำหรับการจัดการข้อยกเว้นที่เกิดขึ้นภายในคอนโทรลเลอร์นี้เท่านั้น วิธีที่ใช้กันทั่วไปคือการใช้ @ExceptionHandler
เกี่ยวกับวิธีการของ @ControllerAdvice
คลาสเพื่อให้การจัดการข้อยกเว้นถูกนำไปใช้ทั่วโลกหรือกับชุดควบคุมย่อย
ControllerAdvice
เป็นคำอธิบายประกอบที่นำมาใช้ในฤดูใบไม้ผลิ 3.2 และตามชื่อที่แนะนำคือ“ คำแนะนำ” สำหรับคอนโทรลเลอร์หลายตัว ใช้เพื่อเปิดใช้งาน ExceptionHandler
เพื่อใช้กับคอนโทรลเลอร์หลายตัว ด้วยวิธีนี้เราสามารถกำหนดวิธีการรักษาข้อยกเว้นดังกล่าวได้จากที่เดียวและตัวจัดการนี้จะถูกเรียกเมื่อข้อยกเว้นถูกโยนออกจากคลาสที่ครอบคลุมโดย ControllerAdvice
ชุดควบคุมย่อยที่ได้รับผลกระทบสามารถกำหนดได้โดยใช้ตัวเลือกต่อไปนี้บน @ControllerAdvice
: annotations()
, basePackageClasses()
และ basePackages()
หากไม่มีการระบุตัวเลือกจะแสดง ControllerAdvice
ถูกนำไปใช้ทั่วโลกกับคอนโทรลเลอร์ทั้งหมด
ดังนั้นโดยใช้ @ExceptionHandler
และ @ControllerAdvice
เราจะสามารถกำหนดจุดศูนย์กลางสำหรับการปฏิบัติต่อข้อยกเว้นและรวมไว้ใน ApiError
วัตถุที่มีองค์กรที่ดีกว่ากลไกการจัดการข้อผิดพลาด Spring Boot ที่เป็นค่าเริ่มต้น
มอนติคาร์โลแบบจำลองการเงินจำลอง
องค์ประกอบและหลักการนิยามการออกแบบ
ขั้นตอนต่อไปคือการสร้างคลาสที่จะจัดการกับข้อยกเว้น เพื่อความเรียบง่ายเราเรียกมันว่า RestExceptionHandler
และต้องขยายจาก Spring Boot’s ResponseEntityExceptionHandler
เราจะขยาย ResponseEntityExceptionHandler
เนื่องจากมีการจัดการข้อยกเว้น Spring MVC พื้นฐานอยู่แล้วดังนั้นเราจึงเพิ่มตัวจัดการสำหรับข้อยกเว้นใหม่ในขณะที่ปรับปรุงข้อยกเว้นที่มีอยู่
การลบล้างข้อยกเว้นที่จัดการใน ResponseEntityExceptionHandler
หากคุณดูซอร์สโค้ดของ ResponseEntityExceptionHandler
คุณจะเห็นวิธีการมากมายที่เรียกว่า handle******()
ชอบ handleHttpMessageNotReadable()
หรือ handleHttpMessageNotWritable()
. ก่อนอื่นมาดูกันว่าเราจะขยาย handleHttpMessageNotReadable()
ได้อย่างไร จัดการ HttpMessageNotReadableException
ข้อยกเว้น เราก็ต้องแทนที่ method handleHttpMessageNotReadable()
ใน RestExceptionHandler
ของเรา ชั้น:
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity 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 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')
at [Source: [email protected] ; line: 4, column: 17]' } }
ตอนนี้เราจะมาดูวิธีสร้างวิธีจัดการข้อยกเว้นที่ยังไม่ได้ประกาศใน Spring Boot’s ResponseEntityExceptionHandler
สถานการณ์ทั่วไปสำหรับแอ็พพลิเคชัน Spring ที่จัดการการเรียกฐานข้อมูลคือการเรียกเพื่อค้นหาเร็กคอร์ดโดยใช้ ID โดยใช้คลาสที่เก็บ แต่ถ้าเรามองเข้าไปใน CrudRepository.findOne()
เราจะเห็นว่ามันส่งกลับ null
หากไม่พบวัตถุ นั่นหมายความว่าหากบริการของเราเรียกใช้เมธอดนี้และส่งกลับไปที่คอนโทรลเลอร์โดยตรงเราจะได้รับรหัส HTTP 200 (ตกลง) แม้ว่าจะไม่พบทรัพยากร ในความเป็นจริงแนวทางที่เหมาะสมคือการส่งคืนรหัส HTTP 404 (ไม่พบ) ตามที่ระบุไว้ในไฟล์ ข้อมูลจำเพาะ HTTP / 1.1 .
ในการจัดการกับกรณีนี้เราจะสร้างข้อยกเว้นที่กำหนดเองชื่อ EntityNotFoundException
อันนี้เป็นข้อยกเว้นที่สร้างขึ้นเองและแตกต่างจาก javax.persistence.EntityNotFoundException
เนื่องจากมีตัวสร้างบางตัวที่ทำให้การสร้างวัตถุง่ายขึ้นและอาจเลือกที่จะจัดการกับ javax.persistence
ข้อยกเว้นแตกต่างกัน
ที่กล่าวมาเรามาสร้าง ExceptionHandler
สำหรับ EntityNotFoundException
ที่สร้างขึ้นใหม่นี้ ใน RestExceptionHandler
ของเรา ชั้นเรียน ในการทำเช่นนั้นให้สร้างเมธอดที่เรียกว่า handleEntityNotFound()
และใส่คำอธิบายประกอบด้วย @ExceptionHandler
ผ่านวัตถุคลาส EntityNotFoundException.class
ไปเลย นี่เป็นสัญญาณบ่งบอกถึงฤดูใบไม้ผลิว่าทุกครั้ง EntityNotFoundException
ถูกโยนสปริงควรเรียกวิธีการนี้เพื่อจัดการกับมัน เมื่อใส่คำอธิบายประกอบวิธีด้วย @ExceptionHandler
มันจะยอมรับพารามิเตอร์ที่ฉีดอัตโนมัติได้หลากหลายเช่น WebRequest
, Locale
และอื่น ๆ ตามที่อธิบายไว้ ที่นี่ . เราจะให้ข้อยกเว้น EntityNotFoundException
เป็นพารามิเตอร์สำหรับสิ่งนี้ handleEntityNotFound
วิธี.
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity 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 .
ต่อไปนี้เป็นแหล่งข้อมูลเพิ่มเติมที่ช่วยในการเขียนบทความนี้:
c ++ รวมส่วนหัว
Baeldung - เกิดข้อผิดพลาดในการจัดการ REST กับ Spring
บล็อกฤดูใบไม้ผลิ - การจัดการข้อยกเว้นใน Spring MVC
เพื่อให้ไคลเอนต์ API สามารถแยกวิเคราะห์วัตถุข้อผิดพลาดได้อย่างถูกต้อง ข้อผิดพลาดที่ซับซ้อนมากขึ้นอาจทำให้การใช้งานคลาส ApiSubError และให้รายละเอียดเพิ่มเติมเกี่ยวกับปัญหาเพื่อให้ไคลเอ็นต์ทราบว่าต้องดำเนินการใด
มีคลาสที่เรียกว่า ExceptionHandlerExceptionResolver ใน Spring MVC งานส่วนใหญ่เกิดขึ้นในเมธอด doResolveHandlerMethodException ()
โดยปกติสิ่งสำคัญคือต้องแสดงให้เห็นว่าข้อผิดพลาดมาจากไหน มีพารามิเตอร์อินพุตใดที่ทำให้เกิดข้อผิดพลาดหรือไม่? สิ่งสำคัญคือต้องให้คำแนะนำเกี่ยวกับวิธีแก้ไขการโทรที่ล้มเหลว