SpringBoot之全局异常处理

默认情况下的异常现象

创建一个接口 (接口需要传递参数key)
@RestController
@RequestMapping("/exception")
public class ExceptionController {

    @GetMapping("/accept")
    public String acceptKey(@RequestParam("key") String key) {
        return key;
    }
}
访问链接(不传递参数key,使得抛出异常) http://localhost:8080/exception/accept
在浏览器中的现象

在 Postman 中的现象

小结

在浏览器中返回一个 html 页面,在 Postman 中返回一个 json 数据

解决方案

在默认静态资源路径下创建 error 子文件夹,并创建文件 400.html

默认静态资源路径如下:

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/

400.html 明细如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
   <h1>404</h1>
</body>
</html>

再次在浏览器下访问链接 : http://localhost:8080/exception/accept

PS:错误码需要和 html 名字一致,或者将 html 改成 4xx、5xx,这样以 4 开头的错误码就会跳转到 4xx.html,以 5 开头的错误码就会跳转到 5xx.html

再次在 Postman 中访问链接 : http://localhost:8080/exception/accept

好像并没有起作用,我们再尝试其他方法

创建 GlobalExceptionHandler,处理全局异常
创建实体类 ExceptionInfo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExceptionInfo {

    private String msg;
}
创建全局异常处理配置类 GlobalExceptionHandler
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ExceptionInfo resolverException(Exception exception) {
        ExceptionInfo exceptionInfo = new ExceptionInfo();
        exceptionInfo.setMsg(exception.getMessage());
        return exceptionInfo;
    }
}
访问链接 : http://localhost:8080/exception/accept

源码解析

ErrorMvcAutoConfiguration

ErrorMvcAutoConfiguration 会定义一个类型为 BasicErrorController 的  Bean。BasicErrorController 中定义了两个接口方法:errorHtmlerror ,其中 errorHtml 方法只对 request 的 accept 能兼容 text/html 的请求有效,error 方法则可以认为是一个兜底方法,它对 request 的 accept 没有要求

当我们的请求抛出异常,会转发到这个接口(/error),如果这个默认的 URI(/error) 和我们的项目有冲突,我们可以在配置文件中定义 server.error.path | error.path 来修改默认值。

StandardHostValve#custom (请求转发)

DispatcherServlet#processDispatchResult

我们需要关注两个方法 processHandlerExceptionrender

case1:访问一个不存在的接口或者不存在的文件

这种情况 exception 和 mv(ModelAndView)都为 null,所以既不会执行 processHandlerException 方法,也不会执行 render 方法,然后转发到 /error 接口。

