Spring boot (11) - exception handling and custom error page

Posted by scott_ttocs46 on Tue, 26 Nov 2019 08:39:14 +0100

1. Abnormal source

To handle an exception that occurs in a program, you first need to know where the exception comes from?

1. The front-end wrong request path will cause 4xx errors in the program, the most common is 404. By default, when this error occurs in the spring boot request path, the pc response page is as follows

If it is mobile terminal (mobile terminal), it will respond to json format data, as follows

2. Spring boot exception handling principle

Why do we ask for the wrong path, and boot will return us an error page or json format data? What is the principle?

After the Springboot project is started, execute the main method of the startup class annotated with @ SpringBootApplication, and load it through @ EnableAutoConfiguration

All configuration classes in META-INF/spring.factories under springbootAutoConfiguration.jar package (after these configuration classes are loaded, the components in each configuration class will be injected into the container and then used) one of the autoconfiguration classes

ErrorMvcAutoConfiguration, through the code, you can see that the following four components are used

DefaultErrorAttributes,BasicErrorController,errorPageCustomizer,DefaultErrorViewResolver

The other three are basically similar. When an error such as 4xx or 5xx occurs, the errorPageCustomizer will take effect. this.properties.getError().getPath()) and come to the / error request. Core code

//errorPageCustomizer
@Value("${error.path:/error}")
    private String path = "/error";

The / error request is processed by the BasicErrorController, which is a Controller. There are two processing methods, one is HTML,

One is JSON format. Where the visitor's information can be obtained from getErrorAttributes. DefaultErrorAttributes is the implementation class of ErrorAttributes.

critical code

@RequestMapping(produces = "text/html") //HTML
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
    }

    @RequestMapping
    @ResponseBody //JSON
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }

When in HTML mode, a resolveErrorView class is built, and resolveErrorView continues to call ErrorViewResolver. critical code

protected ModelAndView resolveErrorView(HttpServletRequest request,
            HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
        for (ErrorViewResolver resolver : this.errorViewResolvers) {
            ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
            if (modelAndView != null) {
                return modelAndView;
            }
        }
        return null;
    }

When we do not make a custom configuration, ErrorViewResolver will point to DefaultErrorViewResolver.

static {
        //You can use 4xx and 5xx filenames to uniformly match errors, but the principle of precise priority will be followed
        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) {
        ModelAndView modelAndView = resolve(String.valueOf(status), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //Splice error code after error
        String errorViewName = "error/" + viewName;
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
                .getProvider(errorViewName, this.applicationContext);
        //If the template engine is available, let the template engine parse for example: Template/error/404
        if (provider != null) {
            return new ModelAndView(errorViewName, model);
        }
        //If the template engine is not available, find the resource file in the static resource folder, error/404
        return resolveResource(errorViewName, model);
    }

3. How to customize error and exception response

Understand the exception handling mechanism of boot, how can we customize the exception response rules?

First, the pc returns the static error page, and the mobile returns the boot default json data

If there is a template engine (jsp,thmeleaf,freemarker) in the project, you can name the error page as the status code. html and put it in the error folder under the template engine folder,

If an exception occurs, whether it is a front-end request or a back-end program error, it will come to the corresponding error page. You can name the error page 4xx and 5xx to match all the errors,

However, the exact status code. html page is preferred; and the following information can be obtained on the template engine page

Here the template engine uses thmeleaf

4xx code

<!DOCTYPE html>
<html lang="en">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="'Status code:'+${status}"></title>
</head>
<body>
< img src="../images/404.jpg" style="width: 40%;">
<h1 th:text="'time:'+${timestamp}"></h1>
<h1 th:text="'Error prompt:'+${error}"></h1>
<h1 th:text="'Exception object:'+${exception}"></h1>
<h1 th:text="'Abnormal information:'+${message}"></h1>
</body>
</html>
5xx code
<!DOCTYPE html>
<html lang="en">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="'Status code:'+${status}"></title>
</head>
<body>
<h2>500</h2>
<h1 th:text="'time:'+${timestamp}"></h1>
<h1 th:text="'Error prompt:'+${error}"></h1>
<h1 th:text="'Exception object:'+${exception}"></h1>
<h1 th:text="'Abnormal information:'+${message}"></h1>
</body>
</html>
We requested a wrong address path

We create an exception in the program code and request a response

Above is how to handle errors and exceptions with template engine,

If there is no template engine in the project (the template engine cannot find the error page), the corresponding 4xx or 5xx or more accurate error page will be found under the static resource folder. But if you don't use the template engine, the page can't get the above page information;

The above two methods use mobile access to return the boot default json data

Second, the pc side returns the dynamic page, and the mobile side returns the dynamic json data

The first one above can easily handle exceptions. You only need to place a static page (without a template engine) or a page (with a template engine) with relevant information in the specified path.

The disadvantage is that we can't carry the data we want to display on the page. For example, when an exception is released somewhere in our program, we need to return the error information we prompted. How to deal with this kind of exception?

By default, in Spring Boot, all the exception data is actually the first five pieces of data shown. These data are defined in the org.springframework.boot.web.reactive.error.DefaultErrorAttributes class, and specifically in the getErrorAttributes method: the core code is as follows

@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
                boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        errorAttributes.put("path", request.path());
        Throwable error = getError(request);
        HttpStatus errorStatus = determineHttpStatus(error);
        errorAttributes.put("status", errorStatus.value());
        errorAttributes.put("error", errorStatus.getReasonPhrase());
        errorAttributes.put("message", determineMessage(error));
        handleException(errorAttributes, determineException(error), includeStackTrace);
        return errorAttributes;
}

