文章目录
不想听原理可以直接看总结
原理部分
spring boot默认处理
- 浏览器,返回一个默认的错误页面
- 如果是其他客户端,默认响应一个json数据
源码分析
在spring boot中,关于错误的自动配置类是 ErrorMvcAutoConfiguration
这个自动配置类, 给容器中加入了这几个关键组件:
- ErrorPageCustomizer:
这个类的作用是定制错误的响应规则。在它的的registerErrorPages方法里的getPath方法指定了处理错误默认发送的请求为/error请求 - BasicErrorController:
这个类就是来处理/error请求的,它有如下两个方法:
@RequestMapping(produces = "text/html") // 指定响应HTML数据
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
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) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
@ResponseBody // 指定响应json数据
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);
}
spring boot 默认处理/error请求时,会根据响应头来判断发送请求的是浏览器还是app,然后响应HTML或者json数据。
浏览器这部分代码处理完请求之后返回了一个ModelAndView 指定响应的页面,这个页面会去resolveErrorView这个方法里面找。
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;
}
这个方法遍历所有的ErrorViewResolver,从这里面找返回的页面。在没有指定的情况下,默认来到了第三个注入的组件
- DefaultErrorViewResolver
这个组件的作用是指定浏览器响应的错误页面。
我们来看他是如何指定的:
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 将状态码转换为视图名称
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;
}
可以看出,默认是将响应的状态码指定为视图名称,也就是说我们如果想自定义浏览器的错误响应页面,只需要在静态资源文件夹下创建4xx, 5xx等页面,当发生对应状态码异常时就会来到对应页面。
实战部分
定制浏览器错误页面
- 没有模板引擎的情况下
将错误页面命名为 错误状态码.html 放在静态资源文件夹里面的 error文件夹下,发生此状态码的错误就会来到对应的页面。
ps:如果建立名为4xx.html的文件,当发生以4开头的状态码错误时,在没有被精确匹配的情况下,就会来到这个页面。 - 有模板引擎的情况下
当然要把错误状态码.html文件放在模板引擎文件夹下。
有模板引擎的优势在于:你可在错误页面获取一些讯息:- status:状态码
- timestamp:时间戳
- error:错误提示
- exception:异常对象
- message:异常消息
- errors:JSR303数据校验的错误都在这里
- 如果以上两个地方都没找到,就会来到默认一开始的页面。
定制json错误消息
假设我们现在有一个UserNotExistException
public class UserNotExistException extends RuntimeException {
private String id;
public String getId() {
return id;
}
public UserNotExistException(String id) {
super("用户不存在");
this.id = id;
}
}
我们可以利用springMvc的ControllerAdvice来定制这个异常的处理
@ControllerAdvice // 这是一个专门处理异常的Controller
public class ControllerExceptionHandler {
@ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
@ResponseBody // 已json的形式返回
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 指定响应的错误状态码为404,不指定默认是200
public Map<String, Object> handleUserNotExistException(UserNotExistException ex) {
Map<String, Object> result = new HashMap<>();
result.put("id", ex.getId());
result.put("message", ex.getMessage());
return result;
}
}
这么写,我们app端访问该异常时能够正确返回我们定制的json数据。
但是存在一个问题:浏览器出现该异常时返回的也是json数据,而不是我们定制的404.html页面,失去了spring boot默认的自适应效果。
如何达到自适应效果
我们知道spring boot在处理/error请求时,默认就是自适应的。
所以我们想要得到自适应效果,只需要将请求转发到/error就好了。
@ControllerAdvice // 这是一个专门处理异常的Controller
public class ControllerExceptionHandler {
@ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
@ResponseStatus(HttpStatus.NOT_FOUND) // 指定响应的错误状态码,不指定默认是200
public String handleUserNotExistException(UserNotExistException ex) {
Map<String, Object> result = new HashMap<>();
result.put("id", ex.getId());
result.put("message", ex.getMessage());
return "forward:/error";
}
}
这样确实能达到自适应效果,但又出现新的问题
- 设置响应状态码的注解不起作用,还是默认的200。导致无法定位我们为浏览器设置的404.html界面。
- 我们自定义数据不起作用
如何传递状态码信息给/error请求
我们再回头看一下BasicErrorController里的方法,看它是如何获取状态码的。然后我们就发现了HttpStatus status = getStatus(request);
方法。点进去一看:
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
...
}
原来它是从request域中按照这个javax.servlet.error.status_code属性获取的。
所以我们把它的参数HttpServletRequest也拿到我们的ExceptionHandler方法中。
然后设置request域中属性的值
@ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
public String handleUserNotExistException(UserNotExistException ex, HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
result.put("id", ex.getId());
result.put("message", ex.getMessage());
request.setAttribute("javax.servlet.error.status_code", 404);
return "forward:/error";
}
成功传入状态码。但是没有自定义数据。
最终版:自适应、自定义的异常处理
我们已经完成自适应了,接下来再说自定义。
再回头看一看BasicErrorController的源码。看它是怎么获取响应数据的。
然后我们发现,不管是浏览器返回的model还是app返回的map。数据都是由getErrorAttributes这个方法获取的。点进来。
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
看一下返回的这个this.errorAttributes:
public abstract class AbstractErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
...
public AbstractErrorController(ErrorAttributes errorAttributes) {
this(errorAttributes, null);
}
...
}
我们发现这个ErrorAttributes是从容器中获取的。
而ErrorAttributes在我们一开始说的ErrorMvcAutoConfiguration这个自动配置类中第一个注入的就是DefaultErrorAttributes!
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
我们可以看到在它的getErrorAttributes方法中,设置了我们可以在模板引擎中获取的数据。
@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;
}
也就是说,如果我们要自定义消息,只需要重写这个getErrorAttributes方法就好了。
先在ControllerExceptionHandler的handleUserNotExistException方法中,在转发请求之前添加一行代码:request.setAttribute("extResult", result);
将我们自定义的数据一并转发过去。
然后继承DefaultErrorAttributes重写getErrorAttributes方法。
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
// 父类返回的map
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
// 自定义的添加数据
map.put("author", "joker");
// 从请求域中获得我们自己添加的数据
Map<String, Object> extResult = (Map<String, Object>) requestAttributes.getAttribute("extResult", 0);
map.put("extResult", extResult);
return map;
}
}
总结:如何实现自适应、自定义的异常处理
- 编写一个异常类
public class UserNotExistException extends RuntimeException {
private String id;
public String getId() {
return id;
}
public UserNotExistException(String id) {
super("用户不存在");
this.id = id;
}
}
- 写一个ControllerAdvice指定一个ExceptionHandler方法处理该异常
@ControllerAdvice // 这是一个专门处理异常的Controller
public class ControllerExceptionHandler {
@ExceptionHandler(UserNotExistException.class) // 这个方法处理UserNotExistException异常
public String handleUserNotExistException(UserNotExistException ex, HttpServletRequest request) {
// 自定义消息
Map<String, Object> result = new HashMap<>();
result.put("id", ex.getId());
result.put("message", ex.getMessage());
// 设置错误状态码
request.setAttribute("javax.servlet.error.status_code", 404);
// 将自定义消息放入转发域
request.setAttribute("extResult", result);
// 转发到/error请求,使其获得自适应效果
return "forward:/error";
}
}
- 编写一个类继承DefaultErrorAttributes重写getErrorAttributes方法。
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
// 父类返回的map
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
// 自定义的添加数据
map.put("author", "joker");
Map<String, Object> extResult = (Map<String, Object>) requestAttributes.getAttribute("extResult", 0);
map.put("extResult", extResult);
return map;
}
}
- 在模板引擎文件夹或者静态资源文件夹中加入 错误状态码.html