Analysis of Exception Error Handling Mechanism in Springboot Series Spring Boot web Development

Posted by JsF on Sat, 18 May 2019 06:24:11 +0200

Preface

I'm sure you'll often encounter this page when you first start experiencing Springboot, which is the default return page to a page that doesn't exist.


Spring Boot Default Error Page

If it is a request from another client, such as an interface test tool, the JSON data is returned by default.

{
        "timestamp":"2019-01-06 22:26:16",
        "status":404,
        "error":"Not Found",
        "message":"No message available",
        "path":"/asdad"
}

Clearly, SpringBoot handles responses differently based on the HTTP request header information. HTTP-related knowledge can be found here.

1. SpringBoot exception handling mechanism

Following the SpringBoot source can analyze the default error handling mechanism.

// org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
// Bind some error messages to 1
    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes(
                this.serverProperties.getError().isIncludeException());
    }
// Default processing/error count as 2
    @Bean
    @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
        return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
                this.errorViewResolvers);
    }
// Error handling pages are marked as 3
    @Bean
    public ErrorPageCustomizer errorPageCustomizer() {
        return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
    }
    @Configuration
    static class DefaultErrorViewResolverConfiguration {

        private final ApplicationContext applicationContext;

        private final ResourceProperties resourceProperties;

        DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
                ResourceProperties resourceProperties) {
            this.applicationContext = applicationContext;
            this.resourceProperties = resourceProperties;
        }
// Deciding which error page to go to is marked 4
        @Bean
        @ConditionalOnBean(DispatcherServlet.class)
        @ConditionalOnMissingBean
        public DefaultErrorViewResolver conventionErrorViewResolver() {
            return new DefaultErrorViewResolver(this.applicationContext,
                    this.resourceProperties);
        }

    }

With the above comments, the four methods in the code above are the main part of the Springboot implementation that returns the error page by default.

1.1. errorAttributes

ErorAttributes are literally translated into error attributes, which is exactly the way to track source code directly.
The code is located at:

// org.springframework.boot.web.servlet.error.DefaultErrorAttributes

This class shares a lot of error information for error situations, such as.

errorAttributes.put("timestamp", new Date());
errorAttributes.put("status", status);
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("exception", error.getClass().getName());
errorAttributes.put("message", error.getMessage());
errorAttributes.put("trace", stackTrace.toString());
errorAttributes.put("path", path);

This information is returned as shared information, so when we use the template engine, it is as easy to take out as other parameters.

1.2. basicErrorControll

Tracking the source content of the BasicErrorController directly reveals the following section of code.

// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
@Controller
// Define the request path, if there is no error.path path, then the path is/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
  
    // If supported format text/html
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        // Get the value to return
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        // Parse the error view information, which is the logic in 1.4 below
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        // Return to the view and use the default error view template if no page template exists
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        // If you accept HTTP requests in all formats
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        // Response to HttpEntity
        return new ResponseEntity<>(body, status);
    }  
}

As you can see from this, basicErrorControll is used to create a controller class for request returns and to return corresponding information in different formats that are acceptable for HTTP requests, so there are differences in the results returned when testing with browser and interface testing tools.

1.3. ererrorPageCustomizer

Look directly at the new ErrorPageCustomizer (this.serverProperties, this.dispatcher ServletPath) in the method;

//org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer
    /**
     * {@link WebServerFactoryCustomizer} that configures the server's error pages.
     */
    private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

        private final ServerProperties properties;

        private final DispatcherServletPath dispatcherServletPath;

        protected ErrorPageCustomizer(ServerProperties properties,
                DispatcherServletPath dispatcherServletPath) {
            this.properties = properties;
            this.dispatcherServletPath = dispatcherServletPath;
        }
        // Registration Error Page
        // this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())
        @Override
        public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
            //getPath() gets the following address, and if there is no custom error.path attribute, go/error location
            //@Value("${error.path:/error}")
            //private String path = "/error";
            ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
                    .getRelativePath(this.properties.getError().getPath()));
            errorPageRegistry.addErrorPages(errorPage);
        }

        @Override
        public int getOrder() {
            return 0;
        }

    }

As you can see from the above, when an error is encountered, the request is forwarded to/error if there is no custom error.path attribute.

1.4. conventionErrorViewResolver

From the code above, take a step-by-step look at the default error handling implementation of SpringBoot and the conventionErrorViewResolver method.Below is some code for the DefaultErrorViewResolver class, comment resolution.

// org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver
    
