我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况。接下来来看看SpringBoot的错误处理机制。(本文章以整合模板引擎 thymeleaf 为例)
[TOC]
SpringBoot默认的错误处理机制
1、默认效果(自适应)
自适应:指访问
浏览器
响应错误页面
,访问客户端
响应JSON 数据
(适用于传统项目);不支持自适应指不管访问的是浏览器
还是客户端
,响应的都是JSON 数据
(适用于前后端分离)。
- 浏览器访问,如果出现错误,SpringBoot会返回一个默认的 错误页面
其他客户端访问,如果出现错误,SpringBoot默认会响应一串 json数据
2、底层 – ErrorMvcAutoConfiguration
SpringBoot的错误处理机制是由 ErrorMvcAutoConfiguration 自动配置组件
实现的。
下面看看这个配置组件中有什么重要的组件
组件
ErrorPageCustomizer
用于错误页面定制
其中 @Value("${error.path:/error}")
用的语法是
1 | /** |
BasicErrorController
根据
请求头的 Accept
控制,调用指定
方法
1 |
|
DefaultErrorAttributes
接受处理方法中的request域,
定制
返回的格式
1 | (Ordered.HIGHEST_PRECEDENCE) |
DefaultErrorViewResolver
默认错误视图解析器:通过解析,根据
模板引擎
的类别、是否存在,返回指定的视图
1 | public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { |
流程
一旦系统出现了4xx或5xx之类的错误;ErrorPageCustomizer(定制错误页面规则)就会生效;然后定制器会发送 /error 请求,该请求会被 BasicErrorController 处理。 BasicErrorController 会根据请求头的 Accept 调用对应的方法 errorHtml 或 error(自适应),在这两个方法中,都调用了父类AbstractErrorController
的方法 getErrorAttributes() (父类通过构造方法注入组件ErrorAttributes)
,getErrorAttributes()中调用了 ErrorAttributes组件的 getErrorAttributes(),在该方法中对返回的数据进行封装,详细请看源码~~
分析
该控制器是如何做到对浏览器响应页面,对其他客户端响应 json 数据的呢?
访问浏览器
访问客户端
BasicErrorController 根据这两种方式定制了两个方法 errorHtml 和 error 来匹配请求
3、如何定制错误响应
虽然,Spring Boot
中实现了默认的 error 映射,但是在实际应用中,上面你的错误页面对用户来说并不够友好,我们通常需要去实现我们自己的异常提示。
错误页面
templates 文件夹
SpringBoot
默认先去 templates 文件夹 下找资源 ==error/状态码.html==,将匹配的页面返回 (所以我们在使用的时候,直接将错误页面命名为 状态码.html,放在 template/error/ 下即可。SpringBoot
就会去找与错误状态码对应的页面)
那么问题就来了?错误码这么多。难道我们需要定制n个错误页面吗?
当然不用。
Spring Boot
已经帮我们想好了这个问题,DefaultErrorViewResolver
组件中可以看到,对于同一系列的错误码,我们可以使用 前缀xx 的格式定制错误页面。如 ==4xx、5xx==。如果我们需要对指定的状态码做特殊的处理,这时候我们可以单独给这个状态码做一个页面,如 404.html 。SpringBoot 匹配的时候会优先查找 精确的状态.html,后模糊匹配。(先精确后模糊
)
数据模板(定制的时候,一些非自定义的异常。通过下面格式拿数据,实现统一格式。自定义的可以建个父异常,然后在自定义的 ErrorAttributes
中根据是否继承分类讨论)
1 | { |
static 文件夹
如果在 templates 下找不到错误页面,SpringBoot 接下来会去 static 下找;如果都这个两个文件夹都没有,那么 SpringBoot 会返回默认的错误页面
json数据
当客户端访问报错后,显示的 json 数据是固定的。那么我们如何定制自己的 json 数据呢。
自适应
(推荐)
定制自己想要的 json 格式的数据,保持自适应效果。
即使浏览器的不需要也无妨,我们只用它客户端的姿势就行。
需要 ExceptionHandler
+ ErrorAttributes
两个组件共同实现
在异常处理方法中,我们将想要的数据放到 map 中
要求:
- 返回类型为
String
- 返回值为
"forward:/error"
- request域 中放我们自己的状态码,不放默认为 200
- 数据放在
request域
中
1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 测试定制json数据(保持自适应效果)的异常处理方法
*/
(AdaptiveException.class)
public String adaptive(Exception e, HttpServletRequest request) {
// 传入自己的错误状态码,如 4xx,5xx 不传默认200
request.setAttribute(CUSTOM_STATUS_CODE, 500);
Map<String, Object> map = new HashMap<>();
map.put("code", "adaptive.code");
map.put("message", e.getMessage());
request.setAttribute("ext", map);
return "forward:/error";
}- 返回类型为
在getErrorAttributes() 中我们 定制返回的 json 数据的格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* 这个方法有点像代理
* @param webRequest :接受异常处理方法中的request(可参考源码)
* @param includeStackTrace
* @return
*/
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
/**
* 这个map就是最后返回的数据,如果你想改格式,改这个map即可
*/
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
// 后面的scope知道你对哪个域存取数据
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext", ext);
map.put("where", "进入自定义ErrorAttributes的getErrorAttributes中了~~");
return map;
}
不自适应
缺点:没有自适应效果
1 | /** |
环境/版本一览:
- 开发工具:Intellij IDEA 2018.2.2
- springboot: 2.0.6.RELEASE
- jdk:1.8.0_171
- maven:3.3.9
- spring-boot-starter-thymeleaf:2.0.6.RELEASE
- lombok:1.16.20
pom.xml
1 | <dependencies> |
application.yml
1 | spring: |
html
指定三个错误页面
4xx.html
1 |
|
5xx.html
1 |
|
404.html
1 |
|
目录如下:(一定要放在templates
下的error
中)
FatalErrorAttributes
自定义一个 ErrorAttributes
组件,用于定制返回的(统一的) JSON 格式
为什么说统一呢?因为如果你直接在
ExceptionHandler
上面,加了@RequestBody
,那么没被你拦截到的异常显示的格式是什么样子,还是 timestamp 那一串默认的 json。也许你会说,我加个ExceptionHandler
专门处理剩下来的所有异常,code
都返回 500。这可不行哦,这种用法很危险,你还有很多 4xx 类型的错误呢。比方说,前端搞错请求方式,把GET
搞成了POST
,一顿操作,真的报 500 ,他截图发了句,你这个接口有 bug,你看了下日志,发现是对方请求方式不对,但是你怎么跟他说,百口难辩呀。一定程度下还降低了双方对接的效率…但是,如果你自定义这个组件,就不同了,根据
是否自定义
来封装返回的属性;就能达到自定义的ExceptionHandler可以返回定制的错误提示
,未自定义的ExceptionHandler能返回默认的错误提示
。
1 | package com.fatal.attribute; |
Custom Exception
TraditionalException
1 | package com.fatal.exception; |
AdaptiveException
1 | package com.fatal.exception; |
GlobalExceptionHandler
写一个全局异常处理器
,里边可以自定义处理各种异常的处理器(ExceptionHandler
)。类上标记注解@ControllerAdvice
声明该组件是全局异常处理器
。
1 | package com.fatal.handler; |
FatalController
1 | package com.fatal.controller; |
Test
1 | package com.fatal; |
测试
启动项目
400
404
不自适应
浏览器
客户端
自适应
自适应
客户端
构造函数注入
启动 Chapter23ApplicationTests.contextLoads()
,显示如下
笔记
全局异常处理器
在全局异常处理器中,各个处理方法都遵循两个原则
- 子类的处理方法优先于父类获得处理权
- 若子类不存在,就近原则,离子类最近的父类的处理方法获得处理权
构造方法注入
可以不用在构造方法上加注解,只需要将组件作为构造方法参数即可
。
总结
定制错误页面
在 templates/error 下定制 状态码.html(可以 指定 某个状态码,也可以使用 前缀xx 的格式进行模糊匹配)。SpringBoot 会优先匹配精确的,再匹配模糊的。
定制json数据(保持自适应)
步骤:
- 创建全局异常处理器(对处理方法的要求:返回类型必须是
String
;返回值为"forward:/error"
;request域 中放我们自己的状态码,不放默认为 200;数据放在request域
中) - 创建自定义 ErrorAttrubute
SpringBoot
的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn、方志朋的专栏、程序猿DD、纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~
以上文章是我用来学习的Demo
,都是基于 SpringBoot2.x
版本。
源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter23