SpringBoot day 16 - Web exception handling

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 nameinformationConfiguration noderemarks
statusHTTP status code-Always open
timestamptime stamp-Always open
errorerror message-Always open
exceptionException typeserver.error.include-exceptionIt is closed by default and needs to be opened manually
messageAbnormal informationserver.error.include-messageIt is closed by default and needs to be opened manually
pathRequest path-Always open
traceStack informationserver.error.include-stacktraceIt is closed by default and needs to be opened manually
errorsBinding error messageserver.error.include-binding-errorsIt 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:

  1. Global exception handling: the method annotated by @ ExceptionHandler in the class can handle all exceptions generated by the Controller;
  2. Global data binding: the methods annotated by @ InitBinder in the class can be used for global data binding;
  3. 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 nameComponent type
errorAttributesDefaultErrorAttributes
basicErrorControllerBasicErrorController
conventionErrorViewResolverDefaultErrorViewResolver
errorStaticView
beanNameViewResolverBeanNameViewResolver

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:

  1. First use the status code for accurate matching, such as 404 status code matching 404 html;
  2. 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:

nameeffect
DefaultErrorAttributesThe default highest priority is used to put the exception object into the request domain. It does not provide parsing function (return null)
ExceptionHandlerExceptionResolverUse the exception handling method with @ ExceptionHandler annotation to handle exceptions
ResponseStatusExceptionResolverIf the handler method with exception has @ ResponseStatus annotation, use the annotation content to parse and handle the exception
DefaultHandlerExceptionResolverSpring'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.

Keywords: Java Spring Boot Back-end Spring MVC

Added by ochi on Tue, 01 Feb 2022 14:46:03 +0200