// Initialization parameter, key is the first bit of HTTP status code.
    static {
        Map<Series, String> views = new EnumMap<>(Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
            Map<String, Object> model) {
        // Use the HTTP full status code to check if there are any pages to match
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            // Create a view object using parameters in the first bitmatch initialization of the HTTP status code
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }

    
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        // Split Error View Path/eroor/[viewname]
        String errorViewName = "error/" + viewName;
        // Attempting to create a view object using a template engine
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
                .getProvider(errorViewName, this.applicationContext);
        if (provider != null) {
            return new ModelAndView(errorViewName, model);
        }
        // No template engine, use static resource folders to resolve views
        return resolveResource(errorViewName, model);
    }

    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        // Traverse through the static resource folder to check for views
        for (String location : this.resourceProperties.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    return new ModelAndView(new HtmlResourceView(resource), model);
                }
            }
            catch (Exception ex) {
            }
        }
        return null;
    }

Thymeleaf parses error pages.

//org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider
public class ThymeleafTemplateAvailabilityProvider
        implements TemplateAvailabilityProvider {
    @Override
    public boolean isTemplateAvailable(String view, Environment environment,
            ClassLoader classLoader, ResourceLoader resourceLoader) {
        if (ClassUtils.isPresent("org.thymeleaf.spring5.SpringTemplateEngine",
                classLoader)) {
            String prefix = environment.getProperty("spring.thymeleaf.prefix",
                    ThymeleafProperties.DEFAULT_PREFIX);
            String suffix = environment.getProperty("spring.thymeleaf.suffix",
                    ThymeleafProperties.DEFAULT_SUFFIX);
            return resourceLoader.getResource(prefix + view + suffix).exists();
        }
        return false;
    }
}

Thus, we can know that the error page first checks the / error/HTTP status code file under the template engine folder, if it does not exist, then checks the / error/4xx or / error/5xx files under the template engine, and if it does not exist, checks the corresponding files under the static resource folder.

2. Customize Exception Page

From the source analysis of the SpringBoot error mechanism above, you know that when an error occurs, SpringBoot will first return to the / error/HTTP status code file under the Template Engine folder. If it does not exist, check the / error/4xx or / error/5xx files under the De-Template Engine. If it does not exist, check the corresponding files under the static resource folder.It also shares some error information when returned, which can be used directly in the template engine.

errorAttributes.put("timestamp", new Date());
errorAttributes.put("status", status);
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("exception", error.getClass().getName());
errorAttributes.put("message", error.getMessage());
errorAttributes.put("trace", stackTrace.toString());
errorAttributes.put("path", path);

Therefore, you need to customize the error page by preventing 4xx or 5xx files in the error folder under the template folder.

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>[[${status}]]</title>
    <!-- Bootstrap core CSS -->
    <link href="/webjars/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body >
<div class="m-5" >
    <p>Error code:[[${status}]]</p>
    <p >Information:[[${message}]]</p>
    <p >Time:[[${#dates.format(timestamp,'yyyy-MM-dd hh:mm:ss ')}]]</p>
    <p >Request path:[[${path}]]</p>
</div>

</body>
</html>

No path exists for random access.

Spring Boot Custom Error Page

3. Custom error JSON

Based on the analysis of SpringBoot error handling principle above, it is known that the JSON information returned is converted from a map object, so as long as the value in the map can be customized, you can customize the JSON format of the error information.You can simply override the getErrorAttributes method of the DefaultErrorAttributes class.

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * Custom error message JSON value
 *
 * @Author niujinpeng
 * @Date 2019/1/7 15:21
 */
@Component
public class ErrorAttributesCustom extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        String code = map.get("status").toString();
        String message = map.get("error").toString();
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("code", code);
        hashMap.put("message", message);
        return hashMap;
    }
}

Request tests using postman.

4. Unified exception handling

Unified exception handling can be achieved using @ControllerAdvice with the @ExceptionHandler annotation, and the @ExceptionHandler annotated class automatically applies to each method annotated by @RequestMapping.Throw layer by layer when an exception occurs in the program

import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 * Unified exception handling
 *
 * @Author niujinpeng
 * @Date 2019/1/7 14:26
 */

@Slf4j
@ControllerAdvice
public class ExceptionHandle {

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Response handleException(Exception e) {
        log.info("abnormal {}", e);
        if (e instanceof BaseException) {
            BaseException exception = (BaseException) e;
            String code = exception.getCode();
            String message = exception.getMessage();
            return ResponseUtill.error(code, message);
        }
        return ResponseUtill.error(ResponseEnum.UNKNOW_ERROR);
    }
}

The request exception page was responded as follows.

{
 "code": "-1",
 "data": [],
 "message": "unknown error"
}

The article code has been uploaded to GitHub Spring Boot Web Development-Error Mechanism.

<Finish>
This article originates from a personal blog: https://www.codingme.net Please indicate the source for reprinting.

Topics: SpringBoot Thymeleaf Spring JSON