دليل معالجة خطأ Spring Boot REST API
نشرت: 2022-03-11يعد التعامل مع الأخطاء بشكل صحيح في واجهات برمجة التطبيقات مع تقديم رسائل خطأ ذات مغزى ميزة مرغوبة للغاية ، حيث يمكن أن تساعد عميل واجهة برمجة التطبيقات على الاستجابة بشكل صحيح للمشكلات. يميل السلوك الافتراضي إلى إرجاع تتبعات المكدس التي يصعب فهمها وغير مجدية في النهاية لعميل واجهة برمجة التطبيقات. يؤدي تقسيم معلومات الخطأ إلى الحقول أيضًا إلى تمكين عميل واجهة برمجة التطبيقات من تحليلها وتقديم رسائل خطأ أفضل للمستخدم. في هذه المقالة ، سنغطي كيفية القيام بمعالجة الأخطاء بشكل مناسب عند إنشاء واجهة برمجة تطبيقات REST باستخدام Spring Boot.
أصبح بناء واجهات برمجة تطبيقات REST مع Spring هو النهج القياسي لمطوري Java خلال العامين الماضيين. يساعد استخدام Spring Boot بشكل كبير ، لأنه يزيل الكثير من التعليمات البرمجية المعيارية ويسمح بالتكوين التلقائي للمكونات المختلفة. سنفترض أنك على دراية بأساسيات تطوير API باستخدام تلك التقنيات قبل تطبيق المعرفة الموضحة هنا. إذا كنت لا تزال غير متأكد من كيفية تطوير واجهة برمجة تطبيقات REST أساسية ، فعليك أن تبدأ بهذه المقالة حول Spring MVC أو مقالة أخرى حول إنشاء خدمة Spring REST.
جعل الردود على الخطأ أكثر وضوحًا
خلال هذه المقالة ، سنستخدم الكود المصدري المستضاف على GitHub لتطبيق يقوم بتنفيذ واجهة برمجة تطبيقات REST لاسترداد الكائنات التي تمثل الطيور. يحتوي على الميزات الموضحة في هذه المقالة وبعض الأمثلة الأخرى لسيناريوهات معالجة الأخطاء. فيما يلي ملخص لنقاط النهاية التي تم تنفيذها في هذا التطبيق:
GET /birds/{birdId} | يحصل على معلومات حول طائر ويرمي استثناء إذا لم يتم العثور عليه. |
GET /birds/noexception/{birdId} | تحصل هذه المكالمة أيضًا على معلومات حول طائر ، إلا أنها لا تقدم استثناء في حالة عدم العثور على الطائر. | POST /birds | يخلق طائر. | </tr>
تأتي وحدة Spring framework MVC مع بعض الميزات الرائعة للمساعدة في معالجة الأخطاء. ولكن يُترك للمطور استخدام هذه الميزات لمعالجة الاستثناءات وإرجاع استجابات ذات مغزى إلى عميل واجهة برمجة التطبيقات.
لنلقِ نظرة على مثال لإجابة Spring Boot الافتراضية عندما نصدر HTTP POST إلى نقطة نهاية /birds bird مع كائن 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')\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 بقيمة غير صالحة لكتلة الطائر:
{ "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" } ] } }معالجة خطأ التمهيد الربيعي
دعنا نستكشف بعض تعليقات الربيع التوضيحية التي سيتم استخدامها للتعامل مع الاستثناءات.

RestController هو التعليق التوضيحي الأساسي للفئات التي تتعامل مع عمليات REST.
ExceptionHandler هو تعليق توضيحي في الربيع يوفر آلية لمعالجة الاستثناءات التي يتم طرحها أثناء تنفيذ المعالجات (عمليات وحدة التحكم). إذا تم استخدام هذا التعليق التوضيحي في طرق فئات وحدة التحكم ، فسيكون بمثابة نقطة دخول للتعامل مع الاستثناءات التي تم إلقاؤها داخل وحدة التحكم هذه فقط. إجمالاً ، الطريقة الأكثر شيوعًا هي استخدام @ExceptionHandler على طرق فئات @ControllerAdvice بحيث يتم تطبيق معالجة الاستثناء عالميًا أو على مجموعة فرعية من وحدات التحكم.
ControllerAdvice هو تعليق توضيحي تم تقديمه في Spring 3.2 ، وكما يوحي الاسم ، هو "Advice" لوحدات التحكم المتعددة. يتم استخدامه لتمكين 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 ، فإن رسالة الخطأ ستكون "Malformed JSON request" وسيتم تغليف الخطأ داخل كائن 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 الذي يعالج استدعاءات قاعدة البيانات هو أن يكون لديك مكالمة للعثور على سجل من خلال المعرف الخاص به باستخدام فئة المستودع. ولكن إذا نظرنا إلى طريقة CrudRepository.findOne() ، فسنرى أنها ترجع null إذا لم يتم العثور على كائن. هذا يعني أنه إذا استدعت خدمتنا هذه الطريقة وعادت مباشرة إلى وحدة التحكم ، فسنحصل على رمز HTTP 200 (موافق) حتى إذا لم يتم العثور على المورد. في الواقع ، الطريقة الصحيحة هي إرجاع رمز 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 MVC
