默认情况下的异常现象
创建一个接口 (接口需要传递参数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 中定义了两个接口方法:errorHtml、error ,其中 errorHtml 方法只对 request 的 accept 能兼容 text/html 的请求有效,error 方法则可以认为是一个兜底方法,它对 request 的 accept 没有要求
当我们的请求抛出异常,会转发到这个接口(/error),如果这个默认的 URI(/error) 和我们的项目有冲突,我们可以在配置文件中定义 server.error.path | error.path 来修改默认值。
StandardHostValve#custom (请求转发)
DispatcherServlet#processDispatchResult
我们需要关注两个方法 processHandlerException、render。
case1:访问一个不存在的接口或者不存在的文件
这种情况 exception 和 mv(ModelAndView)都为 null,所以既不会执行 processHandlerException 方法,也不会执行 render 方法,然后转发到 /error 接口。
在 Postman 中发请求,Accept 默认为 */*,在浏览器中发请求 Accept 如下所示:
根据一定的规则,在 Postman 中默认转发到 error 方法,在浏览器中默认转发到 errorHtml 方法
AbstractHandlerMethodMapping#lookupHandlerMethod (请求映射规则)
case1.1 转发到 error 方法
该方法会构建一个 map 对象,设置 timestamp、status、error、path 等信息并响应
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:
- classpath:/templates/error/404.html => view:error/404
- classpath:/META-INF/resources/404.html => view:HtmlResourceView
- classpath:/resources/404.html => view:HtmlResourceView
- classpath:/static/404.html => view:HtmlResourceView
- classpath:/public/404.html => view:HtmlResourceView
- classpath:/templates/error/4xx.html => view:error/4xx
- classpath:/META-INF/resources/4xx.html => view:HtmlResourceView
- classpath:/resources/4xx.html => view:HtmlResourceView
- classpath:/static/4xx.html => view:HtmlResourceView
- classpath:/public/4xx.html => view:HtmlResourceView
- 其他 => 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注解生效:
- 什么都没有配置
- Controller所在的类路径以配置的 basePackages 开头
- Controller类型是指定的类或是其子类
- 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 做兜底