RezaDehghani
RezaDehghani
خواندن ۷ دقیقه·۴ سال پیش

مدیریت استثناء در Spring REST

Exception flow
Exception flow


مدیریت کردن استثنا ها یکی از مهمترین بخش های برنامه نویسی است. بخشی که نه میتوان انجام نداد و نه میتوان آن را دوست داشت:)

برای ساخت یک کلاس Exception در برنامه بهتر است از روشی که در مقاله جاوا کاپ آمده بهره گرفت:

https://javacup.ir/exception-class-best-practice/

در این مقاله قائده نامگذاری، سازنده (Constructor) های مورد نیاز و به پیاده سازی دو نوع استثنا پرداخته شده است.

انواع Throwable ها در جاوا
انواع Throwable ها در جاوا


حال بعد از ساختن یک کلاس استثنا (Exception) این مسئله مطرح می شود که نحوه استفاده از استثنا ها چگونه است؟

در برنامه های معمولی جاوایی (منظور برنامه هایی است که از Spring Framework استقاده نمی شود)، نکاتی هست که با در نظر گرفتن آنها میتوان مدیریت مطلوبی برای استثنا ها داشت. این نکات در مقاله دیگری از جاواکاپ، عنوان شده است که مرور نکات مهم آن بصورت تیتر وار خالی از لطف نیست:

  • با wrap کردنِ استثنا، جلوی از دست رفتن stack trace را بگیرید
  • استثناهای چک‌شده را دقیقا مشخص کنید
  • یا استثنا را لاگ بزنید و یا آن را مجدد پرتاب کنید، نه هر دو
  • هرگز اجازه ندهید از بلوک finally، استثنا پرتاب شود
  • تنها استثناهایی را دریافت کنید که واقعا می‌توانید هندل کنید
  • اگر استثنا را هندل نمی‌کنید، فقط از بلوک finally استفاده کنید
  • زود پرتاب کنید، دیر دریافت کنید (throw early catch late)
  • استثنای مرتبط پرتاب کنید
  • از Try-With-Resource استفاده کنید

برای مطالعه بیشتر مطلب میتونید به ادرس زیر روجوع کنید:

https://javacup.ir/exception-best-practices/


خوب حالا که کلیاتی از Exception ها را یادآوری کردیم بهتر است برویم سراغ Spring REST. در وبسرویس ها یا REST API هایی که با فریمورک (Framework) اسپرینگ مینویسیم، مدیریت کردن استثنا ها به چهار روش قابل انجام است. قبل از Spring 3.2 راهکار های مدیریت کردن استثنا در اپلیکشن های Spring MVC به دو صورت کلی HandlerExceptionResolver و ExceptionHandler@ صورت می گرفت. هر دو این روش ها نقاط ضعف مشخصی دارند. از Spring 3.2 به بعد با اضافه شدن ControllerAdvice@ تقریبا مشکلات دو روش قبل مرتفع شد و یک روش یکپارچه، برای مدیریت کردن استثنا ها در اپلیکیشن بدست آمد. اخیرا نیز در Spring 5 کلاس ResponseStatusException معرفی شد که یک روش سریع برای مدیریت کردن خطا ها در REST API ها است. در ادامه مبحث به بررسی هر کدام از روش ها می پردازیم و نقاط قوت و ضعف آنها را بررسی خواهیم کرد.

روش اول استفاده از ExceptionHandler@ در سطح controller:

در روش اول که در سطح کلاس Controller@ پیاده سازی می شود متدی نوشته می شود که مدیریت استثنا را بر عهده می گیرد. این متد همراه است با ExceptionHendler@ و به صورت زیر پیاده می شود:

@Controller public class FooController{ //... @ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { // } }

همچنین میتوانید در ورودی این متد Exception مورد نیاز خود را مشخص کرده و در بدنه متد به آن دسترسی پیدا کنید. این راه یک مشکل بسیار بزرگ دارد و آن این است که این متد تنها برای مدیریت کردن استثنا های این controller است و برای کل اپلیکیشن نیست. همچنین اضافه کردن این متد به همه controller های موجود مکانیزم جالبی برای مدیریت کردن استثنا نیست.

ما میتوانیم این محدودیت را با ارث بری همه controller ها از یک کلاس BaseController دور بزنیم اما یک مشکل دیگر بوجود خواهد آمد. برای مثال فرض کنید controller ما از یک کلاس دیگر به دلایل فنی بخواهد ارث ببرید که مستقیما در قابل تغییر نیست یا در یک jar دیگر می باشد، آن وقت چه؟

در قسمت بعد به یک روش جامع تر برای مدیریت کردن استثنا ها می پردازیم که تغییرات در controller ها و بطور کلی تر در محصول ما ندارد.

روش دوم تعریف یک HandlerExceptionResolver :

روش دوم تعریف یک HandlerExceptionResolver است که باعث می شود تمام استثنا های موجود در برنامه مدیریت شوند. استفاده از این روش باعث می شود که یک مکانیزم یکپارچه برای مدیریت استثنا در برنامه پیاده شود.

حال قبل از آنکه به ساختن یک Resolver بپردازیم بهتر است به یک پیاده سازی موجود از آن بپردازیم.

1-ExceptionHandlerExceptionResolver

این resolver در spring 3.1 معرفی شد و به صورت پیش فرض در DispatcherServlet فعال است.این Resolver در واقع بخش اصلی مکانیزم مربوط به EXceptionHandler@ است که در بخش قبلی مورد بررسی قرار گرفت.

2-DefaultHandlerExceptionResolver

این Resolver در spring 3.0 معرفی شد و بصورت پیش فرض در DispatcherServlet فعال است. این Resolver مسئول حل و فصل کردن استثنا های استاندارد در spring و تبدیل آن به HTTP status code ها می باشد. لیست کد های مدیریت شده توسط این Resolver به این شرح است:

http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-ann-rest-spring-mvc-exceptions

این Resolver تنها Status code جواب را مشخص میکند و body جواب را مشخص نمی نماید. این یکی از محدودیت های این Resolver می باشد. در یک REST API فقط status code ها برای client کافی نمی باشد و جواب باید دارای بدنه مشخصی باشد تا برنامه اطلاعات بیشتری را در رابطه با error رخ داده، نمایش دهد.

این موضوع با تنظیم کردن view resolution و رندر کردن error در ModelAndView قابل بر طرف کردن است اما کاملا واضح است که این راه حل بهینه نیست. به همین دلیل است که spring در نسخه 3.2 یک راه حل مناسب تر ارائه داد.

3-ResponseStatusExceptionResolver

این resolver هم در spring 3.0 معرفی شد و به صورت پیش فرض در DispatcherServlet فعال شده است. مسئولیت اصلی این resolver بدین صورت است که با استفاده از ResponseStatus@ که بر روی کلاس های استثنا قرار گرفته HTTP status code مربوطه را نمایه کند.

@ResponseStatus(value = HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException() { super(); } public ResourceNotFoundException(String message, Throwable cause) { super(message, cause); } public ResourceNotFoundException(String message) { super(message); } public ResourceNotFoundException(Throwable cause) { super(cause); } }

این Resolver نیز مشکل DefaultHandlerExceptionResolver را دارا می باشد.

4-SimpleMappingExceptionResolver and AnnotationMethodHandlerExceptionResolver

این Resolver مدت هاست که وجود دارد و در مدل های قدیمی Spring MVC مورد استفاده قرار میگیرد و آنچنان به مبحث REST مرتبط نمی باشد. در واقع برای نمایه (map) کردن کلاس استثنا به یک view می باشد.

در واقع AnnotationMethodHandlerExceptionResolver در spring 3.0 برای مدیریت کردن استثنا ها از طریق ExceptionHandler@ معرفی شد که با معرفی ExceptionHandlerExceptionResolver در spring 3.2 این Resolver منسوخ (depricated) شد.

5-Custom HandlerExceptionResolver

ترکیب DefaultHandlerExceptionResolver و ResponseStatusExceptionResolver یک مکانیزم بسیار خوب را برای REST API فراهم می آورد ولی باز هم همان مشکل مطرح شده وجود دارد. مشکل عدم کنترل بر روی body جواب های ارسال شده.

بطور ایده ال برای ما دارا بودن خروجی JSON یا XML (باتوجه به فرمت درخواستی کاربر) بسیار مطلوب می باشد. همین موضوع به تنهایی باعث می شود که ما به custom exception resolver فکر کنیم.

@Component public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof IllegalArgumentException) { return handleIllegalArgument((IllegalArgumentException) ex,response,handler); } ... } catch (Exception handlerException) { logger.warn(“Handling of [“ + ex.getClass().getName() +“] resulted in Exception”, handlerException); } return null; } private ModelAndView handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_CONFLICT); String accept = request.getHeader(HttpHeaders.ACCEPT); ... return new ModelAndView(); } }

