[SpringBoot] A vernacular analysis of the source code of the default error handling mechanism, and custom configuration (in case of a pit)

Preface

SpringBoot version: v2.2.6.RELEASE

This blog mainly summarizes the following 3 aspects:

  • One, SpringBoot error page and data
  • 2. Introduction of related classes
  • Three, source code tracking, the flow of the default processing mechanism
  • Four, custom error data

From the perspective of learners, this blog presents the entire hierarchy and ideas clearly and clearly with popular vernacular, pictures and texts. I believe that if you finish reading it patiently, you should have a different harvest. The source code of any framework is a huge system. Reading the source code does not require an exact understanding of every detail of the code, but should pay attention to and understand the main idea of ​​the main (key) code. Finally, why not like the collection after reading it? I spent most of the day summarizing it, and I feel that I have gained a lot, and I have a clearer and deeper understanding of SpringBoot.

One, SpringBoot error page and data

1. The visitor is a browser

2. The visitor is another client (here, I use Postman to test)

3. How to distinguish visitors? Request header!

The visitor is a browser:

The visitor is another client:

From the above point of view, we can divide SpringBoot's error handling into two categories:

(1) The visitor is a browser, and a corresponding error page is returned

(2) If the visitor is a customer other than the browser, Duan will return the corresponding Json data

 

2. Introduction of related classes

For the key classes here, only a brief introduction, you only need to know the main role that this class plays in the entire default error handling mechanism. In the fourth part of the process description, we will design these classes in depth.

ErrorMvcAutoConfiguration : As the name suggests, this is an automatic configuration class of the error handling mechanism

Looking at the source code, it is found that this class adds the following 3 important components related to the error handling mechanism to the container:

 

1. ErrorPageCustomizer : Customize error generation rules and generate /error requests

2. BasicErrorController : As the name suggests, it is a Controller that processes /error requests

(1) Return to the Html page

(2) Return Json data

3. DefaultErrorViewResolver : Resolution of the error view

4. DefaultErrorAttributes : share information about errors

 

Three, source code tracking, the flow of the default processing mechanism

Take response Html page as an example

1. As you can see from the above introduction, when an error page is returned, this method in BasicErrorController will be called (see the figure below).

In this method, get the requested status code and some Model data (the getErrorAttributes() method is also very important, which will be introduced later), and then call the resolveErrorView() method to resolve the view, and return the corresponding error view. ModelAndView contains the error page address And page content.

Click into the this.resolveErrorView() method and jump to the AbstractErrorController class, as shown in the figure below.

resolveErrorView(), as the name implies: resolve error view! The general meaning of this method is to get all the error view resolvers (ErrorViewResolver), and then traverse one by one, as long as a ModelAndView (matching the corresponding error view) is successfully obtained, it will return immediately.

 

2. Pay attention to the code near the horizontal line in the screenshot above.

What is ErrorViewResolver? Click to see that it is an interface. The resolveErrorView() called by the dashed part is also a method in this interface. Place the mouse near "ErrorViewResolver", and then ctrl + H to view its architecture. Found that the interface has an implementation class: DefaultErrorViewResolver

Is it familiar? DefaultErrorViewResolver is a component mentioned in the previous introduction. Then we enter DefaultErrorViewResolver to see how it resolves the error view (the screenshot below).

What do these 3 methods mean? See the code comments below.

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, 
                                        Map<String, Object> model) {
        // 根据状态码精确匹配错误页面
	ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);

	if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
                // 如果无法找到准确状态码的错误页面,就模糊匹配:4xx(客户端错误),5xx(服务端错误)
		modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
	}

	return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
	// 从上面的方法可以看出来,这里传入的viewName实际上就是状态码
	// 拼接视图名称,例如:error/404
	String errorViewName = "error/" + viewName;
	
	// 模板引擎可以解析这个页面地址就用模板引擎解析
	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
			.getProvider(errorViewName, this.applicationContext);
								
	
	
	return provider != null ? 
        // 模板引擎可用的情况下返回到errorViewName指定的视图地址
                new ModelAndView(errorViewName, model) 
        // 否则就在静态资源文件夹下找errorViewName对应的页面,例如:error/404.html
                : this.resolveResource(errorViewName, model);
}

 

3. Follow the train of thought and get here. In fact, we can clearly know how to customize our own error page! Here I will mention it along the lines of thought, and I will summarize it later.

From the above we know that as long as we create a new error folder under the static or templates folder under resources, and then put the Html page corresponding to the status code (named after the status code, for example: 404.html) under the error folder . Or simply put 4xx.html, 5xx.html, so that all client-side errors will use 4xx.html page, and server-side errors will use 5xx.html page.

However, from the above code analysis, we can also know that if 404.html and 4xx.html exist at the same time, SpringBoot will give priority to 404.html.

 4. Up to the above, we know how to change the static page corresponding to the error state. But what if I need to retrieve the error message from the page? For example, I want to retrieve the timestamp, status code, exception information, etc. of this error. How do we take out this information and display it on our own static page?

Is this information familiar? Yes, when the visitor is a non-browser, the Json data returned by SpringBoot includes this information. Then on the default error page of SpringBoot, related information is also displayed. So when we customize the error page ourselves, SpringBoot must also provide us with these data for us to use. We can even add additional information!

Regarding this issue, where is the entry point for the source code? In fact, it was also mentioned when BasicErrorController was mentioned at the beginning (see the figure below). This time we pay attention to the getErrorAttributes() method, and you can guess what it means by looking at the name. Click in.

Jump to the base class AbstractErrorController of BasicErrorController (see the figure below), and then click to enter (getErrorAttributes() in the underlined part of the figure below).

