Току що обявих новия курс Learn Spring , фокусиран върху основите на Spring 5 и Spring Boot 2:
>> ПРЕГЛЕД НА КУРСА1. Общ преглед
Този урок ще илюстрира как да се приложи обработка на изключения с Spring за REST API. Ще получим и малко исторически преглед и ще видим кои нови опции са въвели различните версии.
Преди Spring 3.2, двата основни подхода за обработка на изключения в Spring MVC приложение са HandlerExceptionResolver или анотацията @ExceptionHandler . И двете имат някои явни недостатъци.
От 3.2 имаме анотацията @ControllerAdvice за справяне с ограниченията на предишните две решения и за насърчаване на унифицирана обработка на изключения в цялото приложение.
Сега Spring 5 представя класа ResponseStatusException - бърз начин за основно обработване на грешки в нашите REST API.
Всички те имат едно общо нещо: Те се справят много добре с разделянето на проблемите . Приложението може да изхвърля изключения обикновено, за да посочи някакъв отказ, който след това ще бъде обработен отделно.
И накрая, ще видим какво Spring Boot носи на масата и как можем да го конфигурираме според нашите нужди.
2. Решение 1: Controller-Level @ExceptionHandler
Първото решение работи на ниво @Controller . Ще дефинираме метод за обработка на изключения и ще го отбележим с @ExceptionHandler :
public class FooController{ //... @ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { // } }
Този подход има голям недостатък: T той @ExceptionHandler анотиран метод е активно само за конкретния контролер , а не в световен мащаб за цялото приложение. Разбира се, добавянето на това към всеки контролер го прави неподходящ за общ механизъм за обработка на изключения.
Можем да заобиколим това ограничение, като накараме всички контролери да разширят базовия клас контролер.
Това решение обаче може да създаде проблем за приложения, при които по някаква причина това не е възможно. Например контролерите може вече да се разширяват от друг базов клас, който може да е в друг буркан или да не може да бъде пряко модифициран, или самите те да не могат да бъдат директно модифицирани.
След това ще разгледаме друг начин за решаване на проблема с обработката на изключения - такъв, който е глобален и не включва промени в съществуващи артефакти като контролери.
3. Решение 2: HandlerExceptionResolver
Второто решение е да се дефинира HandlerExceptionResolver. Това ще разреши всяко изключение, хвърлено от приложението. Това също ще ни позволи да внедрим единен механизъм за обработка на изключения в нашия REST API.
Преди да преминем към персонализиран разделител, нека да разгледаме съществуващите реализации.
3.1. ExceptionHandlerExceptionResolver
Този преобразувател е въведен през пролетта 3.1 и е активиран по подразбиране в DispatcherServlet . Това всъщност е основният компонент на начина, по който работи механизмът @ ExceptionHandler , представен по-рано.
3.2. DefaultHandlerExceptionResolver
Този преобразувател е представен през пролетта 3.0 и е активиран по подразбиране в DispatcherServlet .
Използва се за разрешаване на стандартни Spring изключения за съответните им HTTP кодове на състоянието, а именно клиентска грешка 4xx и сървърна грешка 5xx кодове на състоянието. Ето пълния списък на пролетните изключения, които обработва, и как те се преобразуват в кодове на състоянието.
Въпреки че правилно задава кода на състоянието на отговора, едно ограничение е, че не задава нищо в тялото на отговора. А за REST API - Кодът на състоянието наистина не е достатъчен за представяне на клиента - отговорът също трябва да има тяло, за да позволи на приложението да дава допълнителна информация за отказа.
Това може да бъде решено чрез конфигуриране на разделителна способност на изгледа и изобразяване на съдържание на грешка чрез ModelAndView , но решението очевидно не е оптимално. Ето защо Spring 3.2 представи по-добър вариант, който ще обсъдим в следващ раздел.
3.3. ResponseStatusExceptionResolver
Този преобразувател също е представен през пролетта 3.0 и е активиран по подразбиране в DispatcherServlet .
Неговата основна отговорност е да използва анотацията @ResponseStatus, налична за персонализирани изключения, и да ги съпоставя с кодове на състоянието на HTTP.
Такова персонализирано изключение може да изглежда така:
@ResponseStatus(value = HttpStatus.NOT_FOUND) public class MyResourceNotFoundException extends RuntimeException { public MyResourceNotFoundException() { super(); } public MyResourceNotFoundException(String message, Throwable cause) { super(message, cause); } public MyResourceNotFoundException(String message) { super(message); } public MyResourceNotFoundException(Throwable cause) { super(cause); } }
Същото като DefaultHandlerExceptionResolver , този преобразувател е ограничен по отношение на начина, по който се справя с тялото на отговора - той нанася кода на състоянието на отговора, но тялото все още е нула.
3.4. SimpleMappingExceptionResolver и AnnotationMethodHandlerExceptionResolver
В SimpleMappingExceptionResolver е около продължение на доста време. Той излиза от по-стария модел Spring MVC и не е много подходящ за REST услуга. По същество го използваме за картографиране на имена на класове на изключения за преглед на имена.
В AnnotationMethodHandlerExceptionResolver беше въведен през пролетта на 3.0 да се справят с изключения чрез @ExceptionHandler пояснението, но вече не се използва от ExceptionHandlerExceptionResolver като на пролетта 3.2.
3.5. Персонализиран HandlerExceptionResolver
Комбинацията от DefaultHandlerExceptionResolver и ResponseStatusExceptionResolver изминава дълъг път към осигуряване на добър механизъм за обработка на грешки за Spring RESTful Service. Недостатъкът е, както беше споменато по-рано, липсата на контрол върху тялото на отговора.
В идеалния случай бихме искали да можем да изведем или JSON, или XML, в зависимост от това какъв формат е поискал клиентът (чрез заглавката Accept ).
Само това оправдава създаването на нов, персонализиран разделител на изключения :
@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(); } }
Една подробност, която трябва да забележите тук, е, че имаме достъп до самата заявка , така че можем да разгледаме стойността на заглавката Accept, изпратена от клиента.
Например, ако клиентът поиска application / json , тогава, в случай на грешка, бихме искали да се уверим, че връщаме тяло на отговор, кодирано с application / json .
Другата важна подробност за изпълнението е, че връщаме ModelAndView - това е тялото на отговора и ще ни позволи да зададем каквото е необходимо върху него.
Този подход е последователен и лесно конфигурируем механизъм за обработка на грешки на Spring REST услуга.
Той обаче има ограничения: той си взаимодейства с HtttpServletResponse на ниско ниво и се вписва в стария MVC модел, който използва ModelAndView , така че все още има място за подобрение.
4. Решение 3: @ControllerAdvice
Пролет 3.2 носи поддръжка за глобален @ExceptionHandler с анотацията @ControllerAdvice .
Това позволява механизъм, който се откъсва от по-стария модел на MVC и използва ResponseEntity заедно с типовата безопасност и гъвкавост на @ExceptionHandler :
@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = { IllegalArgumentException.class, IllegalStateException.class }) protected ResponseEntity handleConflict( RuntimeException ex, WebRequest request) { String bodyOfResponse = "This should be application specific"; return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request); } }
The@ControllerAdvice annotation allows us to consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.
The actual mechanism is extremely simple but also very flexible:
- It gives us full control over the body of the response as well as the status code.
- It provides mapping of several exceptions to the same method, to be handled together.
- It makes good use of the newer RESTful ResposeEntity response.
One thing to keep in mind here is to match the exceptions declared with @ExceptionHandler to the exception used as the argument of the method.
If these don't match, the compiler will not complain — no reason it should — and Spring will not complain either.
However, when the exception is actually thrown at runtime, the exception resolving mechanism will fail with:
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...] HandlerMethod details: ...
5. Solution 4: ResponseStatusException (Spring 5 and Above)
Spring 5 introduced the ResponseStatusException class.
We can create an instance of it providing an HttpStatus and optionally a reason and a cause:
@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); } }
What are the benefits of using ResponseStatusException?
- Excellent for prototyping: We can implement a basic solution quite fast.
- One type, multiple status codes: One exception type can lead to multiple different responses. This reduces tight coupling compared to the @ExceptionHandler.
- We won't have to create as many custom exception classes.
- We have more control over exception handling since the exceptions can be created programmatically.
And what about the tradeoffs?
- There's no unified way of exception handling: It's more difficult to enforce some application-wide conventions as opposed to @ControllerAdvice, which provides a global approach.
- Code duplication: We may find ourselves replicating code in multiple controllers.
We should also note that it's possible to combine different approaches within one application.
For example, we can implement a @ControllerAdvice globally but also ResponseStatusExceptions locally.
However, we need to be careful: If the same exception can be handled in multiple ways, we may notice some surprising behavior. A possible convention is to handle one specific kind of exception always in one way.
For more details and further examples, see our tutorial on ResponseStatusException.
6. Handle the Access Denied in Spring Security
The Access Denied occurs when an authenticated user tries to access resources that he doesn't have enough authorities to access.
6.1. MVC — Custom Error Page
First, let's look at the MVC style of the solution and see how to customize an error page for Access Denied.
The XML configuration:
...
And the Java configuration:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedPage("/my-error-page"); }
When users try to access a resource without having enough authorities, they will be redirected to “/my-error-page”.
6.2. Custom AccessDeniedHandler
Next, let's see how to write our custom 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"); } }
And now let's configure it using XML configuration:
...
0r using Java configuration:
@Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler) }
Note how in our CustomAccessDeniedHandler, we can customize the response as we wish by redirecting or displaying a custom error message.
6.3. REST and Method-Level Security
Finally, let's see how to handle method-level security @PreAuthorize, @PostAuthorize, and @Secure Access Denied.
Of course, we'll use the global exception handling mechanism that we discussed earlier to handle the AccessDeniedException as well:
@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ AccessDeniedException.class }) public ResponseEntity handleAccessDeniedException( Exception ex, WebRequest request) { return new ResponseEntity( "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN); } ... }
7. Spring Boot Support
Spring Boot provides an ErrorController implementation to handle errors in a sensible way.
In a nutshell, it serves a fallback error page for browsers (a.k.a. the Whitelabel Error Page) and a JSON response for RESTful, non-HTML requests:
{ "timestamp": "2019-01-17T16:12:45.977+0000", "status": 500, "error": "Internal Server Error", "message": "Error processing the request!", "path": "/my-endpoint-with-exceptions" }
As usual, Spring Boot allows configuring these features with properties:
- server.error.whitelabel.enabled: can be used to disable the Whitelabel Error Page and rely on the servlet container to provide an HTML error message
- server.error.include-stacktrace: with an always value; includes the stacktrace in both the HTML and the JSON default response
Apart from these properties, we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.
We can also customize the attributes that we want to show in the response by including an ErrorAttributes bean in the context. We can extend the DefaultErrorAttributes class provided by Spring Boot to make things easier:
@Component public class MyCustomErrorAttributes extends DefaultErrorAttributes { @Override public Map getErrorAttributes( WebRequest webRequest, boolean includeStackTrace) { Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); errorAttributes.put("locale", webRequest.getLocale() .toString()); errorAttributes.remove("error"); //... return errorAttributes; } }
If we want to go further and define (or override) how the application will handle errors for a particular content type, we can register an ErrorController bean.
Again, we can make use of the default BasicErrorController provided by Spring Boot to help us out.
Например, представете си, че искаме да персонализираме как нашето приложение обработва грешки, задействани в XML крайни точки. Всичко, което трябва да направим, е да дефинираме публичен метод, използвайки @RequestMapping , и да заявим , че той създава тип медия application / xml :
@Component public class MyErrorController extends BasicErrorController { public MyErrorController(ErrorAttributes errorAttributes) { super(errorAttributes, new ErrorProperties()); } @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) public ResponseEntity
8. Заключение
Тази статия обсъжда няколко начина за внедряване на механизъм за обработка на изключения за REST API през пролетта, започвайки от по-стария механизъм и продължавайки с поддръжката на Spring 3.2 и до 4.x и 5.x.
Както винаги, кодът, представен в тази статия, е достъпен в GitHub.
За кода, свързан с Spring Security, можете да проверите модула spring-security-rest.
ПОЧИВКА отдолу