默认错误处理机制
默认情况下,Spring Boot 提供了一个 /error 映射处理所有错误。
不同客户端响应不同格式返回体
对于机器客户端,它将生成一个 JSON 响应,其中包含错误、 HTTP 状态和异常消息的详细信息。
对于浏览器客户端,响应一个“ whitelabel”错误视图,并以 HTML 格式呈现相同的数据。

自动解析 error 目录下的错误模板
如果将 404.html / 4xx.html / 5xx.html 这样的错误页模板放在 src/main/resources/templates/error 下,Spring Boot 会自动解析。
先放置几个模板文件在该目录下:
4xx.html5xx.html404.html
错误码精准匹配
访问一个不存在的 URL:
http://127.0.0.1/123
这个 request mapping 不存在,应该会报一个 404 状态码的错误。因为路径下有 404.html 这个模板,所以 Spring Boot 优先精准匹配到了它。

错误码模糊匹配
Spring Boot 会自动根据错误码匹配路径下的模板,如果没有精准匹配的模板,则进行模糊匹配。
例如:路径下并没有 400.html 这个模板,但是有 4xx.html 。但只要是 4 开头的错误码,都会被自动匹配到 4xx.html 这个模板。
控制器:
@Controller@Slf4jpublic class TableController {@GetMapping(path = "basic_table")public String basic_table(@RequestParam(name = "a") int a) {a = a / 0;return "table/basic_table";}}
访问 URL:
http://127.0.0.1/basic_table
因为我没有带上参数 a ,所以会报一个 MissingServletRequestParameterException 类型的错误,状态码应该会是400。


可以看到,浏览器访问时,Spring Boot 自动匹配到了 4xx.html 模板,并将错误信息渲染到页面上了。
异常处理自动配置原理
自动配置类 ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration 自动配置了异常处理规则。
部分源码:
@Configuration(proxyBeanMethods = false)@ConditionalOnWebApplication(type = Type.SERVLET)@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })// Load before the main WebMvcAutoConfiguration so that the error View is available@AutoConfigureBefore(WebMvcAutoConfiguration.class)@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })public class ErrorMvcAutoConfiguration {...}
可以看到,ErrorMvcAutoConfiguration 使用了 @EnableConfigurationProperties 注解,相当于它绑定了一些属性,是从这些配置文件里获取的。(ServerProperties 和 WebMvcProperties)
ServerProperties:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)public class ServerProperties {
WebMvcProperties:
@ConfigurationProperties(prefix = "spring.mvc")public class WebMvcProperties {
这个大家知道一下就可以。
底层组件功能分析
DefaultErrorAttributes [ 组件 ]
第一个 Bean:DefaultErrorAttributes ,组件id:errorAttributes
@Bean@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)public DefaultErrorAttributes errorAttributes() {return new DefaultErrorAttributes();}
很明显,在容器中没有 ErrorAttributes 这个类型的 Bean 时,才会装配一个 默认的 DefaultErrorAttributes 。
我们点进去看 DefaultErrorAttributes 的源码:
@Order(Ordered.HIGHEST_PRECEDENCE)public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
BasicErrorController [ 组件 ]
第二个 Bean:BasicErrorController,组件id:basicErrorController 。
@Bean@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,ObjectProvider<ErrorViewResolver> errorViewResolvers) {return new BasicErrorController(errorAttributes, this.serverProperties.getError(),errorViewResolvers.orderedStream().collect(Collectors.toList()));}
我们点进去看一下 它的源码:
@Controller@RequestMapping("${server.error.path:${error.path:/error}}")public class BasicErrorController extends AbstractErrorController {
使用了 @Controller 注解,显然是用来处理请求的。
@RequestMapping 注解的参数是动态取值的,默认取配置文件的:server.error.path ,如果没有配置,取冒号后面的:error.path:/error ,如果 error.path 没有配,所以默认值是:/error 。
也就是说,BasicErrorController 处理默认 /error 路径的请求。
继续往下看,有两个 @RequestMapping :
@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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));response.setStatus(status.value());ModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}@RequestMappingpublic 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, getErrorAttributeOptions(request, MediaType.ALL));return new ResponseEntity<>(body, status);}
先看第一个:public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)。
注解参数 produces = MediaType.TEXT_HTML_VALUE ( text/html ),如果是浏览器,就调用这个方法响应 HTML 。
响应的 HTML 内容是什么?是由 resolveErrorView 方法返回的 ModelAdnView 或 new ModelAndView(“error”, model) 。
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
—
要么响应一个页面,要么响应一个 ResponseEntity 把 Map 里所有的数据响应出去,相当于 json 。
ErrorPageCustomizer [ 组件 ]
错误页定制化器,暂时不用管
WhitelabelErrorViewConfiguration [ 静态内部类 ]
白页/错误页配置类
@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;}// If the user adds @EnableWebMvc then the bean name view resolver from// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.@Bean@ConditionalOnMissingBeanpublic BeanNameViewResolver beanNameViewResolver() {BeanNameViewResolver resolver = new BeanNameViewResolver();resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);return resolver;}}
它配置了一个 id 为 error 的视图组件:
private final StaticView defaultErrorView = new StaticView();@Bean(name = "error")@ConditionalOnMissingBean(name = "error")public View defaultErrorView() {return this.defaultErrorView;}
在 BasicErrorController 里:
响应的 HTML 内容是什么?是由 resolveErrorView 方法返回的 ModelAdnView 或 new ModelAndView(“error”, model) 。
返回了一个 error 视图,正好容器中有一个叫 error 的视图组件。相当于 BasicErrorController 响应 HTML 时,返回的是这个静态内部类/配置类为容器注册的 error 视图组件。并且可以看到,使用的是 @ConditionalOnMissingBean 条件装配注解,不存在才注册。
那么,这个叫 error 的 View 长什么样,那就响应什么样。可以看到,它是一个 StaticView ,我们点进来看看它的源码:
/*** Simple {@link View} implementation that writes a default HTML error page.*/private static class StaticView implements View {private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);private static final Log logger = LogFactory.getLog(StaticView.class);@Overridepublic 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();Object timestamp = 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());}private String htmlEscape(Object input) {return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;}private String getMessage(Map<String, ?> model) {Object path = model.get("path");String message = "Cannot render error page for request [" + path + "]";if (model.get("message") != null) {message += " and exception [" + model.get("message") + "]";}message += " as the response has already been committed.";message += " As a result, the response may have the wrong status code.";return message;}@Overridepublic String getContentType() {return "text/html";}}
可以看到, render 方法里的内容就是我们以前看到的默认错误页/白页。
