一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情。
一、Spring Boot 默认错误处理机制
创建工程
使用IDEA创建一个工程spring-boot-exception,只需要添加基本的依赖即可
Spring Boot 默认错误处理
在Web端请求Spring Boot服务出现错误时,Spring Boot默认会返回一个空白的错误页面
在其他客户端请求发生错误时会返回JSON格式的错误数据
这些都是在Spring Boot的自动配置类ErrorMvcAutoConfiguration中配置好的
ErrorMvcAutoConfiguration自动配置类中通过@Bean注解给容器中添加了一些组件
- BasicErrorController
- ErrorPageCustomizer
- DefaultErrorViewResolver
- DefaultErrorAttributes
当请求发生错误时ErrorPageCustomizer会通过registerErrorPages()方法获取path路径
获取到的path具体位置/error
通过Debug也可以确定获取到分发请求的路径是/error,也就是说当出现错误会来到/error这个映射的方法中去处理异常
而容器中注册的另一个组件BasicErrorController中可以处理/error开头的所有请求,BasicErrorController通过@RequestMapping注解定义了能够处理的请求的路径
errorHtml方法返回一个ModelAndView,并且如果ModelAndView为空就new一个ModelAndView,并传入一个name为error的View组件,也就是默认的空白页面,就是在Web页面请求发生错误时返回的页面;而error()方法返回的是一个包含了Map的ResponseEntity,也就是在其他端请求发送错误时返回的JSON格式的错误消息
当在Web端请求发生错误时,请求头中的Accept字段的值是text/html,所以才能够返回html页面
并且@RequestMapping注解中制定了produces属性的值为”text/html“
而在其他端Accept字段则为”*/*“,因此接收的是JSON格式的返回
二、Spring Boot 自定义错误页面
在BasicErrorController类中的errorHtml()方法中返回一个ModelAndView,也就是发生错误时We端显示的错误页面,而返回的这个ModelAndView首先是通过resolveErrorView()方法获取的
resolveErrorView()方法通过循环遍历所有的errorViewResolver,并调用errorViewResolver的resolverErrorView()方法来获ModelAndView,如果获取不到就返回null
errorViewResolver是ErrorViewResolver接口的实现类DefaultErrorViewResolver实例化来的,errorViewResolver通过调用自己的resolveErrorView()方法来回去ModelAndView,resolveErrorView()方法中又调用了resolve()方法来获取ModelAndView
resolve()方法首先是定义了一个errorViewName,既”error/“拼接通过参数传进来的viewName,viewName在resolveErrorView()方法中已经定义,既HttpStatus状态码或者SERIES_VIEWS枚举值
status状态码既404、405、500等,而SERIES_VIEWS枚举值为4xx、5xx
确定了errorViewName之后就是有模板引擎解析或者通过resolveResource方法遍历静态文件夹使用errorViewName拼接.html方式获取页面,从而获得一个ModelAndView并返回到BasicErrorController类的errorHtml()方法,最终返回到Web页面
自定义错误页面
根据上面的分析,想要自定义错误页面可以在templates文件夹下创建error文件夹,并且新增页面4xx.html
代码语言:javascript复制<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>ERROR</title>
</head>
<body>
<h1>Oops!404了,是不是没有写@RequestMapping</h1>
</body>
</html>
重启应用,在浏览器中输入一个该工程中不存在的地址
会自动跳转到4xx.html页面
新增controller包,增加一个HalloController并增加hallo()方法,该方法用于处理POST请求
代码语言:javascript复制@RestController
public class HalloController {
@PostMapping("/hallo")
public String hallo(){
return "success";
}
}
重启应用,访问/hallo,还是会响应4xx.html页面,此时在error文件夹中新增一个405.html
代码语言:javascript复制<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>ERROR 405</title>
</head>
<body>
<h1>Oops! 405,是不是请求方式错了</h1>
</body>
</html>
重启应用,再次访问/hallo
页面显示为405.html,如果有具体的错误码命名的错误页面,会返回具体的错误码名字的页面,否则就返回4xx.html或则5xx.html,也就是精确匹配优先
自定义页面显示异常信息
在BasicErrorController中的errorHtml()方法中创建ModelAndView时,Model中会添加一些属性,也就是说页面能获取的信息都会放在model中,model的数据时通过调用getAttributes()方法获取的
这里调用了父类的getAttributes()方法
父类中又调用类ErrorAttribute的getAttributes()方法,DefaultErrorAttribute是ErrorAttribute接口的实现类,实际调用的是DefaultErrorAttribute类中的getErrorAttributes()方法来往Model中添加属性
addStatus()方法添加了一些status信息
Model中共添加了以下这些属性(信息):
timestamp:时间戳
status:Http状态码
error:错误提示
exception:异常对象
message:异常信息
errors:校验错误信息 可以在页面上取出这些信息。修改405.html,首先添加thymeleaf名称空间
代码语言:javascript复制<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>ERROR 405</title>
</head>
<body>
<h1 th:text="${status}">Oops! 405,是不是请求方式错了</h1>
<h2 th:text="${timestamp}"></h2>
<h2 th:text="${error}"></h2>
<h2 th:text="${message}"></h2>
</body>
</html>
重启请求,访问/hallo
Model中添加的信息都能够正确的获取到
无模板引擎的情况
没有模板引擎的情况下,会遍历静态文件夹寻找页面
注释掉pom.xml文件中的thymeleaf的依赖,重启应用
无模板引擎无error文件夹的情况下
都没有的情况下,resolve()方法返回null
resolveErrorView()返回null就新建一个ModelAndView,传入name为error的View,这个View就是Spring Boot默认的空白页面
默认的错误页面error
默认的错误页面具体内容
三、Spring Boot 自定义JSON格式错误返回
自定义一个异常UserNotExistException
代码语言:javascript复制public class UserNotExistException extends RuntimeException{
public UserNotExistException(){
super("用户不存在");
}
}
在HalloController中定义一个方法,抛出500异常
代码语言:javascript复制@RequestMapping("/hi")
public String hi(@RequestParam("user") String user){
if (user.equals("stark")){
throw new UserNotExistException();
}
return "Hi!";
}
定义5xx.html页面
代码语言:javascript复制<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>5xx-Internal Serve Error</title>
</head>
<body>
<h1 th:text="${status}">Oops! 5xx,是不是后端映射方法报错了</h1>
<h2 th:text="${timestamp}"></h2>
<h2 th:text="${error}"></h2>
<h2 th:text="${message}"></h2>
<h2 th:text="${info}"></h2>
</body>
</html>
定义一个异常处理器,在处理器中可以定义返回的JSON数据
代码语言:javascript复制@ControllerAdvice
public class LilithExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Map<String, Object> handlerException(Exception e){
Map<String, Object> map = new HashMap<>();
map.put("code", "user not exist");
map.put("message", e.getMessage());
return map;
}
}
重新启动应用,访问 http://localhost:8080/hi?user=stark
浏览器返回自定义的JSON格式数据,使用PostMan发送请求
PostMan也返回自定义的JSON格式数据。
浏览器和客户端返回的都是JSON格式数据,缺点是无法自适应,既根据Web和客户端返回页面或者JSON数据,想要能够自适应Web和客户端,转发到/error,使用/error的自适应处理
代码语言:javascript复制@ResponseBody
public String handlerException(Exception e){
Map<String, Object> map = new HashMap<>();
map.put("code", "user not exist");
map.put("message", e.getMessage());
return "forward:/error";
}
重新启动该应用,Web端可以返回页面
客户端可以返回JSON数据
但是Web返回的页面不是自定义的页面
之所以解析不到自定义的错误页面,是因为这里请求返回的是200,而error文件夹中并没有2xx.html这个页面,所以还是会返回默认的空白页面
要想解决这个问题就要重新定义HttpStatus状态码
根据获取状态码时使用的属性名,来设置自己的请求的状态码
代码语言:javascript复制@ExceptionHandler(Exception.class)
public String handlerException(Exception e, HttpServletRequest request){
// 设置status
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 555);
Map<String, Object> map = new HashMap<>();
map.put("code", "user not exist");
map.put("message", e.getMessage());
return "forward:/error";
}
再次重启应用,Web浏览器可以返回自定义的错误页面
客户端返回,可以返回JSON数据
但是客户端返回的JSON格式没有返回自定义的Key
BasicErrorController注册为容器中的组件是在没有ErrorController组件的情况下才会注册,因此可以自定义ErrorController替代BasicErrorController,重写errorHtml()方法,重新定义返回页面,重写error()方法,重新定义返回的JSON格式,这就是太复杂了。
页面上能够使用的数据或者JSON返回的数据都是通过errorAttributes.getErrorAttributes()获取的
因此可以重写ErrorAttribute替代原来的DefaultErrorAttribute,将自定义的JSON中的Key设置到返回的数据中
代码语言:javascript复制@Component
public class LilithErrorAttributes extends DefaultErrorAttributes {
// 返回的Map就是页面和JSON能够获取的字段
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
//添加要显示在JSON格式中的Key
map.put("name", "stark");
// 从请求中获取异常处理保存的信息
Map info = (Map)webRequest.getAttribute("info", 0 );
map.put("info", info);
return map;
}
}
修改异常处理方法,将异常信息保存到request中
代码语言:javascript复制@ExceptionHandler(Exception.class)
public String handlerException(Exception e, HttpServletRequest request){
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 555);
Map<String, Object> map = new HashMap<>();
map.put("code", "自定义的信息,这个用户不能存在,别试了");
map.put("msg", e.getMessage());
// 将map保存到request中
request.setAttribute("info", map);
return "forward:/error";
}
重新启动应用,客户端请求 http://localhost:8080/hi?user=stark
在页面取出自定义的信息,在5xx.html的body标签中增加
代码语言:javascript复制<h2 th:text="${info}"></h2>
取出页面信息,重新启动并在浏览器输入 http://localhost:8080/hi?user=stark