How Springboot implements a custom error page (detailed explanation of error handling mechanism)

Generally, when we are working on a project, the error mechanism is a necessary common sense. Basically every project will do error handling. It is impossible to jump directly to the original error page when the project reports an error. This blog mainly focuses on the default processing mechanism of springboot and self Define the error page processing to explain, friends who need it, follow the editor to learn together!

Here we use code examples to analyze his reasons and principles. Only by understanding the principles can we better use them in actual combat!

In the lower part of the blog is a small demo of myself, which is convenient for everyone to learn quickly!

If you feel boring about the principle, you can skip it directly and look at the code. After reading it, you can come back and look at it again if you are interested.

Example of default effect

Springboot has its own default processing mechanism. When you just create a springboot project to visit a path that does not exist, you will find that it will pop up such a message.

When we use the postman direct interface to access, we will find that what he returns is no longer a page. Response to a json data by default

At this time, someone should be thinking, how does springboot recognize whether we are page visitors?

Effect example reason

The default error handling mechanism of springboot is judged based on Accept in Headers. This parameter will be passed in whether it is postman access or page access.

When the page is accessed, he passes in test/html

And postman is this

Principle of error mechanism

We probably understand the reason, and then we will briefly understand his principle by looking at the source code.

A brief review of the principle of springboot

The reason why springboot is used out of the box is because many frameworks have already been configured for us. There are many AutoConfigurations in it, and the ErrorMvcAutoConfiguration class is the error mechanism configuration.

Stored in this jar package
Insert picture description here

In springboo 2.4 version, ErrorMvcAutoConfiguration is stored in this path

Springboot 1.5 version ErrorMvcAutoConfiguration is stored in this path

Of course, it is just that the class storage location has changed between versions, but the source code difference is not very big.

All the configurations used in springboot are taken from the container. The role of the container is to put the configuration instantiation process on startup. When we use it, we take it directly from the container without creating it. This is to develop around the container. The reason for this should be discovered when using springboot. If we want to modify some of the default configuration of springboot, we will try our best to put it in the container before it will take effect.

In the source code, you will find a large number of @ConditionalOnMissingBean, this is that if this configuration is configured in our project, springboot will not use his default configuration, just use our configuration.

ErrorMvcAutoConfiguration配置

ErrorMvcAutoConfiguration adds the following components to the container:

1、DefaultErrorAttributes

The error information in the page, and the access time, etc., are obtained in these two methods in DefaultErrorAttributes.

	@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
		Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
		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;
	}

	@Override
	@Deprecated
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, webRequest);
		addErrorDetails(errorAttributes, webRequest, includeStackTrace);
		addPath(errorAttributes, webRequest);
		return errorAttributes;
	}

2、BasicErrorController

Handling default/error requests

It is also the two methods of BasicErrorController to determine whether to return an error page or return json data

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

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

3、ErrorPageCustomizer

After an error occurs in the system, the error request is processed; (equivalent to the error page rule registered by web.xml)

4、DefaultErrorViewResolver

DefaultErrorViewResolverConfiguration inner class

Here we can see that he injected DefaultErrorViewResolver into the container

There are two methods in the DefaultErrorViewResolver object to complete the page jump according to the state.

	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map<String, Object> model) {
			
		//获取错误状态码,这里可以看出他将状态码传入了resolve方法
		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) {
		//从这里可以得知,当我们报404错误的时候,他会去error文件夹找404的页面,如果500就找500的页面。
		String errorViewName = "error/" + viewName;
		//模板引擎可以解析这个页面地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
				.getProvider(errorViewName, this.applicationContext);
		//模板引擎可用的情况下返回到errorViewName指定的视图地址
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		//模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
		return resolveResource(errorViewName, model);
	}

Component execution steps

Once the system has an error such as 4xx or 5xx; ErrorPageCustomizer will take effect (customized error response rules); it will come to the /error request; it will be processed by BasicErrorController; which page to go to is parsed by DefaultErrorViewResolver;

Code example

Here I choose to directly upload the code, so that everyone can get started faster.

1. Import dependencies

Here I quoted the thymeleaf template, and the page jump function is configured for us inside springboot.

This is a blog about thymeleaf written by me. If you haven't used it or don't know it well, you can learn it!

thymeleaf learning: https://blog.csdn.net/weixin_43888891/article/details/111350061 .

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-tomcat</artifactId>
		<scope>provided</scope>
	</dependency>
</dependencies>

2. Custom exception

Function: In the face of some errors that report a null pointer because no data is found, we can manually throw an exception.

package com.gzl.cn;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {

    public NotFoundException() {
    }

    public NotFoundException(String message) {
        super(message);
    }

    public NotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

3. Define exception interception

package com.gzl.cn.handler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class ControllerExceptionHandler {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());


    @ExceptionHandler(Exception.class)
    public ModelAndView exceptionHander(HttpServletRequest request, Exception e) throws Exception {
        logger.error("Requst URL : {},Exception : {}", request.getRequestURL(),e);
		
		//假如是自定义的异常,就让他进入404,其他的一概都进入error页面
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
            throw e;
        }

        ModelAndView mv = new ModelAndView();
        mv.addObject("url",request.getRequestURL());
        mv.addObject("exception", e);
        mv.setViewName("error/error");
        return mv;
    }
}

4. Create a test interface

package com.gzl.cn.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.gzl.cn.NotFoundException;

@Controller
public class HelloController {

    //这个请求我们抛出我们定义的错误,然后被拦截到直接跳到404,这个一般当有一些数据查不到的时候手动抛出
    @GetMapping("/test")
    public String test(Model model){
        String a = null;
        if(a == null) {
        	throw new NotFoundException();
        }
        System.out.println(a.toString());
        return "success";
    }
    
    //这个请求由于a为null直接进500页面
    @GetMapping("/test2")
    public String test2(Model model){
        String a = null;
        System.out.println(a.toString());
        return "success";
    }
}

5. Create a 404 page

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>404</h2>
    <p>对不起,你访问的资源不存在</p>
</body>
</html>

6. Create an error page

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>错误</h2>
    <p>对不起,服务异常,请联系管理员</p>
    
	<!--这段代码在页面不会展现,只会出现在控制台,假如线上报错可以看控制台快速锁定错误原因-->    
	<div>
	    <div th:utext="'&lt;!--'" th:remove="tag"></div>
	    <div th:utext="'Failed Request URL : ' + ${url}" th:remove="tag"></div>
	    <div th:utext="'Exception message : ' + ${exception.message}" th:remove="tag"></div>
	    <ul th:remove="tag">
	      <li th:each="st : ${exception.stackTrace}" th:remove="tag"><span th:utext="${st}" th:remove="tag"></span></li>
	    </ul>
	    <div th:utext="'--&gt;'" th:remove="tag"></div>
	</div>
</body>
</html>

7. Project structure

8. Running effect

http://localhost:8080/test2

At this time, it can be observed that that piece of code takes effect here. The advantage of this is that the customer cannot see it, but it is not beautiful when it is seen, so this method is adopted.

Visit a page that does not exist

Visit http://localhost:8080/test
this time, you will find that he jumped to the 404 page

Remember to like the editor!

Guess you like

Origin blog.csdn.net/weixin_43888891/article/details/112254711