不想听原理可以直接看总结
在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等页面,当发生对应状态码异常时就会来到对应页面。
假设我们现在有一个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界面。我们自定义数据不起作用我们再回头看一下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; } }