Come to the interface of ErrorAttributes. We ctrl + H to view the architecture of this interface.

Sure enough, it has an implementation class: DefaultErrorAttributes. Isn't it familiar? It is also one of the 4 key components mentioned above. Attention here! ! ! There are two DefaultErrorAttributes with the same name in SpringBoot. What we want to look at is under the org.springframework.boot.web.servlet.error package. Make no mistake! ! !

 

5. Come to DefaultErrorAttributes and see how it shares error information and how to share error information.

Looking at this class, we found: The following information is stored in the Map:

timestamp: timestamp

status: status code

error: error message

exception: exception object

message: exception message

errors: JSR303 data verification errors are here

trace: stack information

This information can actually be retrieved in the form of variables from the page through the thymeleaf tag. Examples are as follows:

Pay attention to a small problem here. The version of SpringBoot I am using has no way to retrieve the Exception information by default. The reason can be known from the source code (I will not go into it here, I will post a picture as an entry point, you can follow the source code yourself), in order to solve this problem, only need to add the following configuration in the configuration file: server.error. include-exception=true

the reason:

Four, custom error data

So far, we know:

(1) How to customize your own error page

(2) How to use the existing error information in the custom error page

(3) Understand the flow of SpringBoot's default error handling mechanism

I believe that you have a deeper understanding of the key categories mentioned in "II". So, now I also want to customize the error data. In other words, I want to share the wrong information I want. This error message is customized by me, I added it to the shared domain, and then I used it in my error page!

After an error occurs, it will come to the /error request, which will be processed by BasicErrorController. The error data that can be obtained in response is obtained by getErrorAttributes (the method specified by AbstractErrorController (ErrorController)). If we want to add custom error information, we can write an implementation class of ErrorController [or a subclass of AbstractErrorController], put it into the container, and completely replace SpringBoot's DefaultErrorAttributes component.

code show as below:

package com.ysq.springbootweb.component;


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

import java.util.Map;

/**
 *
 * @author passerbyYSQ
 * @create 2020-07-29 16:16
 */
@Component // 给容器中加入我们自己的ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {

    //  如果不复写,Exception又获取不到了
    public MyErrorAttributes() {
        super(true);
    }

    /**
     * 这里返回的Map,就是页面和Json数据能获取到的所有字段
     * @param webRequest
     * @param includeStackTrace
     * @return
     */
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        // 额外信息
        map.put("company", "ysq");

        return map;
    }

}

Here we will encounter two problems:

(1) If the null parameter construction method is not overwritten, the current version of SpringBoot will not get Exception information. Regarding the acquisition of Exception, I mentioned earlier. But what is the situation now?

Originally, SpringBoot used DefaultErrorAttributes, and DefaultErrorAttributes did not put Exception information in the shared domain by default. What we have previously modified through the configuration file is to allow DefaultErrorAttributes to put Exception information in the shared domain. Today, we completely replace the DefaultErrorAttributes component by writing a subclass of DefaultErrorAttributes and putting it in the container. SpringBoot will not use DefaultErrorAttributes, but instead completely use our custom component MyErrorAttributes. Therefore, the configuration of the configuration file is invalid. Therefore, we can directly override the null parameter structure of the parent class of MyErrorAttributes, allowing to obtain Exception information.

This question is an episode. The following question (2) is the focus.

(2) Write MyErrorAttributes directly, override the parent method getErrorAttributes, and add custom error information to the Map before the method returns the result. This leads to a problem: no matter what the error is, the same error message will be added. I want to add specific information for different errors.

For example: a 500 error caused by a custom exception, add its unique information for the error page to access.

How to solve this demand? See code below

package com.ysq.springbootweb.controller;

import com.ysq.springbootweb.exception.UserNotExistException;
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;
import java.util.HashMap;
import java.util.Map;

/**
 * @author passerbyYSQ
 * @create 2020-07-29 15:47
 */
@ControllerAdvice // 在SpringMVC要成为异常处理器,需要增加此注解
public class MyExceptionHandler {

    // 只要发生UserNotExistException异常,SpringMVC就调用该方法,并将异常对象对传递进来
    @ExceptionHandler(UserNotExistException.class) 
    public String handleException(Exception e, HttpServletRequest request) {

        Map<String, Object> map = new HashMap<>();

        // 需要传入我们自己的错误状态
        // 如果不传,就默认200(成功),就不会进入自己定制的错误页面的解析流程
        // 虽然有定制的页面效果了,但是我定制的Map数据不生效
        request.setAttribute("javax.servlet.error.status_code", 500);

        // 自定义的错误信息
        map.put("code", "user not exist");
        map.put("message", "用户不存在!!!!");
        // 将额外的信息添加到请求域中,以供MyErrorAttributes取出
        request.setAttribute("ext", map);

        // 转发到/error请求,让BasicErrorController处理
        return "forward:/error"; 
    }
}

At the same time, make the following changes to MyErrorAttributes:

package com.ysq.springbootweb.component;


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

import java.util.Map;

/**
 *
 * @author passerbyYSQ
 * @create 2020-07-29 16:16
 */
@Component // 给容器中加入我们自己的ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {

    //  如果不复写,Exception又获取不到了
    public MyErrorAttributes() {
        super(true);
    }

    /**
     * 这里返回的Map,就是页面和Json数据能获取到的所有字段
     * @param webRequest
     * @param includeStackTrace
     * @return
     */
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        // 额外信息
        map.put("company", "ysq");

        // 针对UserNotExistException异常的错误信息
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        if (ext != null) {
            map.put("ext", ext);
        }

        return map;
    }

}

My page effect:

See friends here, don’t forget to like your favorites. Thank you!

Guess you like

Origin blog.csdn.net/qq_43290318/article/details/107669722