默认错误处理机制

官方文档位置:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-error-handling

默认情况下,Spring Boot 提供了一个 /error 映射处理所有错误。

不同客户端响应不同格式返回体

对于机器客户端,它将生成一个 JSON 响应,其中包含错误、 HTTP 状态和异常消息的详细信息。
对于浏览器客户端,响应一个“ whitelabel”错误视图,并以 HTML 格式呈现相同的数据。
image.png
image.png

自动解析 error 目录下的错误模板

如果将 404.html / 4xx.html / 5xx.html 这样的错误页模板放在 src/main/resources/templates/error 下,Spring Boot 会自动解析。

先放置几个模板文件在该目录下:
image.png
4xx.html5xx.html404.html

错误码精准匹配

访问一个不存在的 URL:
http://127.0.0.1/123

这个 request mapping 不存在,应该会报一个 404 状态码的错误。因为路径下有 404.html 这个模板,所以 Spring Boot 优先精准匹配到了它。
image.png
image.png

错误码模糊匹配

Spring Boot 会自动根据错误码匹配路径下的模板,如果没有精准匹配的模板,则进行模糊匹配。

例如:路径下并没有 400.html 这个模板,但是有 4xx.html 。但只要是 4 开头的错误码,都会被自动匹配到 4xx.html 这个模板。

控制器:

  1. @Controller
  2. @Slf4j
  3. public class TableController {
  4. @GetMapping(path = "basic_table")
  5. public String basic_table(@RequestParam(name = "a") int a) {
  6. a = a / 0;
  7. return "table/basic_table";
  8. }
  9. }

访问 URL:
http://127.0.0.1/basic_table

因为我没有带上参数 a ,所以会报一个 MissingServletRequestParameterException 类型的错误,状态码应该会是400。

image.png
image.png

可以看到,浏览器访问时,Spring Boot 自动匹配到了 4xx.html 模板,并将错误信息渲染到页面上了。

异常处理自动配置原理

自动配置类 ErrorMvcAutoConfiguration

org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration 自动配置了异常处理规则。
image.png
部分源码:

  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnWebApplication(type = Type.SERVLET)
  3. @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
  4. // Load before the main WebMvcAutoConfiguration so that the error View is available
  5. @AutoConfigureBefore(WebMvcAutoConfiguration.class)
  6. @EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
  7. public class ErrorMvcAutoConfiguration {
  8. ...
  9. }

可以看到,ErrorMvcAutoConfiguration 使用了 @EnableConfigurationProperties 注解,相当于它绑定了一些属性,是从这些配置文件里获取的。(ServerProperties 和 WebMvcProperties)

ServerProperties:

  1. @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
  2. public class ServerProperties {

WebMvcProperties:

  1. @ConfigurationProperties(prefix = "spring.mvc")
  2. public class WebMvcProperties {

这个大家知道一下就可以。

底层组件功能分析

DefaultErrorAttributes [ 组件 ]

第一个 Bean:DefaultErrorAttributes ,组件id:errorAttributes

  1. @Bean
  2. @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
  3. public DefaultErrorAttributes errorAttributes() {
  4. return new DefaultErrorAttributes();
  5. }

很明显,在容器中没有 ErrorAttributes 这个类型的 Bean 时,才会装配一个 默认的 DefaultErrorAttributes 。

我们点进去看 DefaultErrorAttributes 的源码:

  1. @Order(Ordered.HIGHEST_PRECEDENCE)
  2. public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {

BasicErrorController [ 组件 ]

第二个 Bean:BasicErrorController,组件id:basicErrorController 。

  1. @Bean
  2. @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
  3. public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
  4. ObjectProvider<ErrorViewResolver> errorViewResolvers) {
  5. return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
  6. errorViewResolvers.orderedStream().collect(Collectors.toList()));
  7. }

我们点进去看一下 它的源码:

  1. @Controller
  2. @RequestMapping("${server.error.path:${error.path:/error}}")
  3. public class BasicErrorController extends AbstractErrorController {

使用了 @Controller 注解,显然是用来处理请求的。
@RequestMapping 注解的参数是动态取值的,默认取配置文件的:server.error.path ,如果没有配置,取冒号后面的:error.path:/error ,如果 error.path 没有配,所以默认值是:/error 。

也就是说,BasicErrorController 处理默认 /error 路径的请求。

继续往下看,有两个 @RequestMapping :

  1. @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  2. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  3. HttpStatus status = getStatus(request);
  4. Map<String, Object> model = Collections
  5. .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
  6. response.setStatus(status.value());
  7. ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  8. return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  9. }
  10. @RequestMapping
  11. public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
  12. HttpStatus status = getStatus(request);
  13. if (status == HttpStatus.NO_CONTENT) {
  14. return new ResponseEntity<>(status);
  15. }
  16. Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
  17. return new ResponseEntity<>(body, status);
  18. }

