SpringBoot 错误处理机制的简单理解

  • 1 默认的错误响应结果
  • 2 默认的错误处理机制
    • 2.1 步骤
    • 2.2 原理
    • 2.1.1 ErrorPageCustomizer
    • 2.2 BasicErrorController
    • 2.3 DefaultErrorAttributes
    • 2.4 、DefaultErrorViewResolver
  • 3 自定义错误页面
    • 3.1 不转发到 /error ,自定义异常处理和返回定制的json数据
    • 3.2 转发到 /error 进行自适应响应效果处理

1 默认的错误响应结果

不同的客户端,响应的结果不同

SpringBoot 根据 请求头的 Accept 来选择响应

Accept:指定客户端能够接收的内容类型。

  • 响应一个html 页面
    请求头
    SpringBoot 错误处理机制的简单理解_第1张图片
    响应
    SpringBoot 错误处理机制的简单理解_第2张图片

  • 响应一条json数据
    请求头
    SpringBoot 错误处理机制的简单理解_第3张图片
    响应
    SpringBoot 错误处理机制的简单理解_第4张图片

可以看到,
只要发生 4XX 或者 5XX 类型的错误,SpringBoot都会返回一个状态码以及一个错误页面;
不同的Accept,得到的响应结果不同。这其实都和Springboot 处理错误的机制有关。


2 默认的错误处理机制

SpringBoot 自动装配了处理所有错误的配置类 ErrorMvcAutoConfiguration

2.1 步骤

(1) ErrorPageCustomize 注册错误页面对象 ErrorPager:把默认的/error路径下对应的错误页面注入到Servlet中,在请求访问异常的时候,会转发到 /error 路径下

(2) 发生 4xx 或者 5xx 错误

(3) 转发到/error请求,被BasicErrorController处理

(4)BasicErrorController 利用

2.2 原理

SpringBoot 启动,SpringBoot 利用 ErrorMvcAutoConfiguration 往 IOC 容器中 注入组件,SpringBoot 的默认错误处理机制,依赖于这些组件。

  • ErrorPageCustomizer :用于在 Servlet 中 注册错误页面对象 ErrorPage
  • BasicErrorController : 统一异常处理 Controller,处理默认/error请求
  • DefaultErrorViewResolver : 默认的错误视图解析器,解析要跳转到哪个错误页面
  • 、DefaultErrorAttributes : 一个专门收集 error 发生时错误信息的bean,把错误信息注入到错误页面中

2.1.1 ErrorPageCustomizer

ErrorPageCustomizerErrorMvcAutoConfiguration的一个静态内部类;

通过调用registerErrorPages方法的getPath()方法最终得到错误页面路径;

errorPageRegistry.addErrorPages(errorPage)/error路径下对应的错误页面注入到Servlet中,在请求访问异常的时候,会转发到 /error 路径下。

//org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer

/**
 * 注入 ErrorPageCustomizer 组件
 */
@Bean
public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
    return new ErrorMvcAutoConfiguration.ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}

/**
 * ErrorPageRegistrar 接口 用于定义在发生异常时该有哪些错误页面被添加
 * 
 */
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;
    }

    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        // 根据错误页面的路径,实例化错误页面对象
        // getPath()得到错误页面路径,如果没有自定义error.path属性,则去/error位置
        ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
        // 添加处理异常时将使用的错误页对象
        errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage});
    }

    public int getOrder() {
        return 0;
    }
}

// getPath() 来获取需要解析的错误页面路径
public class ErrorProperties {

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

    public String getPath() {
        return this.path;
    }
}

2.2 BasicErrorController

SpringBoot 统一的异常处理Controller

用于请求返回的 Controller类,根据HTTP请求可接受的格式(Accept)不同返回对应的信息,所以在使用浏览器和接口测试工具测试时返回结果存在差异。

// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController

