使用 Sping Boot 开发过程中, 我们经常会遇到 404,500 等错误,那么 Spring Boot 对于出现的错误,又是怎么个处理流程呢?
1.前情回顾
①当我们使用浏览器发送一个不存在的localhost:8081/users
请求,出现 404 错误时,Spring Boot 解析按成之后会为我们返回一个默认的 Html 页面提示
②我们不止使用浏览器访问接口,当我们使用客户端来发送相同请求时,此时客户端为我们返回的却是一个 Json 数据。这是为什么呢? (接下来我们就带着这个问题来分析 Spring Boot 错误处理机制)
2.Spring Boot 错误处理源码分析时序图
如需原图原图,请点击本链接下载(提取码:dikj)
3.错误处理源码分析
1.先来找错误原理入口
先来找入口。我们知道错误信息其实是由Spring MVC 来解析的,在 Spring Boot 自动配置spring-boot-autoconfigure
源码包下,我们直接从 mvc 出入手,发现有以下几个类就是用来处理我们的错误请求的。显然ErrorMvcAutoConfiguration
类就是错误处理的自动配置类。
2.ErrorMvcAutoConfiguration
类分析
Ⅰ.我们先来分析错误处理自动配置类为我们自动配置了哪些重要的组件?(共4个)
①DefaultErrorAttributes 组件 容器中没有 ErrorAttributes 组件时才会自动配置
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
②BasicErrorController 组件 容器中没有 ErrorController组件时才会自动配置
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
③ErrorPageCustomizer 组件
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties);
}
④DefaultErrorViewResolver 组件
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,this.resourceProperties);
}
Ⅱ.当系统出现 4xx 或者 5xx之类的错误;ErrorPageCustomizer组件就会生效(来定制错误的响应规则),此时就会来到 /error 请求;该请求会被 BasicErrorController
处理;
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//系统出现错误后,会来到error请求进行处理
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()+ this.properties.getError().getPath());
errorPageRegistry.addErrorPages(errorPage);
}
@Value("${error.path:/error}")//${error.path:/error} 会去配置文件获取配置的路径,如果没有配置就走"/error"请求
private String path = "/error";//getError().getPath()最终获取到的是"error"请求
Ⅲ.发生错误后,BasicErrorController 就起作用了,用来处理"/error"错误请求
以下代码就是页面访问和客户端访问返回不同结果的原因。
一个产生 html 数据、一个产生 json 数据。Spring Boot 是通过发送请求时请求头中的"accept"参数
来区别浏览器和客户端的
@Controller//该组件就是一个普通 Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//1.产生 html 类型的数据,浏览器发送的请求就会来到这个方法处理
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
//(此处预留,后面讲)此处调用 getErrorAttributes() 方法,用来获取错误信息
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//解析错误页面(获取错误页面地址和页面内容)
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
//2.产生json数据,其他客户端来到这个方法处理;
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
}
此处分开来分析:
①先来分析:响应页面 HTML 请求
1. 来分析解析错误页面 resolveErrorView()方法
代码
protected ModelAndView resolveErrorView(HttpServletRequest request,HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
//for循环解析所有的ErrorViewResolver,得到 ModelAndView 对象
for (ErrorViewResolver resolver : this.errorViewResolvers) {
//调用 resolveErrorView 方法(此处调用的是 DefaultErrorViewResolver类中的resolveErrorView方法)
//先看完第 2 步你就知道了为什么。
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
2. 来分析ErrorViewResolver 类
,我们发现该类是一个接口。它有一个唯一实现类DefaultErrorViewResolver
类,该类就是我们开篇分析自动注入的四个重要组件之一。
3. 我们来分析DefaultErrorViewResolver
类 resolveErrorView() 方法。你也可以直接去看简单概括↓↓↓
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
//使用静态块,定义异常 4xx、5xx,放入到 SERIES_VIEWS 中
static {
Map<Series, String> views = new HashMap<Series, String>();
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
//(根据传入的HttpStatus 状态 和 参数)开始解析
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) {
//默认 SpringBoot可以去找到一个页面? error/404
String errorViewName = "error/" + viewName;
//1.如果模板引擎可以解析这个页面地址,就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//2.模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
return resolveResource(errorViewName, model);
}
}
简单概括就是:
① 如果模板引擎可用,就使用模板引擎解析 /error/404
、/error/500
等;(重要:如果使用的 Thymeleaf 模板引擎,直接在 /template 文件加下创建一个 /error/404.html 即可被解析,模板引擎会自动为你拼接前缀+后缀)
② 如果模板引擎不可用,就去静态资源文件夹下找对应的 /error/404.html
页面。(静态资源文件共有5个,可参考:Spring Boot 对 js、css 等静态资源的映射规则,将 /error/404.html 页面放在这 5 个文件夹下即可)
③以上两种情况都没有 error 页面,此时就会执行 Spring Boot 默认的错误页面,new ModelAndView(“error”, model)
返回 error 视图,这个 error 视图就是 Spring Boot 为我们配置的默认页面。如下图所示:
重点:静态块中定义了 4xx 和5xx 两个常量值,所以我们也可以使用 4xx 和 5xx 作为错误页面的文件名来匹配这种类型的所有错误,精确优先
(优先寻找精确的状态码.html,404错误,如果有404.html 就优先匹配,没有才匹配4xx.html);
4. 此处来分析getErrorAttributes()
方法 (此处会涉及到开篇提及的最后一个还没用到的组件:DefaultErrorAttributes)
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
//errorAttributes是一个接口
//此处调用的是该接口的唯一实现类 DefaultErrorAttributes类中的 getErrorAttributes()
return this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace);
}
//具体实现
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
DefaultErrorAttributes 组件
主要帮我们在页面之间共享信息;在DefaultErrorAttributes
类中,我们一共能够获取到如下所有信息:
- timestamp:时间戳
- status:状态码
- error:错误提示
- exception:异常对象
- message:异常消息
- errors:JSR303数据校验的错误都在这里
这些参数,我们就可以直接拿来使用,如下:
②再来分析:响应定制的 Json 请求
- 同样通过getErrorAttributes() 方法,返回异常相关信息;
- 调用 getStatus() 方法,通过
request.getAttribute(“javax.servlet.error.status_code”);
的方式获取状态值; - 将错误信息封装成一个 ResponseEntity 类,并以 Json 格式数据返回。
博主写作不易,来个关注呗
求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙
博主不能保证写的所有知识点都正确,但是能保证纯手敲,错误也请指出,望轻喷 Thanks♪(・ω・)ノ