先看第一个:public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)。
注解参数 produces = MediaType.TEXT_HTML_VALUE ( text/html ),如果是浏览器,就调用这个方法响应 HTML 。

响应的 HTML 内容是什么?是由 resolveErrorView 方法返回的 ModelAdnView 或 new ModelAndView(“error”, model) 。

  1. ModelAndView modelAndView = resolveErrorView(request, response, status, model);

要么响应一个页面,要么响应一个 ResponseEntity 把 Map 里所有的数据响应出去,相当于 json 。

ErrorPageCustomizer [ 组件 ]

错误页定制化器,暂时不用管

WhitelabelErrorViewConfiguration [ 静态内部类 ]

白页/错误页配置类

  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
  3. @Conditional(ErrorTemplateMissingCondition.class)
  4. protected static class WhitelabelErrorViewConfiguration {
  5. private final StaticView defaultErrorView = new StaticView();
  6. @Bean(name = "error")
  7. @ConditionalOnMissingBean(name = "error")
  8. public View defaultErrorView() {
  9. return this.defaultErrorView;
  10. }
  11. // If the user adds @EnableWebMvc then the bean name view resolver from
  12. // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
  13. @Bean
  14. @ConditionalOnMissingBean
  15. public BeanNameViewResolver beanNameViewResolver() {
  16. BeanNameViewResolver resolver = new BeanNameViewResolver();
  17. resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
  18. return resolver;
  19. }
  20. }

它配置了一个 id 为 error 的视图组件:

  1. private final StaticView defaultErrorView = new StaticView();
  2. @Bean(name = "error")
  3. @ConditionalOnMissingBean(name = "error")
  4. public View defaultErrorView() {
  5. return this.defaultErrorView;
  6. }

在 BasicErrorController 里:

响应的 HTML 内容是什么?是由 resolveErrorView 方法返回的 ModelAdnView 或 new ModelAndView(“error”, model) 。

返回了一个 error 视图,正好容器中有一个叫 error 的视图组件。相当于 BasicErrorController 响应 HTML 时,返回的是这个静态内部类/配置类为容器注册的 error 视图组件。并且可以看到,使用的是 @ConditionalOnMissingBean 条件装配注解,不存在才注册。

那么,这个叫 error 的 View 长什么样,那就响应什么样。可以看到,它是一个 StaticView ,我们点进来看看它的源码:

  1. /**
  2. * Simple {@link View} implementation that writes a default HTML error page.
  3. */
  4. private static class StaticView implements View {
  5. private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
  6. private static final Log logger = LogFactory.getLog(StaticView.class);
  7. @Override
  8. public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
  9. throws Exception {
  10. if (response.isCommitted()) {
  11. String message = getMessage(model);
  12. logger.error(message);
  13. return;
  14. }
  15. response.setContentType(TEXT_HTML_UTF8.toString());
  16. StringBuilder builder = new StringBuilder();
  17. Object timestamp = model.get("timestamp");
  18. Object message = model.get("message");
  19. Object trace = model.get("trace");
  20. if (response.getContentType() == null) {
  21. response.setContentType(getContentType());
  22. }
  23. builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
  24. "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
  25. .append("<div id='created'>").append(timestamp).append("</div>")
  26. .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
  27. .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
  28. if (message != null) {
  29. builder.append("<div>").append(htmlEscape(message)).append("</div>");
  30. }
  31. if (trace != null) {
  32. builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
  33. }
  34. builder.append("</body></html>");
  35. response.getWriter().append(builder.toString());
  36. }
  37. private String htmlEscape(Object input) {
  38. return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
  39. }
  40. private String getMessage(Map<String, ?> model) {
  41. Object path = model.get("path");
  42. String message = "Cannot render error page for request [" + path + "]";
  43. if (model.get("message") != null) {
  44. message += " and exception [" + model.get("message") + "]";
  45. }
  46. message += " as the response has already been committed.";
  47. message += " As a result, the response may have the wrong status code.";
  48. return message;
  49. }
  50. @Override
  51. public String getContentType() {
  52. return "text/html";
  53. }
  54. }

可以看到, render 方法里的内容就是我们以前看到的默认错误页/白页。