在 Postman 中发请求,Accept 默认为 */*,在浏览器中发请求 Accept  如下所示:

根据一定的规则,在 Postman 中默认转发到 error 方法,在浏览器中默认转发到 errorHtml 方法

AbstractHandlerMethodMapping#lookupHandlerMethod (请求映射规则)

case1.1 转发到 error 方法

该方法会构建一个 map 对象,设置 timestampstatuserrorpath 等信息并响应

case1.2 转发到 errorHtml 方法

默认情况下 errorViewResolvers 只有一个,类型为 DefaultErrorViewResolver,它是在 ErrorMvcAutoConfiguration 内部类 DefaultErrorViewResolverConfiguration 中定义的,相关源码如下:

DefaultErrorViewResolver#resolveErrorView

当我们访问一个不存在的链接,viewName 为  404,errorViewName 为 error/404,如果 TemplateAvailabilityProviders 的 getProvider 方法返回一个非 null 对象,则返回一个 ModelAndView 对象

TemplateAvailabilityProviders#getProvider

如果 TemplateAvailabilityProvider 的 isTemplateAvailable 方法返回 true,则返回当前 TemplateAvailabilityProvider 对象,即最终会返回一个 ModelAndView 对象

根据 SpringBoot 的自动配置,容器中存在五个 TemplateAvailabilityProvider ,我们来看一下 ThymeleafTemplateAvailabilityProvider 的 isTemplateAvailable 方法。

即默认情况下,如果我们的环境中,存在指定的类(org.thymeleaf.spring5.SpringTemplateEngine),并且资源 classpath:/templates/error/404.html 存在,则返回一个 ModelAndView 对象

可以导入下方所示的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.6.13</version>
</dependency>

DefaultErrorViewResolver#resolveResource

如果 TemplateAvailabilityProviders 的 getProvider 方法返回 null,则继续调用 resolveResource 方法。该方法会遍历 staticLocations,判断这些默认静态文件路径下是否存在相关文件(是否存在error/404.html),如果存在则构建一个 HtmlResourceView 对象,staticLocations 明细列表如下:

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/

DefaultErrorViewResolver#resolveErrorView (2)

如果错误状态码是以 4 或 5 开头,则以 viewName 为  4xx,errorViewName 为 error/4xx,再执行一遍上述的流程

BasicErrorController#errorHtml(2)

即不管我们有没有在当前环境中找到指定文件,都会返回一个 ModelAndView 对象,如果存在以下资源,viewName 则不为 error:

  1. classpath:/templates/error/404.html  => view:error/404
  2. classpath:/META-INF/resources/404.html => view:HtmlResourceView
  3. classpath:/resources/404.html  => view:HtmlResourceView
  4. classpath:/static/404.html  => view:HtmlResourceView
  5. classpath:/public/404.html  => view:HtmlResourceView
  6. classpath:/templates/error/4xx.html => view:error/4xx
  7. classpath:/META-INF/resources/4xx.html => view:HtmlResourceView
  8. classpath:/resources/4xx.html => view:HtmlResourceView
  9. classpath:/static/4xx.html => view:HtmlResourceView
  10. classpath:/public/4xx.html => view:HtmlResourceView
  11. 其他 => view:error

DispatcherServlet#processDispatchResult(2)

第二次进入 DispatcherServlet 的 processDispatchResult 方法,此时 mv 不等于null,则进入render 方法

如果 viewName 为 error/404、error/4xx、error 则执行 resolveViewName 方法

一共有5个resolver,我们只需要关注 ContentNegotiatingViewResolver 的 resolveViewName 方法

ContentNegotiatingViewResolver 的内部属性 viewResolvers 有其他四个 resolvers,即 ContentNegotiatingViewResolver 相当于一个大管家,具体还是由其他四个 resolvers 处理,我们简要分析一下 beanNameViewResolver

ErrorMvcAutoConfiguration 的内部类 WhitelabelErrorViewConfiguration 会定义两个bean,其中一个 beanName 为 error,类型为 View,另一个 beanName 为 beanNameViewResolver,类型为 BeanNameViewResolver

即 BeanNameViewResolver 的 resolveViewName 方法会返回一个类型为 StaticView 的 View

View#render

StaticView#render

我们可以看到 StaticView 的 render 方法就是我们经常看到的页面

StaticView#render

HtmlResourceView#render

HtmlResourceView 的 render 方法就是将指定资源用流写出去

case2:访问一个存在的接口且抛出异常
DispatcherServlet#processHandlerException

一共有两个 HandlerExceptionResolver,其中一个类型为 DefaultErrorAttributes,DefaultErrorAttributes 的 resolveException 方法比较简单,主要是给 request 赋值,我们主要关注 HandlerExceptionResolverComposite 的 resolveException 方法

HandlerExceptionResolverComposite#resolveException

一共有三个 HandlerExceptionResolver,我们主要分析一下 ExceptionHandlerExceptionResolver的 resolveException 方法

ExceptionHandlerExceptionResolver#resolveException

最终会执行类上标记 @ControllerAdvice 注解,方法上标记 @ExceptionHandler 注解的符合条件的方法,就是我们在【解决方案】的 GlobalExceptionHandler#resolverException 方法

如何选择 ServletInvocableHandlerMethod

遍历 exceptionHandlerAdviceCache 对象,通过 ExceptionHandlerMethodResolver 的resolveMethod 方法,获取一个 method 对象,并将其封装成 ServletInvocableHandlerMethod 对象

isApplicableToBeanType

以下四种情况,@ControllerAdvice注解生效:

  1. 什么都没有配置
  2. Controller所在的类路径以配置的 basePackages 开头
  3. Controller类型是指定的类或是其子类
  4. Controller上含有指定注解

resolveMethod

遍历 mappedMethods 对象,如果存在多个 @ExceptionHandler 标注的方法,则选择一个优先级最高的

ExceptionHandlerExceptionResolver 的 exceptionHandlerAdviceCache 是如何赋值的

ExceptionHandlerExceptionResolver 继承 InitializingBean 接口,所以 bean 在实例化的过程中会执行其 afterPropertiesSet 方法

ExceptionHandlerExceptionResolver#afterPropertiesSet

如果 bean 上存在 @ControllerAdvice 注解,则构建一个 ControllerAdviceBean 对象

循环遍历查找出来的 adviceBeans,每存在一个 ControllerAdviceBean,则构建一个ExceptionHandlerMethodResolver 并将其 put 到 exceptionHandlerAdviceCache

ExceptionHandlerMethodResolver的实例化(给 mappedMethods 赋值)

如果方法存在 @ExceptionHandler 注解,则给 mappedMethods 赋值

PS : 如果 @ExceptionHandler 注解标注的方法也抛出异常,则使用 case1 做兜底

上一篇:git-各种场景-撤销指令


下一篇:分布式搜索引擎ES-elasticsearch入门