SpringBoot - Web exception handling
Understand the exception handling mechanism of SpringBoot for Web development.
1. Exception handling mechanism
1.1 default exception handling
By default, SpringBoot provides / error mapping to handle all exceptions,
And register as a global error handling page in the Servlet container.
For browser clients, SpringBoot responds to a "Whitelabel" error view,
Displayed in HTML format, which contains the details of the error, HTTP status code and exception. It looks like this:
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Sat Jun 05 15:19:37 CST 2021 There was an unexpected error (type=Internal Server Error, status=500). / by zero
For the application client, SpringBoot responds to the JSON view, which contains the same information as the HTML view,
It looks like this:
{ "timestamp": "2021-06-05T07:19:31.987+00:00", "status": 500, "error": "Internal Server Error", "exception": "java.lang.ArithmeticException", "message": "/ by zero", "path": "/test" }
1.2 custom error page
We can write a custom error page and display the error information by using the default processing mechanism of SpringBoot.
Create the / error directory in the default static resource file directory (/ public, / static, / resources, / META-INF/resources),
Put it into a static HTML page and it will be automatically parsed and used by SpringBoot. The error page is named 4xx or 5xx (5xx handles all error types with HTTP status starting with 5,
500 only processes the error types with HTTP status code of 500), and SpringBoot will automatically select the corresponding error page according to the HTTP status code.
You can also use the template engine to process and display the error messages generated by SpringBoot, and create the / error directory under the template directory (/ templates),
The template file will also be automatically parsed and used by SpringBoot.
For example (using Thymeleaf):
<!-- src/main/resources/templates/error/5xx.html --> <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>There was an error on the page you visited</title> </head> <body> <h1>Server internal error</h1> HTTP Status:<span th:text="${#request.getAttribute('status')}"></span><br> Timestamp:<span th:text="${#request.getAttribute('timestamp')}"></span><br> Error message:<span th:text="${#request.getAttribute('error')}"></span><br> Exception type:<span th:text="${#request.getAttribute('exception')}"></span><br> Exception information:<span th:text="${#request.getAttribute('message')}"></span><br> Request path:<span th:text="${#request.getAttribute('path')}"></span><br> Binding error:<span th:text="${#request.getAttribute('errors')}"></span><br> Stack information:<span th:text="${#request.getAttribute('trace')}"></span> </body> </html>
SpringBoot provides 8 parameters to encapsulate the data generated by the default exception handling mechanism (/ error):
Parameter name | information | Configuration node | remarks |
---|---|---|---|
status | HTTP status code | - | Always open |
timestamp | time stamp | - | Always open |
error | error message | - | Always open |
exception | Exception type | server.error.include-exception | It is closed by default and needs to be opened manually |
message | Abnormal information | server.error.include-message | It is closed by default and needs to be opened manually |
path | Request path | - | Always open |
trace | Stack information | server.error.include-stacktrace | It is closed by default and needs to be opened manually |
errors | Binding error message | server.error.include-binding-errors | It is closed by default and needs to be opened manually |
Thymeleaf template can obtain the above parameters by using ${#request.getAttribute()}.
1.3 user defined error handling mechanism
We can customize the error handling mechanism to replace the default behavior.
1.3.1 @ControllerAdvice + @ExceptionHandler
The class annotated by @ ControllerAdvice is called Controller enhancer, which can realize the following three functions:
- Global exception handling: the method annotated by @ ExceptionHandler in the class can handle all exceptions generated by the Controller;
- Global data binding: the methods annotated by @ InitBinder in the class can be used for global data binding;
- Global data preprocessing: the method annotated with @ ModelAttribute in the class can put the return value into the model for use by other controllers.
The annotation @ ExceptionHandler enables the annotated method to be used by Spring to handle exceptions. The method marked by this annotation is called an exception handler.
It has one attribute:
- Class<? Extends throwable > [] value: the type of exception that can be handled. It can handle multiple exceptions.
for instance:
@ControllerAdvice public class GlobalExceptionHandler { // The method annotated by @ ExceptionHandler is the exception handler // The exception handler in the Controller enhancer can handle global exceptions // The exception handler in a Controller can only handle the exceptions of the methods in the current Controller @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(Exception e, Model model) { model.addAttribute("exception", e); return "error/500"; } }
In the above example, this exception handler can handle any global mathematical operation exception in the Controller enhancer.
If in a Controller, the exception handler can only handle the mathematical operation exceptions in the current Controller.
The exception handler can also perform fuzzy matching, and the exception handler handling the parent exception can also handle the child exception.
When an exception occurs, Spring first calls the exception handler in the Controller where the handler method is located to handle the exception, and then calls the global exception handler to handle the exception.
Spring gives priority to accurately matching exception handlers, followed by fuzzy matching exception handlers.
@The underlying exception handler mechanism is parsed by the exception parser of the exception handler exception resolver processor.
The ModelAndView object returned by the underlying exception handler is parsed into an error view and responded to the client.
1.3.2 @ResponseStatus + custom exception
Annotation @ ResponseStatus can be annotated on classes or methods:
When the annotation is on the processor method, request the corresponding URL to return directly according to the content of the @ ResponseStatus annotation, and do not execute the processor method any more;
When the annotation is on the exception class, if the processor method throws the exception, Spring will respond according to the contents of the @ ResponseStatus annotation.
The annotation @ ResponseStatus has two properties:
- HttpStatus value/code: the returned HTTP status code;
- String reason: returned exception information.
@The bottom layer of the ResponseStatus exception handling mechanism is parsed by the ResponseStatusExceptionResolver processor exception parser.
The underlying parsing called response Senderror method, send the / error request to Spring's default exception handling mechanism for processing.
1.3.3 user defined exception parser + Order interface
We can also write our own handler exception parser to parse exceptions.
The HandlerExceptionResolver interface needs to be implemented; But by default, the processor exception parser we write has the lowest priority,
Spring may have used the default exception handling mechanism without using our custom exception parser. So we also need to implement the Order interface to change the priority.
for instance:
@Order(Ordered.HIGHEST_PRECEDENCE) // You can use the @ Order annotation or implement the Order interface @Component // Use the @ Component annotation to add this exception parser to the container public class CustomHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { request.setAttribute("exception", ex); return new ModelAndView("myExceptionPage"); } }
Generally, the highest priority is not required, otherwise all exceptions will be resolved by this exception parser.
2. Principle of exception handling
This paper analyzes the principle of exception handling in Web development with SpringBoot
2.1 automatic configuration principle of exception handling
The automatic configuration class of SpringBoot's exception handling mechanism for spring MVC is ErrorMvcAutoConfiguration.
At org springframework. boot. autoconfigure. web. servlet. Error package.
Analyze the content of automatic configuration class, which mainly includes five components:
Component name | Component type |
---|---|
errorAttributes | DefaultErrorAttributes |
basicErrorController | BasicErrorController |
conventionErrorViewResolver | DefaultErrorViewResolver |
error | StaticView |
beanNameViewResolver | BeanNameViewResolver |
The following is a brief introduction to the functions of these five components. The detailed usage is learned when analyzing the default exception handling process in 2.2.
2.1.1 component: DefaultErrorAttributes
DefaultErrorAttributes implements the ErrorAttributes interface, which is used to obtain various data of exceptions, namely the 8 parameters mentioned above.
Its getErrorAttributes method:
// org.springframework.boot.web.servlet.error.DefaultErrorAttributes.getErrorAttributes(org.springframework.web.context.request.WebRequest, org.springframework.boot.web.error.ErrorAttributeOptions) // DefaultErrorAttributes.java Line:109~128 @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { // Call another getErrorAttributes method to get the encapsulated exception information in the request Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE)); // Next, eliminate unnecessary parameters according to the configuration file if (Boolean.TRUE.equals(this.includeException)) { options = options.including(Include.EXCEPTION); } if (!options.isIncluded(Include.EXCEPTION)) { errorAttributes.remove("exception"); } if (!options.isIncluded(Include.STACK_TRACE)) { errorAttributes.remove("trace"); } if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) { errorAttributes.put("message", ""); } if (!options.isIncluded(Include.BINDING_ERRORS)) { errorAttributes.remove("errors"); } return errorAttributes; }
Another getErrorAttributes method called:
// org.springframework.boot.web.servlet.error.DefaultErrorAttributes.getErrorAttributes(org.springframework.web.context.request.WebRequest, boolean) // DefaultErrorAttributes.java Line:130~139 @Override @Deprecated // It is not recommended to use this method directly. It is recommended to use the previous getErrorAttributes method according to the configuration file public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { // Use a Map to save various information of the obtained exception Map<String, Object> errorAttributes = new LinkedHashMap<>(); // Add timestamp errorAttributes.put("timestamp", new Date()); // Add HTTP status code addStatus(errorAttributes, webRequest); // Add the details of the exception (exception, exception information, stack information and data binding error) addErrorDetails(errorAttributes, webRequest, includeStackTrace); // Add request path with exception addPath(errorAttributes, webRequest); return errorAttributes; }
These addition methods use getAttribute method to obtain information:
// org.springframework.boot.web.servlet.error.DefaultErrorAttributes.getAttribute // DefaultErrorAttributes.java Line:248~251 @SuppressWarnings("unchecked") private <T> T getAttribute(RequestAttributes requestAttributes, String name) { // The ServletWebRequest object indirectly implements the RequestAttributes interface // Therefore, the RequestAttributes method calls the underlying request Getattribute method to get the value return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST); }
At the same time, DefaultErrorAttributes implements the HandlerExceptionResolver interface as a handler exception parser for Spring.
Its resolveException method:
// org.springframework.boot.web.servlet.error.DefaultErrorAttributes.resolveException // DefaultErrorAttributes.java Line:98~103 @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // The storeErrorAttributes method has only one statement // request.setAttribute(ERROR_ATTRIBUTE, ex); // ERROR_ATTRIBUTE is an attribute of DefaultErrorAttributes, which is a string // The value is org springframework. boot. web. servlet. error. DefaultErrorAttributes. ERROR storeErrorAttributes(request, ex); // This method only puts the exception object into the request domain and does not parse and generate ModelAndView return null; }
2.1.2 component: BasicErrorController
BasicErrorController implements the ErrorController interface, which is Spring's default implementation of the error handler and is used to handle exception requests / errors by default,
The annotation @ RequestMapping("${server.error.path:${error.path:/error}}") on the class handles the / error request by default.
We know that there are two default views of SpringBoot response exceptions, one is the "Whitelabel"HTML view and the other is the JSON view.
Processor method to return HTML view:
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml // BasicErrorController.java Line:90~98 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // This method can only produce text/html media types public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { // Get HTTP status code HttpStatus status = getStatus(request); // Use the getErrorAttributes method of the parent class AbstractErrorController to obtain various information of the exception // The bottom layer uses the getErrorAttributes method of the errorAttributes attribute of the parent class Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); // Set the response status code for the response object response.setStatus(status.value()); // Use the resolveErrorView method of the parent class AbstractErrorController to parse the view // The bottom layer traverses all error view parsers and calls their resolveErrorView method to parse the view ModelAndView modelAndView = resolveErrorView(request, response, status, model); // If there are no errors and the view parser can parse the view, use Spring's default view error return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); }
Processor method to return JSON view:
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error // BasicErrorController.java Line:100~108 @RequestMapping // Return the user-defined response body type, and then convert the message according to the content negotiation results public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { // Get HTTP status code HttpStatus status = getStatus(request); // If the status code is 204 if (status == HttpStatus.NO_CONTENT) { // Only the status code is returned without other information return new ResponseEntity<>(status); } // Use the getErrorAttributes method of the parent class AbstractErrorController to obtain various information of the exception Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); // Return status code and information return new ResponseEntity<>(body, status); }
2.1.3 component: DefaultErrorViewResolver
DefaultErrorViewResolver implements the ErrorViewResolver interface, which is Spring's default implementation of the error view parser and is used to parse the error view.
Its resolveErrorView method:
@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // First, pass the status code directly into the resolve method ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); // If the resolve method cannot be resolved and the SERIES_VIEWS contains the current status code type if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { // The corresponding type SERIES_VIEWS passes in the resolve method modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; }
SERIES_VIEWS is a Map. DefaultErrorViewResolver initializes it:
// org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver // DefaultErrorViewResolver.java Line:62~67 static { // Create EnumMap collection with key as Series status code type and value as String type Map<Series, String> views = new EnumMap<>(Series.class); // Put the key as series CLIENT_ Error (i.e. 4), value is 4xx views.put(Series.CLIENT_ERROR, "4xx"); // Put the key as series SERVER_ Error (i.e. 5), value is 5xx views.put(Series.SERVER_ERROR, "5xx"); // Assign views to SERIES_VIEWS SERIES_VIEWS = Collections.unmodifiableMap(views); }
Status code matching method:
- First use the status code for accurate matching, such as 404 status code matching 404 html;
- If the exact matching cannot be matched, fuzzy matching shall be carried out again: for example, the type of series status code of 404 status code is 4, which is matched to series_ 4xx of views, the last view used is 4xx html
This is the 4xx custom error view, which can be used by any status code exception starting with 4.
resolve method:
// org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver.resolve // DefaultErrorViewResolver.java Line:122~130 private ModelAndView resolve(String viewName, Map<String, Object> model) { // Splice "error /" and status code or 4xx or 5xx, that is, the name of logical view. error/4xx corresponds to 4xx written by us html String errorViewName = "error/" + viewName; // Get the provider of the template. If there is a template engine, you can use the template engine to parse the view TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } // If there is no template engine available, call the resolveResource method to resolve the view itself // Directly stream the view file to the client return resolveResource(errorViewName, model); }
2.1.4 component: StaticView
StaticView is the default error view of "Whitelabel" defined by Spring. The view name is error,
If no error view parser can parse other error views, use this view as the default error view.
Since this view is added to the container by Spring, the BeanNameViewResolver view parser is required to use this view.
2.2 default exception handling process (source code)
Follow the debugging and learn more about the default exception handling process of Spring Web development.
Since we are learning the default exception handling process, we will not add any other error handling methods.
The processor method for debugging will generate mathematical operation exception (java.lang.ArithmeticException):
@RequestMapping("/test") @ResponseBody public Object testError() { int i = 1 / 0; // Classic 1 / 0 return "test"; }
Use the browser to request, enter debugging, and go to the doDispatch method of DispatcherServlet (omit other codes):
// org.springframework.web.servlet.DispatcherServlet.doDispatch // DispatcherServlet.java Line:1020~1100 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { // ... Omit other codes try { // Declare the dispatchException object to store the exceptions generated during the execution of the processor method Exception dispatchException = null; try { // Execute the processor method to get the ModelAndView object mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); } // Catch exception catch (Exception ex) { dispatchException = ex; } // Catch larger exceptions catch (Throwable err) { dispatchException = new NestedServletException("Handler dispatch failed", err); } // Processing execution results processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } // Catch exceptions generated during processing execution results catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } // Catch larger exceptions generated during processing execution results catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { // ... Omit other codes } }
Exceptions generated during the execution of the processor method are handled by the processDispatchResult method:
// org.springframework.web.servlet.DispatcherServlet.processDispatchResult // DispatcherServlet.java Line:1118~1134 private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; // If an exception occurs if (exception != null) { // First judge whether it is ModelAndViewDefiningException. The exception generated here is a mathematical operation exception, so skip if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { // Get processor Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); // Use the processHandlerException method to handle exceptions mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } // Did the handler return a view to render? if (mv != null && !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isTraceEnabled()) { logger.trace("No view rendering, null ModelAndView returned."); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Concurrent handling started during a forward return; } if (mappedHandler != null) { // Exception (if any) is already handled.. mappedHandler.triggerAfterCompletion(request, response, null); } }
processHandlerException method for handling exception calls:
// org.springframework.web.servlet.DispatcherServlet.processHandlerException // DispatcherServlet.java Line:1309~1349 @Nullable protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { // ... Omit other codes ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { // Traverse all processor exception parsers for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { // Call their resolveException to resolve the view exMv = resolver.resolveException(request, response, handler, ex); // If you successfully parse and get the view, jump out of the loop if (exMv != null) { break; } } } // Is the view successfully parsed and obtained if (exMv != null) { // If the view is empty if (exMv.isEmpty()) { // Put an exception in the request field_ The parameter of attribute, the content of which is the exception object request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } // If the view does not provide a view if (!exMv.hasView()) { // Get the default view name using the parse request object String defaultViewName = getDefaultViewName(request); if (defaultViewName != null) { exMv.setViewName(defaultViewName); } } if (logger.isTraceEnabled()) { logger.trace("Using resolved error view: " + exMv, ex); } else if (logger.isDebugEnabled()) { logger.debug("Using resolved error view: " + exMv); } // Expose the exception information of the request WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); // Return to view return exMv; } // If there is no processor exception, the parser can parse it and throw the exception directly throw ex; }
2.2.1 processor exception parser
The processor exception parser is used in the processHandlerException method to resolve the exception.
The handler exception resolver is used to handle exceptions that occur during processor mapping matching and processor method execution,
Resolve the exception to a view object.
Its essence is an interface HandlerExceptionResolver, which has a method:
// org.springframework.web.servlet.HandlerExceptionResolver public interface HandlerExceptionResolver { @Nullable ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); }
- resolveException: resolve exception information and return ModelAndView view view object.
Spring provides the following handler exception parser implementation classes:
name | effect |
---|---|
DefaultErrorAttributes | The default highest priority is used to put the exception object into the request domain. It does not provide parsing function (return null) |
ExceptionHandlerExceptionResolver | Use the exception handling method with @ ExceptionHandler annotation to handle exceptions |
ResponseStatusExceptionResolver | If the handler method with exception has @ ResponseStatus annotation, use the annotation content to parse and handle the exception |
DefaultHandlerExceptionResolver | Spring's default implementation of the processor exception parser only supports parsing spring MVC standard exceptions. The principle is to use response after translating the standard exception into HTTP status code Senderror method processing |
Returning to the debugging process, when the for loop traverses the processor exception parser, it gives priority to calling the resolveException method of the DefaultErrorAttributes component and puts the exception object into the request domain.
Then traverse other processor exception parsers, but we do not provide any other processing methods, so no processor exception parser can handle this exception, so the processHandlerException method finally threw this exception.
The exception is caught by the underlying Servlet. After a series of other operations, the underlying Servlet forwards it to the / error request and makes the request again.
/The error request is then processed by the method in the default error processor of the component BasicErrorController. Since we use the browser to make the request, it is processed by the errorHtml method of BasicErrorController. After returning the ModelAndView object, the normal view parsing process will follow. Respond the component StaticView, i.e. "Whitelabel" default view to the browser.