The DefaultErrorAttributes class itself is defined in the org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration exception autoconfiguration class,

If the developer does not provide an instance of ErrorAttributes, then Spring Boot will automatically provide an instance of ErrorAttributes, which is DefaultErrorAttributes.

Based on this, developers can customize ErrorAttributes in two ways:

1. Directly implement the ErrorAttributes interface
Inherit DefaultErrorAttributes (recommended), because the processing of exception data in DefaultErrorAttributes has been completed, and developers can use it directly.

package com.javayihao.top.config;

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

import java.util.Map;

@Component
public class MyErrorAttributes  extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        if ((Integer)map.get("status") == 500) {
            //Set according to your own needs here
            map.put("message", "Server internal error!");
        }
        if ((Integer)map.get("status") == 404) {
            map.put("message", "path does not exist!");
        }
        return map;
    }
}

Our server access error path

Client response

Access controller with exception

Client response

Of course, I can throw an exception anywhere in the program and use the global exception handler to handle it

Custom exception

Global exception handler

@ControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(MyException.class)
    public String jsonErrorHandler(HttpServletRequest request, Exception e) {
        Map<String, Object> map = new HashMap<>();
        request.setAttribute("java.servlet.error.status_code", 500);
        map.put("code", -1);
        map.put("msg", e.getMessage());
        request.setAttribute("ext", map);
        //Forward to error
        return "forward:/error";
    }
}

Custom ErrorAttributes

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    //The returned map is all the fields that the page or json can get
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        //Additional content can be added
        map.put("company", "javayihao");
        //Take out the data carried in the exception processor
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);//Passing in 0 means getting from request
        map.put("ext", ext);
        return map;
    }
}

 

The third type: the pc side returns dynamic json data, and the mobile side also returns dynamic json data

This method is mainly aimed at the project with front and back separation. We define an exception to throw in the program

Define a return result object (or use map directly without definition) to store exception information

Define a global exception handler

/*ControllerAdvice Used to configure the package and annotation types that need exception handling,
 For example, @ ControllerAdvice(annotations = RestController.class)
 Only classes marked with rescontrolle will be blocked
 */
@ControllerAdvice
public class MyExceptionHandler {
    //The exception created by yourself can be returned according to the information written by yourself
    @ExceptionHandler(value = MyException.class)
    @ResponseBody
    public ErrorInfo<String> errorInfo(HttpServletRequest req, MyException e) {
        ErrorInfo<String> r = new ErrorInfo<>();
        r.setCode(ErrorInfo.ERROR);
        r.setMessage(e.getMessage());
        r.setData("test data");
        r.setUrl(req.getRequestURL().toString());
        return r;
    }
    //When the system is abnormal, the returned exception number is - 1, and the returned exception information is "the system is under maintenance"; the original exception information cannot be returned
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ErrorInfo<String> errorInfo(HttpServletRequest req, Exception e) {
        ErrorInfo<String> r = new ErrorInfo<>();
        r.setCode(ErrorInfo.ERROR);
        r.setMessage("System maintenance in progress");
        return r;
    }
}


Topics: Java JSON Mobile Spring Thymeleaf