@Controller
// 定义请求路径,如果没有配置server.error.path,默认为 /error
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {

    // 返回 错误页面
    // 如果 客户端可以接收的响应格式为 text/html
    @RequestMapping(produces = {"text/html"})
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        // 获取 错误状态码
        HttpStatus status = this.getStatus(request);
        // 获取 要返回的信息
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        // 设置 响应状态码
        response.setStatus(status.value());
        // 去哪个错误界面
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        // 返回视图,如果 自定义的页面模板不存在,则使用默认的 错误视图模板
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    // 返回 json 数据
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        // 获取 错误状态码
        HttpStatus status = this.getStatus(request);
        // 如果 是 204 ,就是表示服务器已成功完成请求,并且在响应正文中没有要发送的内容
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity(status);
        } else {
            // 如果是接受所有格式的HTTP请求 */*
            Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
            // 响应 ResponseEntity extends HttpEntity
            return new ResponseEntity(body, status);
        }
    }
}

可以看到这两个方法中都有一段共同的代码HttpStatus status = getStatus(request);,它的作用就是为了给解析视图提供状态码,通过下面的源码request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)可以看出来,SpringBoot首先是通过request域对象中获取状态码,但是这里有一个参数RequestDispatcher.ERROR_STATUS_CODE,这个参数的具体值就是javax.servlet.error.status_code

所以我们在自定义异常处理器时,需要手动指定状态码,否则不生效

request.setAttribute(“javax.servlet.error.status_code”,5000);
protected HttpStatus getStatus(HttpServletRequest request) {
    Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    if (statusCode == null) {
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
    try {
        return HttpStatus.valueOf(statusCode);
    }
    catch (Exception ex) {
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

public interface RequestDispatcher {
	public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
}

2.3 DefaultErrorAttributes

一个专门收集 error 发生时错误信息的bean,把错误信息注入到错误页面中

DefaultErrorAttributes中的 getErrorAttributes 方法提供了默认的错误信息注入,可以获取到:

getErrorAttributes()方法获取timestamp:时间戳

addStatus()方法获取status:状态码

addErrorDetails()方法获取exception:异常对象

addStackTrace()方法获取trace:堆栈信息(能力尚浅,仅是猜测)

addExceptionErrorMessage()方法获取message:异常错误信息

addBindingResultErrorMessage()方法获取errors:JSR303校验信息

addPath()获取请求出错的路径path:错误路径

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

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

2.4 、DefaultErrorViewResolver

如果Accept:text/html请求,这时候就会进入到DefaultErrorViewResolverresolveErrorView方法

  • 分两种情况:

    • 第一种可以通过模板引擎解析的页面;
    • 第二种模板引擎无法解析的页面
  • 以上两种情况又各分为两种情况:

    • 可以精确匹配到的页面,比如404.html;
    • 另一种则是模糊匹配页面,比如4xx.html、5xx.html.

第一种:
可以通过模板引擎解析的页面的精确匹配,首先这个页面是需要放在 /template/error/ 路径下的,当请求进入后,模板引擎会根据状态码解析错误页面,精确匹配到路径下的页面,功能实现就是这句

this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext)

第二种:
模板引擎无法解析的页面和静态资源文件夹下也没有页面的情况,这时候
this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext)会返回一个null,从而进入到 resolveResource 方法,通过遍历静态资源文件位置获取匹配的页面,如果没有页面,则会返回系统默认的视图对象。

通过上面遍历寻找页面可以得出一个结论:精确优先,寻找位置的优先级(从高到底):

META-INFO/resources/error/

项目路径下/resources/error/

项目路径下/static/error/

项目路径下/public/error/

最后没有找到对应的页面,SpringBoot则会使用默认的页面展示,也就是文章开头那个图
// org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {

    // 调用resolve 方法,使用 HTTP完整状态码 检查是否有页面可以匹配
    ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);

    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
        // 使用 HTTP 状态码 第一位匹配初始化中的参数创建视图对象
        modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
    }

    return modelAndView;
}

// resolve 
private ModelAndView resolve(String viewName, Map<String, Object> model) {
    // 拼接错误视图路径 /eroor/[viewname]
    String errorViewName = "error/" + viewName;
    // 使用模版引擎尝试创建视图对象
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
    // 模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/[viewname]
    return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}

// resolveResource
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
    String[] var3 = this.resourceProperties.getStaticLocations();
    int var4 = var3.length;

    // 遍历静态资源文件夹,检查是否有存在视图
    for(int var5 = 0; var5 < var4; ++var5) {
        String location = var3[var5];

        try {
            Resource resource = this.applicationContext.getResource(location);
            resource = resource.createRelative(viewName + ".html");
            if (resource.exists()) {
                return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
            }
        } catch (Exception var8) {
        }
    }

    return null;
}

