در این بخش به چگونگی رسیدگی به Exception ها در REST میپردازیم
تا قبل از Spring 3.2 رسیدگی به خطا ها به دو روش اصلی استفاده از کلاس HandlerExceptionResolver و ExceptionHandler@ محدود بود که امروزه هر دوی آنها منسوخ شده اند
از Spring 3.2 با معرفی ControllerAdvice@ جایگزین بهتری برای رسیدگی برای خطا ها در کل برنامه ایجاد شد
در Spring 5 کلاس ResponseStatusException راهی سریع برای رسیدگی به خطا های REST Api معرفی شد
روش ControllerAdvice@ :
@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); } }
این روش برای رسیدگی به خطا ها بصورت global کاربرد دارد و همانطور که مشاهده میکنید خطاهایی که قرار است گرفته شود را با ExceptionHandler@ مشخص میکنیم ولی اگر در runtime خطایی دریافت کردیم که در این لیست موجود نبود آن خطا نا دیده گرفته خواهد شد و این یکی از ضعف های این روش است
روش ResponseStatusException :
در این روش مدیریت خطا انعطاف بیشتری دارد کافی است هنگام catch کردن خطا یک نمونه از این کلاس بسازیم و Http Status Code و توضیح چرایی خطا را ارسال کنیم و در قالب جواب ارسال کنیم :
@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); } }
منافع استفاده از این روش :
- روشی عالی برای ساخت نمونه اولیه از کد و تست آن چون سریع قابل پیاده سازی است
- تنها با یک کلاس میتوانیم چندین status code داشته باشیم وقتی یک خطایی ایجاد شد میتواند بسته به شرایط نیاز باشد که توضیحات و کد وضعیت مختلفی ارسال شود که توسط این کلاس قابل انجام است
- عدم نیاز به ساخت کلاس های مدیریت خطای سفارشی
- کنترل بیشتر روی خطا ها و مدیریت انها
- وقتی برنامه بزرگ میشود کنترل خطا ها توسط ControllerAdvice سخت تر میشود و انعطاف کافی را ندارد
- تکرار کد کمتری خواهیم داشت
همچنین میتوانیم ما از ControllerAdvice@ بصورت global و کلاس ResponseStatusExceptions بصورت local استفاده کنیم که بسته به منطق طراحی دارد
*نکته ای که وجود دارد نباید یک نوع Exception را در دو حالت catch کنیم
رسیدگی به کنترل عدم دسترسی به منابعی که کاربر برای ان منابع مجاز نیست :
ایجاد صفحه دسترسی غیر مجاز :
تنظیمات xml :
<http> <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/> ... <access-denied-handler error-page="/my-error-page" /> </http>
روش تنظیمات Java :
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedPage("/my-error-page"); }
کاربر اگر به منابعی بخواهد دسترسی داشته باشد که غیر مجاز باشد به صفحه my-error-page/ هدایت خواهد شد
ساخت AccessDeniedHandler سفارشی :
@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException { response.sendRedirect("/my-error-page"); } }
تنظیمات xml :
<http> <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/> ... <access-denied-handler ref="customAccessDeniedHandler" /> </http>
تنظیمات به روش Java :
@Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler) }
در این روش میتوانیم محتوای سفارشی مربوط به صفحه توضیحات عدم دسترسی به منابع دلخواه را بسازیم
ایجاد امنیت در سطح Method Level در REST :
به کمک PreAuthorize@ و PostAuthorize@ و Secure@ میتوانیم سطوح دسترسی به متد ها را تعیین کنیم و به کمک ControllerAdvice@ یک مدیریت خطا global برای نمایش پیام خطا موقع دسترسی به این متد ها ایجاد میکنیم :
@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ AccessDeniedException.class }) public ResponseEntity<Object> handleAccessDeniedException( Exception ex, WebRequest request) { return new ResponseEntity<Object>( "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN); } ... }
مدیریت خطاهای REST با کمک Spring Boot :
Spring Boot با پیاده سازی ErrorController یک روش مدیریت رسیدگی به خطا فراهم میکند و یک صفحه نمایش خطا برای درخواست های http در مرورگر (Whitelabel Error Page) و برای RESTful خطا ها در قالب پیام های JSON ارسال میکند
{ "timestamp": "2019-01-17T16:12:45.977+0000", "status": 500, "error": "Internal Server Error", "message": "Error processing the request!", "path": "/my-endpoint-with-exceptions" }
به طور معمول ما اجازه داریم برخی ویژگی ها را تغییر تا ست کنیم :
- با server.error.whitelabel.enabled میتوانیم نمایش Whitelabel Error Page را فعال یا غیر فعال کنیم و نمایش پیام خطا بصورت HTML را به servlet بسپاریم
- با server.error.include-stacktrace اگر مقدار always را بدهیم میتوانیم stacktrace را در دو حالت HTML و JSON ارسال کنیم
همچنین با استفاده از Spring Boot میتوانیم با ارث بری کردن از کلاس DefaultErrorAttributes که یک Bean از نوع ErrorAttributes است برخی ویژگی ها را تغییر دهیم
@Component public class MyCustomErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes( WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); errorAttributes.put("locale", webRequest.getLocale() .toString()); errorAttributes.remove("error"); //... return errorAttributes; } }
اگر بخواهیم پا فراتر بگذاریم و مدیریت رسیدگی به خطا ها را برای یک نوع content type خاص مثلا xml ایجاد کنیم کافی است یک Bean از نوع ErrorController داشته باشیم کلاس BasicErrorController در این زمینه ارائه شده است :
@Component public class MyErrorController extends BasicErrorController { public MyErrorController(ErrorAttributes errorAttributes) { super(errorAttributes, new ErrorProperties()); } @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) { // ... } }
( به غیر از امکان بالا میتوانیم از یک صفحه error/ کاملا سفارشی همراه با ViewResolver هم استفاده کنیم )