默认错误处理
重要组件
配置类:ErrorMvcAutoConfiguration
重要组件:
DefaultErrorAttributes
作为设置 Model 数据存在, 在错误页面中共享数据,也就是说可以使用这些数据。
- timestamp
- status
- error
- exception
- message
- path
- errors:与数据校验 JSR 303 有关
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
BasicErrorController
这是一个控制器,由 SpringBoot 自动注册到容易中的。从名字中可以看出来,这是一个基础的错误控制器。
从实际代码看起来,可以在配置文件中配置错误请求路由:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
// ......
}
首先获取配置项:server.error.path,若是没有则获取:error.path,若是没有则默认为:/error
而默认的:/error 以及 error.path 配置项,正是 ErrorPageCustomizer 这个组件访问的默认的路径。
SpringBoot 对不同客户端的错误请求会呈现不同的处理结果,若是浏览器,手机等可视化的客户端,将会返回 text/html 文件,若是像 PostMan,其它特定需求的客户端,则返回 json 格式的数据。
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // TEXT_HTML_VALUE = "text/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
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, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
对于错误请求,响应有两种方式:
- text/html
- application/json
如下图,使用 PostMan 访问:

使用浏览器访问:
呈现的结果就不同。
是如何渲染错误页面?
@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
ErrorPageCustomizer
- 根据需要,使用给定的注册表注册页面
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
定制错误处理
通过上面的源码解析,可以知道如何自定义错误页面以及响应 Json 格式的数据。同时,还可以获取在出现这些错误情况的时候,能获取到哪些数据。
定制错误页面
-
有模板引擎的时候,就是在模板引擎的规则下搜索。
- 模板引擎的 prefix + /error/状态码 + 模板引擎的 suffix
- 例如说:templates/error/404.html
-
默认规则
-
在能找到对应页面的时候,优先匹配精准的页面(4**,5**)
-
若是没有找到精准的页面,那么会根据状态码除于 100 若是等于 4、5,则搜索 error 目录下的 4xx、5xx 页面。
-
若是以上规则都不满足,则直接返回 error 作为错误视图名称
- 要么从模板引擎规则下搜索到
- 要么从SpringBoot 默认的静态资源路径下搜索到
- 要么是 SpringBoot 内置的 error 错误页面
@Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final StaticView defaultErrorView = new StaticView(); // 标注四 @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } }
在标注四的位置,这个就是 SpringBoot 的默认 error 页面,其实本质就是 StaticView 类创建出来的一段代码字符串。
@Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = getMessage(model); logger.error(message); return; } response.setContentType(TEXT_HTML_UTF8.toString()); StringBuilder builder = new StringBuilder(); Date timestamp = (Date) model.get("timestamp"); Object message = model.get("message"); Object trace = model.get("trace"); if (response.getContentType() == null) { response.setContentType(getContentType()); } builder.append("<html><body><h1>Whitelabel Error Page</h1>").append( "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>") .append("<div id='created'>").append(timestamp).append("</div>") .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error"))) .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>"); if (message != null) { builder.append("<div>").append(htmlEscape(message)).append("</div>"); } if (trace != null) { builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>"); } builder.append("</body></html>"); response.getWriter().append(builder.toString()); }
-
定制错误 JSON 数据
实现返回的错误 JSON 格式的数据有几种方式:
- 使用环切注解:@ControllerAdvice,同时搭配:@ExceptionHandler
- 自定义异常处理控制器,可以继承 BasicErrorController
- 自定义异常属性类,可以继承 DefaultErrorAttributes 类
使用 @ControllerAdvice
创建要给标注为 @ControllerAdvice 注解的类,并且同时在方法上标注 @ExceptionHandler 注解,指定处理哪些异常。
可以搭配 @ModelAttribute 注解,将数据放入请求域中。
@ControllerAdvice
public class UserExceptionHandler {
@ModelAttribute
public Model test(Model map) {
map.addAttribute("error", "test");
return map;
}
@ExceptionHandler(RuntimeException.class)
public String handleException(Exception e) {
return "forward:/error";
}
}
缺点:
- @ModelAttribute 注解也无法重写错误信息里面的格式,也就是无法覆盖。所以,返回的 JSON 格式还是一样 SpringBoot 返回。
继承 BasicErrorController
这个有些复杂,暂时不考虑,可以从将其中的两个方法重写即可:
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // text/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
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, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
继承 DefaultErrorAttributes 类
重写 getErrorAttributes 方法,然后调用父类的方法,只做一些略微的数据修改,就很简单啦。
同时,这里可以接受 request 中的数据,也就是在环切中的 @ExceptionHandler 方法中可以使用 request 存储数据,然后在这里取出来使用。
@Component
public class UserErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.remove("timestamp");
map.put("code", map.get("status"));
map.remove("status");
map.remove("error");
return map;
}
}
注意
对于上面的拦截方式,其中 @ControllerAdvice 注解的 @ExceptionHandler 方法是无法拦截请求不存在 url 或者 filter 过滤器等一些特殊情况的异常。
获取不了自定义异常信息,可以在配置文件中添加 server.error.include-exception=true 配置
扩展
- @ControllerAdvice:https://docs.spring.io/spring-framework/docs/5.0.0.M1/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html#basePackageClasses--
- @InitBinder
- @ModelAttribute
Q.E.D.
Comments | 1 条评论