3 自定义错误页面

由上面的分析可知,当系统出现错误时,Spring Boot默认是去类路径下的/error请求。对应的请求为error/状态码,如果将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的/error文件夹下,发生此状态码的错误就会来到对应的页面。

当需要定义错误页面时,可以使用4xx5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html)。

如果模板引擎具体的 /error 下有就是用对应的页面;
如果没有,则去项目的静态资源文件夹下找;
如果前面的路径下都没有,使用Spring Boot默认的错误提示页面。

其中从错误页面可以获取到的默认信息有:

timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里

3.1 不转发到 /error ,自定义异常处理和返回定制的json数据

此处理器不在请求到 /error ,不论客户端是啥,直接都统一返回json数据

自定义的UserNotExistException异常

// UserNotExistException
public class UserNotExistException extends RuntimeException {

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

全局异常处理器

// 全局异常处理器
@ControllerAdvice
public class SelfExceptionHandler {
    /**
     * 专门捕获 自定义的 UserNotExistException 异常,进行处理
     *
     * 这里捕获了异常,不论客户端是啥,是直接都统一返回json数据
     *
     * @param e Exception
     * @return  Map
     */
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handleException(Exception e){
        Map<String,Object> map = new HashMap<>(4);
        map.put("message",e.getMessage());
        map.put("exception",e.getClass());
        map.put("code",ResponseCode.FAILURE.getCode);
        return map;
    }
}

3.2 转发到 /error 进行自适应响应效果处理

// 全局异常处理器
@ControllerAdvice
public class SelfExceptionHandler {
    /**
     *
     * 利用 SpringBoot 的 BasicErrorController,
     * 对 /error 请求请求进行自适应处理(text/html或者json)
     *
     * @param e Exception
     * @param request HttpServletRequest
     * @return 转发到 /error 请求
     */
    @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request){
        Map<String,Object> map = new HashMap<>(4);
        // 先通过 Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code") 从request获取状态码
        // HttpStatus status = this.getStatus(request) 获取状态码
        // 设置 响应的状态码
        request.setAttribute("javax.servlet.error.status_code", 5000);

        // 
        map.put("message",e.getMessage());
        map.put("exception",e.getClass());
        map.put("code",ResponseCode.FAILURE.getCode());

        // 需要响应的数据,添加到request 域,
        // 提供给 SelfErrorAttribute 类 的 getErrorAttributes 方法获取
        request.setAttribute("ext",map);
        return "forward:/error";
    }
}

将定制数据携带出去:出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes()得到。getErrorAttributes()AbstractErrorController规定的方法,AbstractErrorControllerErrorController接口的实现类。

为了使用定制的错误响应,要进行如下步骤:

  • 编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中

  • 页面上能用的数据,或者是json返回能用的数据都是通过
    errorAttributes.getErrorAttributes得到。容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;同样也可以自定义errorAttributes,通过getErrorAttributes()同样可以获取到。

给容器中加入我们自己定义的ErrorAttributes

// 给容器中加入我们自己定义的ErrorAttributes

/**
 * 自定义 ErrorAttribute
 * 实现 DefaultErrorAttributes,用于添加或者改变 响应的errorAttributes
 */
@Component
public class SelfErrorAttributes extends DefaultErrorAttributes {

    // 返回值的map就是页面和json能获取的所有字段
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
        // 添加响应参数
        errorAttributes.put("param1", "param1");
        // 从request区域中,获取 SelfExceptionHandler 类,传递过来的参数
        // 我们的异常处理器携带的数据
        // 0 就是用来确定request域对象的,这样我们就可以从request域对象中获取之前自定义的异常信息了
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        // 如果是 UserNotExistException 异常
        if (ext != null) {
            errorAttributes.put("ext", ext);
        }
        return errorAttributes;
    }

}

WebRequest implement RequestAttributes

public interface RequestAttributes {
    int SCOPE_REQUEST = 0;
    int SCOPE_SESSION = 1;
}

你可能感兴趣的:(springboot,spring,java)