1. SpringBoot默认的错误处理机制
1.1 默认效果
默认效果:
1). 返回一个默认的错误页面
2). 其他客户端访问,默认响应JSON数据
浏览器响应数据
其他客户端响应JSON数据
但是实际情况,不只是电脑会发送这个请求。可能有手机,平板等。
模拟一下其他客户端工具。 Postman
发现其他客户端访问时,响应的是json数据:
{
"timestamp": "2020-06-30T12:17:14.545+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/crud/aaa"
}
1.2 原理
参照:
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.ErrorProperties
错误处理的自动配置
1). 如何定制错误响应:
1. 如何定制错误页面
2. 如何定制错误的JSON(针对其他的客户端)
2). ErrorMvcAutoConfiguration添加的组件
ErrorMvcAutoConfiguration给容器中添加了这几个重要的组件:
1. DefaultErrorAttributes
帮我们在页面共享信息,它给页面获取这几个属性:
timestamp:时间戳
status:状态码
erro:错误提示
exception:异常对象
message:异常消息
errors:(BingdingResult)JSR数据校验有关
2. ErrorPageCustomizer : 定制的错误页面
3. BasicErrorController
4. DefaultErrorViewResolver
3). 步骤
1. ErrorPageCustomizer : 定制的错误页面
一旦系统出现4XX或者5XX之类的错误,ErrorPageCustomizer就会生效(定制错误的响应规则)。
追踪源码:
ErrorPageCustomizer构造方法中会注册一个错误页面
注册的错误页面的路径是:"/error";
@Value("${error.path:/error}")
private String path = "/error";
所以出现错误页面后ErrorPageCustomizer就会生效,定制错误的响应规则,就会发送/error请求。
2. BasicErrorController:基本的错误处理器。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
${server.error.path:${error.path:/error}}:映射${server.error.path}的值,如果没有
就映射${error.path:/error}。
映射到后,BasicErrorController就来处理。
BasicErrorController处理默认的/error请求。
3. BasicErrorController的处理方式
//响应HTML数据的: 浏览器客户端发送的请求来这里处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//去哪个页面作为错误页面:ModelAndView中包含页面地址和页面内容
ModelAndView modelAndView = 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 = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
所以浏览器响应的是HTML页面,其他端响应的是JSON数据,
4. 怎么知道是浏览器还是其他客户端呢?
浏览器发送请求的请求头中有识别标志:Accept: text/html
其他客户端请求的请求头中有识别标志:Accept: "*/*"
5. 响应页面,怎么来到这个个页面?
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
去哪个页面是由DefaultErrorViewResolver解析得到的。
6. DefaultErrorViewResolver
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), 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+错误状态码
String errorViewName = "error/" + viewName;
//如果模版引擎可以解析这个页面地址:就用模版引擎
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模版引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模版引擎不可用,就在静态资源文件夹下找errorViewName对应的页面。
//error+404.html
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//模版引擎不可用,就在静态资源文件夹下找errorViewName对应的页面。
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
7. DefaultErrorAttributes
帮我们在页面设置信息,它给页面获取这几个属性:
timestamp:时间戳
status:状态码
erro:错误提示
exception:异常对象
message:异常消息
errors:(BingdingResult)JSR数据校验有关
1.3 如何定制错误页面
1.3.1 没有模版引擎的情况下
1). 有模版引擎的情况下:error/状态码
Thymeleaf默认取到classpath:/templates/error路径下解析。
小结:有模版引擎的情况下,将错误页面命名为错误状态码.html,然后放在
classpath:/templates/error文件夹下。发生此状态码的错误就会来到该页面。
但是错误类型繁多,所以用量一种写法。
2). 模糊匹配 4XX.html和5XX.html
精确优先。
没有精确的匹配的话,就匹配4XX.html
3). 定制错误页面
根据DefaultErrorAttributes获取的信息,我们在自己的错误页面上定制
timestamp:时间戳
status:状态码
erro:错误提示
exception:异常对象
message:异常消息
errors:(BingdingResult)JSR数据校验有关
1. 在定制4XX.html页面
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${timestamp}]]</h2>
<h2>error:[[${error}]]</h2>
2. 效果
1.3.2 不在模版引擎的目录下--或者说模版引擎找不到这个错误页面
或者说模版引擎找不到这个错误页面,会去到静态资源文件夹下找错误页面。
能找到,但是由于不在模版引擎解析的目录下,就无法解析想要定制的信息,
1.3.3 静态资源目录和模版引擎都没有时
就是默认来到SpringBoot默认的空白页面。
1.4 如何定制错误的JSON数据
1.4.1 自定义一个异常
SpringBoot做了一个自适应的效果,浏览器访问会响应错误页面,其他客户端会响应错误JSON数据。
1. 自定义一个异常类:UserNotExistException
public class UserNotExistException extends RuntimeException{
public UserNotExistException(){
super("用户不存在");
}
}
2. controller
@ResponseBody
@RequestMapping("/hello")
public String hello(@RequestParam("user") String user){
if(user.equals("aaa")){
//抛一个自定义异常
throw new UserNotExistException();
}
return "Hello World";
}
如果请求的参数中有user=aaa,那么就会抛出自定义异常。
3. 自定义5XX.html错误页面
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${timestamp}]]</h2>
<h2>error:[[${error}]]</h2>
<h2>exception:[[${exception}]]</h2>
<h2>message:[[${message}]]</h2>
4. 效果展示:http://localhost:8080/crud/hello?user=aaa
5. 开启配置
# 开启获取自定义异常类exception。
server.error.include-exception=true
# 开启获取自定义异常信息message
server.error.include-message=always
6. 效果展示
7. 其他端效果展示
{
"timestamp": "2020-06-30T14:03:05.992+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "com.atguigu.springboot.exception.UserNotExistException",
"message": "用户不存在",
"path": "/crud/hello"
}
1.4.2 自定义一个异常处理器
1. 写一个自己的异常处理器:MyExceptionHandler
@ControllerAdvice
public class MyExceptionHandler {
/**
* 指明要处理的异常:UserNotExistException.class
* 所有异常:Exception.class
* @return
*/
@ResponseBody //将map以json数据形式返回
@ExceptionHandler(UserNotExistException.class)
public Map<String, Object> handlerException(Exception e){
//捕获该异常后,将异常对象传进来Exception e
Map<String, Object> map = new HashMap<>();
map.put("code", "user.notexist");
map.put("message", e.getMessage());
return map;
}
}
自定义处理器的几个注意:
1. Spring中异常处理器要标注@ControllerAdvice
2. @ExceptionHandler(UserNotExistException.class)标注在handlerException方法上
SpirngMVC的注解
捕获要处理的异常:UserNotExistException.class
所有异常:Exception.class
3. public Map<String, Object> handlerException(Exception e){}
方法参数是:捕获的异常对象
4. @ResponseBody 将定义的错误的信息存在map并且以json数据形式返回
2. 效果展示:没有自适应效果。
1. 其他端
2. 浏览器客户端
发现浏览器也响应的是JSON数据。
3. 原因就是我们在自定义异常处理器中,定义的处理规则返回JSON形式的map。
浏览器和其他客户端被响应的都是数据
1.4.3 自适应的自定义异常处理器
因为SpringBoot默认是由BasicErrorController这个基本的错误处理器来处理/error请求,
而且BasicErrorController处理/error请求是自适应的。
1. 所以我们的自定义异常在捕获的异常时,定义好JSON数据后,请求转发到/error请求。
那么BasicErrorController就会自适应的处理/error请求。
@ExceptionHandler(UserNotExistException.class)
public String handlerException(Exception e){
//捕获该异常后,将异常对象传进来Exception e
Map<String, Object> map = new HashMap<>();
map.put("code", "user.notexist");
map.put("message", e.getMessage());
//因为BasicErrorController处理/error请求是自适应的。
return "forward:/error";
}
2. 出现问题:
没有来到自定义的错误页面,也没有显示自定义的错误信息。
发现页面的状态码是:200。
3. 传入我们自己的错误状态码:不传的话默认是200
看一下SpringBoot中咋么获取状态码:
1.AbstractErrorController : BasicErrorController的父类
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
2.RequestDispatcher
String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
4. 自定义异常处理器代码改进
@ExceptionHandler(UserNotExistException.class)
public String handlerException(Exception e, HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
//传入我们自己的错误状态码
/**
* Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
* String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
*/
request.setAttribute("javax.servlet.error.status_code", 400);
map.put("code", "user.notexist");
map.put("message", e.getMessage());
//因为BasicErrorController处理/error请求是自适应的。
return "forward:/error";
}
5. 效果展示
1.4.4 将我们定制的数据携带出去。
1. 注意上面的做的结果还有一个问题:
自适应效果有了,但是显示的message是自定义类中的message,而不是我们在异常处理器中自定义的。
2. 怎么将我们在自定义异常处理器中存在map中的数据携带出去呢?
出现异常以后,会来到/error请求,会被BasicErrorController处理。
响应出去的数据都是由:
getErrorAttributes()方法得到的。
BasicErrorController处理结果:
方法1:如果是页面返回ModelAndView
方法2:若是其他端也是以JSON形式返回一个Map<String,Object> body
响应出去的数据都是由:
getErrorAttributes()方法得到的。
AbstractErrorController的方法, 它是BasicErrorController的父类。
ErrorController是AbstractErrorController的接口
3. ErrorMvcAutoConfiguration添加的BasicErrorController组件时的条件
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
没有ErrorController组件时,会添加一个BasicErrorController组件。
继承关系:
BasicErrorController extends AbstractErrorController implements ErrorController
所以BasicErrorController实现了ErrorController
4. 解决方法1.
我们来另外编写一个ErrorController的实现类(或者AbstractErrorController的子类),
放到容器中。那么SpringBoot中的异常相关的自动配置类ErrorMvcAutoConfiguration
就不会再添加BasicErrorController组件了。
用我们自己的异常处理器,来完成我们要求的异常处理规则。
我们来写一个AbstractErrorController的子类,重写返回ModelAndView的方法和返回JSON的方法。
但是太复杂,用另一种方法
1.4.5 解决方法2:原理分析
1. 给页面携带的数据:ModelAndView
响应给JSON的数据:Map<String,Object>
2. 而且这俩个响应的数据都是通过getErrorAttributes()方法得到的。
getErrorAttributes()方法里面是:
return this.errorAttributes.getErrorAttributes
响应到页面上的数据和响应的JSON,都是通过这个方法得到的
3. errorAttributes是什么:
private final ErrorAttributes errorAttributes;
4. ErrorMvcAutoConfiguration在进行配置时:如果没有ErrorAttributes会自动配置一个
DefaultErrorAttributes
相当于DefaultErrorAttributes默认进行数据处理的。
5. DefaultErrorAttributes中有哪些数据:
其中有一个map中加入了很多属性。
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
6. 所以我们可以自己设置这些信息,然后加到容器中。
7. 小结:
return this.errorAttributes.getErrorAttributes。响应到页面上的数据和响应的JSON,
都是通过这个方法得到的。
errorAttributes对象是DefaultErrorAttributes类
DefaultErrorAttributes中实现ErrorAttributes接口方法getErrorAttributes()
所以我们自定义一个类继承DefaultErrorAttributes然后重写其中的getErrorAttributes()方法,
在方法里面重新定义属性信息等。
也就是将我们这个重写的继承DefaultErrorAttributes注入到IOC中,那么当BasicErrorController
来处理/error时,就从this.errorAttributes.getErrorAttributes()得到返回的数据。
注意:重点来了。
this.errorAttributes.getErrorAttributes():
errorAttributes是DefaultErrorAttributes类的实例化对象
而此时你自定义了一个类继承了DefaultErrorAttributes重写了其中的getErrorAttributes()方法
所以BasicErrorController再处理/error时,也会调用我们自定义的类中getErrorAttributes()
方法,获取我们自定以的值。
1.4.6. 解决方法
1. 代码
//给容器中加入我们自己定义的错误属性。
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = getErrorAttributes(webRequest, options);
map.put("company", "陶大哥");
return map;
}
}
2. 效果展示
响应的JSON数据:
3. BasicErrorController 源码
自适应响应数据的两个方法:都从getErrorAttributes()方法中取数据。
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) //响应HTML
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping //响应JSON
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
1.5 将我们自定义异常处理类中的信息页携带过去。
1. 将自定义异常处理类中的信息存入request中
@ExceptionHandler(UserNotExistException.class)
public String handlerException(Exception e, HttpServletRequest request){
//捕获该异常后,将异常对象传进来Exception e
Map<String, Object> map = new HashMap<>();
//传入我们自己的错误状态码
/**
* Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
* String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
*/
request.setAttribute("javax.servlet.error.status_code", 500);
map.put("code", "user.notexist");
map.put("message", "用户出错了");
request.setAttribute("ext", map);
//因为BasicErrorController处理/error请求是自适应的。
return "forward:/error";
}
2. 在重写的错误属性类中,将自定义异常处理类中的信息存入map
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("company", "atguigu");
/**
* 0:request
* 1:session
* 2:global_session
*
* webRequest是一个封装了的Request对象
* 得到异常处理器携带的数据
*/
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
errorAttributes.put("ext", ext);
return errorAttributes;
}
}
注意:
webRequest是一个封装了的Request对象可以获得request中的参数
webRequest.getAttribute("ext", 0) 第二个参数代表域:
0:request
1:session
2:global_session
3. 效果演示:
3.1 响应JSON
3.2 响应HTML
几个关键点:
1. 出现异常以后,会来到/error请求,会被BasicErrorController自适应的处理。
2. BasicErrorController处理结果:
方法1:如果是页面返回ModelAndView
方法2:若是其他端也是以JSON形式返回一个Map<String,Object> body
3. 响应出去的数据都是由:
getErrorAttributes()方法得到的。
getErrorAttributes()方法里面是:
return this.errorAttributes.getErrorAttributes
响应到页面上的数据和响应的JSON,都是通过这个方法得到的
6. ErrorMvcAutoConfiguration在进行配置时:如果没有ErrorAttributes会自动配置一个
DefaultErrorAttributes
相当于DefaultErrorAttributes默认进行数据处理的。
DefaultErrorAttributes中的方法getErrorAttributes()
4. 继承关系:
BasicErrorController extends AbstractErrorController implements ErrorController
5. 没有ErrorController组件时,会添加一个BasicErrorController组件。
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
6. 小结:
return this.errorAttributes.getErrorAttributes。响应到页面上的数据和响应的JSON,
都是通过这个方法得到的。
errorAttributes对象是DefaultErrorAttributes类
DefaultErrorAttributes中实现ErrorAttributes接口方法getErrorAttributes()
所以我们自定义一个类继承DefaultErrorAttributes然后重写其中的getErrorAttributes()方法,
在方法里面重新定义属性信息等。
也就是将我们这个重写的继承DefaultErrorAttributes注入到IOC中,那么当BasicErrorController
来处理/error时,就从this.errorAttributes.getErrorAttributes()得到返回的数据。
注意:重点来了。
this.errorAttributes.getErrorAttributes():
errorAttributes是DefaultErrorAttributes类的实例化对象
而此时你自定义了一个类继承了DefaultErrorAttributes重写了其中的getErrorAttributes()方法
所以BasicErrorController再处理/error时,也会调用我们自定义的类中getErrorAttributes()
方法,获取我们自定以的值。
最终的效果:响应是自适应的,可以通过定制getErrorAttributes改变需要返回的内容。
### 2. 完整代码:
1. 自定义异常类 UserNotExistException
public class UserNotExistException extends RuntimeException{
public UserNotExistException(){
super("用户不存在");
}
}
2. 自定义异常处理类 MyExceptionHandler
是为了自定义错误的JSON数据,但是还是没有将在自定义异常处理类定义的异常信息响应过去。
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public String handlerException(Exception e, HttpServletRequest request){
//捕获该异常后,将异常对象传进来Exception e
Map<String, Object> map = new HashMap<>();
//传入我们自己的错误状态码
/**
* Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
* String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
*/
request.setAttribute("javax.servlet.error.status_code", 500);
map.put("code", "user.notexist");
map.put("message", "用户出错了");
request.setAttribute("ext", map);
//因为BasicErrorController处理/error请求是自适应的。
return "forward:/error";
}
}
3. 抛自定义异常的异常Controller: HelloWorld
@Controller
public class HelloWorld {
@ResponseBody
@RequestMapping("/hello")
public String hello(@RequestParam("user") String user){
if(user.equals("aaa")){
//抛一个自定义异常
throw new UserNotExistException();
}
return "Hello World";
}
}
4. 定制错误页面:有模版引擎的话就去classpath:/templates/error路径下解析
400.html
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${timestamp}]]</h2>
<h2>error:[[${error}]]</h2>
4XX.html
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${timestamp}]]</h2>
<h2>error:[[${error}]]</h2>
5XX.html
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${timestamp}]]</h2>
<h2>error:[[${error}]]</h2>
<h2>exception:[[${exception}]]</h2>
<h2>message:[[${message}]]</h2>
<h2>company:[[${company}]]</h2>
<h2>ext:[[${ext.code}]]</h2>
<h2>ext:[[${ext.message}]]</h2>
5. 定义一个类继承了DefaultErrorAttributes重写了其中的getErrorAttributes()方法
所以BasicErrorController再处理/error时,也会调用我们自定义的类中getErrorAttributes()
方法,获取我们自定以的值。
同时将自定的异常处理类的异常信息加进来。
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("company", "atguigu");
/**
* 0:request
* 1:session
* 2:global_session
*
* webRequest是一个封装了的Request对象
* 得到异常处理器携带的数据
*/
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
errorAttributes.put("ext", ext);
return errorAttributes;
}
}