默认错误处理机制
默认情况下,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
@Slf4j
public 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);
}
@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, 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
@ConditionalOnMissingBean
public 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);
@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();
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;
}
@Override
public String getContentType() {
return "text/html";
}
}
可以看到, render 方法里的内容就是我们以前看到的默认错误页/白页。