یک موضوع قابل توجه دیگر این است که با توجه به دسترسی ما به Request می توان به مقدار Acceptheader که کاربر ارسال نموده دسترسی پیدا کرد.

روش سوم ControllerAdvice@

در Spring 3.2 پشتیبانی از یک ExceptionHandler@ جامع با انوتیشن ControllerAdvice@ ممکن شد. این موضوع مکانیزمی جدیدی نسبت به مدل MVC بوجود می آورد.

@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = { IllegalArgumentException.class, IllegalStateException.class }) protected ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) { String bodyOfResponse = “This should be application specific”; return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), httpStatus.CONFLICT, request); } }

این مکانیزم در عین سادگی، انعطاف پذیر نیز می باشد و امکانات زیر را به ما می دهد:

  • کنترل کامل بر روی بدنه جواب همچنین به Status code ها
  • نمایه کردن چندین استثنا بر روی یک متد تا با یکدیگر مدیریت شوند.

روش چهارم ResponseStatusException

در Spring 5 کلاس ResponseStatusException معرفی شد که می توان یک نمونه (instance) از این کلاس ساخت و HttpStatus و reason و یا cause آن را به سازنده (constructor) آن پاس داد.

@GetMapping(value = “/{id}”) public Foo findById(@PathVariable(“id”) Long id, HttpServletResponse response) { try { Foo resourceById = RestPreconditions.checkFound(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response)); return resourceById; } catch (MyResourceNotFoundException exc) { throw new ResponseStatusException( HttpStatus.NOT_FOUND, “Foo Not Found”, exc); } }

مزایای استفاده از ResponseStatusException چیست؟

  • می تواند یک راه حل ساده و سریع باشد.
  • نیازی به ساخت کلاس های متعدد استثنا نیست.
  • یک نوع استثنا میتواند Status code های متعددی را داشته باشد چیزی که در ExceptionHandler@ ممکن نبود.

جمع بندی:

در این مقاله در ابتدا با "به روش" های ساخت کلاس استثنا آشنا شدیم و در ادامه به نحوه برخورد با این استثنا ها در قسمت های مختلف برنامه پرداخته شد.

بعد از بررسی این کلیات روش های مختلف مدیریت کردن استثنا در Spring REST را بررسی کردیم و مزایا و معایب هر کدام از این چهار روش را خاطر نشان کردیم.

آن چه که واضح است این موضوع می باشد که هیچ کدام از این روش ها برای همه حالات رخ داده بهینه نیستند. درواقع ترکیبی از این روش هاست که می تواند مدیریت استثنا مطلوبی را برای برنامه ما ایجاد کند. پس توصیه می شود به هیچ کدام از این روش ها بطور تنها اتکا نکرده و مجموعه ای از آنها را برای استثنا های برنامه مورد استفاده قرار دهید تا یک مدیریت استثنا مطلوب را در برنامه خود شاهد باشید.

موفق باشید :)

منابع:

خب بازم محوریت مطلب ارائه شده ترجمه بود و از کتاب Building a REST API with Spring که گویا تازه منتشر شده کمک گرفته شد. اما باید از مقالات سایت جاواکاپ هم اسم برد زیرا مطالبی از سایت مذکور در این مقاله مورد استفاده قرار گرفته.


exceptionجاواjavaspringrest
شاید از این پست‌ها خوشتان بیاید