当项目代码执行错误(5XX)、找不到页面(4xx)或其他错误时,SpringBoot 自动将请求转发到 /error 下进行处理,所以每次出现错误我们都能得到这样一个页面。相信很多人都跟我一样,看到这个页面就想到是页面找不到(404)。但是却没有注意到 Spring 给我们的这么多提示。无意中发现这个,我们访问的路径是 /logins,但是错误提示确实 /error找不到映射。这是为什么呢?会不会是 SpringBoot 默认的一种错误处理方式?
找了几篇博客,看了几个视频,终于找到原因,下面通过源码简单探索一下SpringBoot默认错误处理的机制。
1. SpringBoot 错误处理机制是区分浏览器和客户端的,如果是浏览器访问,默认返回错误页面,如果是客户端访问默认是返回错误的JSON数据。 通过PostMan访问这个页面可以得到这样一串JSON数据
{
"timestamp": 1567084756940,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/logins"
}
2. 错误处理机制的源码实现,首先可以找到相关的自动配置类:ErrorMvcConfiguration.java(不要问我怎么知道的),在这个自动配置类里我们很容易在 101 行找到一个很熟悉的东西-----BasicErrorController,SpringBoot默认注入了这个错误处理的控制器组件。
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
3. 在这个BasicErrorController可以发现
1)通过类上的@RequestMapping注解可以知道,SpringBoot 默认的错误处理映射路径 /error
2)从 errorHtml 方法上面的注解 @RequestMapping(produces = "text/html") 可以知道这个方法是返回错误页面的;
从 error方法上面的注解 @ResponseBody 可以知道这个方法是返回错误JSON数据的;
3)在处理错误请求时,调用父类的 getErrorAttributes 方法来获取错误信息,调用父类的 resolveErrorView 解析错误视图
package org.springframework.boot.autoconfigure.web;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.AbstractErrorController;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace;
import org.springframework.boot.autoconfigure.web.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
@Controller
// 此处会从配置文件中读取默认的错误映射路径 server.error.path
// 如果没有配置 server.error.path,则会继续查找 error.path 有没有配置
// 如果 error.path 也没有配置,会使用默认映射路径的路径 /error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties) {
this(errorAttributes, errorProperties,
Collections.<ErrorViewResolver>emptyList());
}
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@Override
public String getErrorPath() {
return this.errorProperties.getPath();
}
/**
* 响应错误页面的方法
*/
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
// 获取 Http 状态码
HttpStatus status = getStatus(request);
// 进入 getErrorAttributes 方法获取错误信息,我们在页面上看到的错误信息就是在这获取的,这个方法继承自父类 AbstractErrorController
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 进入 resolveErrorView 方法解析错误视图,这个方法继承自父类 AbstractErrorController
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 如果解析视图失败,则获取默认的空白视图"error",这是SpringBoot默认提供的视图,在文章的第7点中会讲到
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
/**
* 响应错误JSON数据的方法
*/
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 获取错误信息
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
// 获取 Http 状态码
HttpStatus status = getStatus(request);
// 直接返回错误数据
return new ResponseEntity<Map<String, Object>>(body, status);
}
protected boolean isIncludeStackTrace(HttpServletRequest request,
MediaType produces) {
IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
if (include == IncludeStacktrace.ALWAYS) {
return true;
}
if (include == IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}
4. 进入 AbstractErrorController 找到 getErrorAttributes 和 resolveErrorView 两个方法。在 getErrorAttributes 方法中又调用了 ErrorAttributes 接口 的 getErrorAttributes 方法来获取错误信息。在 resolveErrorView 中:遍历容器中所有的 ErrorViewResolver 来解析视图,如果解析得到,则返回视图,否则返回null 。
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
// 调用 ErrorAttributes 的 getErrorAttributes 方法获取错误参数
// ErrorAttributes 是一个接口,进入他的实现类的 getErrorAttributes 方法
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
/**
* Resolve any specific error views. By default this method delegates to
* {@link ErrorViewResolver ErrorViewResolvers}.
* @param request the request
* @param response the response
* @param status HTTP状态码
* @param model 错误信息
* @return a specific {@link ModelAndView} or {@code null} if the default should be
* used
* @since 1.4.0
*/
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
// 遍历 Spring 容器中所有 ErrorViewResolver 错误视图解析器
for (ErrorViewResolver resolver : this.errorViewResolvers) {
// 调用 ErrorViewResolver 视图解析器 的 resolveErrorView 方法解析视图
// ErrorViewResolver 是一个接口,进入他的实现类的 resolveErrorView 方法
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
5. 进入 ErrorAttributes 类的 getErrorAttributes 方法。ErrorAttributes 主要用于解析错误信息。
package org.springframework.boot.autoconfigure.web;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
/**
* Default implementation of {@link ErrorAttributes}. Provides the following attributes
* when possible:
* <ul>
* <li>timestamp - The time that the errors were extracted</li>
* <li>status - The status code</li>
* <li>error - The error reason</li>
* <li>exception - The class name of the root exception</li>
* <li>message - The exception message</li>
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception
* <li>trace - The exception stack trace</li>
* <li>path - The URL path when the exception was raised</li>
* </ul>
*
* @author Phillip Webb
* @author Dave Syer
* @author Stephane Nicoll
* @since 1.1.0
* @see ErrorAttributes
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
implements ErrorAttributes, HandlerExceptionResolver, Ordered {
private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName()
+ ".ERROR";
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
storeErrorAttributes(request, ex);
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_ATTRIBUTE, ex);
}
/**
* 获取的错误信息
* @param requestAttributes
* @param includeStackTrace 是否包含错误堆栈详细信息
*/
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
// 获取时间戳
errorAttributes.put("timestamp", new Date());
// 获取 Http 状态码
addStatus(errorAttributes, requestAttributes);
// 获取错误详细信息
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
// 获取错误路径信息
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
private void addStatus(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes) {
// 从 requestAttributes 对象中获取 name 为 javax.servlet.error.status_code 的值
// 可以得出,如果想手动定义错误状态码status的,只需要设置 HttpRequest 中 name 为 javax.servlet.error.status_code 的值
Integer status = getAttribute(requestAttributes,
"javax.servlet.error.status_code");
// 如果没有获取到错误状态,则设置默认的 999 状态码和错误信息 None
if (status == null) {
errorAttributes.put("status", 999);
errorAttributes.put("error", "None");
return;
}
// 获取到错误状态码时,从HttpStatus中获取错误信息
// HttpStatus 是枚举类型,包含两个属性错误状态码 value 和错误原因 reasonPhrase
errorAttributes.put("status", status);
try {
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
}
catch (Exception ex) {
// 找到对应值的 reasonPhrase 则会发生异常,获取异常信息
errorAttributes.put("error", "Http Status " + status);
}
}
private void addErrorDetails(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes, boolean includeStackTrace) {
// 从 requestAttributes 获取异常对象
Throwable error = getError(requestAttributes);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = ((ServletException) error).getCause();
}
// 获取异常类型
errorAttributes.put("exception", error.getClass().getName());
// 获取异常信息
addErrorMessage(errorAttributes, error);
// 添加堆栈跟踪信息
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
// 获取 requestAttributes 携带的异常信息
Object message = getAttribute(requestAttributes, "javax.servlet.error.message");
// 如果上面获取不到 error 对象,则获取 requestAttributes 携带的错误信息
if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
&& !(error instanceof BindingResult)) {
errorAttributes.put("message",
StringUtils.isEmpty(message) ? "No message available" : message);
}
}
/**
* 获取 JSR-303校验的错误信息
* @param errorAttributes
* @param error
*/
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
BindingResult result = extractBindingResult(error);
if (result == null) {
errorAttributes.put("message", error.getMessage());
return;
}
if (result.getErrorCount() > 0) {
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("message",
"Validation failed for object='" + result.getObjectName()
+ "'. Error count: " + result.getErrorCount());
}
else {
errorAttributes.put("message", "No errors");
}
}
/**
* 提取BindingResult对象,BindingResult 包含 JSR-303校验的错误信息
* @param error 异常对象
* @return
*/
private BindingResult extractBindingResult(Throwable error) {
if (error instanceof BindingResult) {
return (BindingResult) error;
}
if (error instanceof MethodArgumentNotValidException) {
return ((MethodArgumentNotValidException) error).getBindingResult();
}
return null;
}
/**
* 获取异常的堆栈跟踪信息
* @param errorAttributes 错误信息集合
* @param error 异常对象
*/
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
errorAttributes.put("trace", stackTrace.toString());
}
/**
* 获取请求路径
* @param errorAttributes
* @param requestAttributes
*/
private void addPath(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes) {
String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri");
if (path != null) {
errorAttributes.put("path", path);
}
}
/**
* 获取异常信息
* @param requestAttributes
* @return
*/
@Override
public Throwable getError(RequestAttributes requestAttributes) {
Throwable exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE);
if (exception == null) {
exception = getAttribute(requestAttributes, "javax.servlet.error.exception");
}
return exception;
}
@SuppressWarnings("unchecked")
private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
}
}
6. 进入 ErrorViewResolver 的 resolveErrorView 方法,ErrorViewResolver主要用于解析错误视图。通过这个类,可以通过错误请求的 HttpStatus 错误代码将 template 文件夹下的 error/HttpStatus.html 文件解析成我们想要的视图。如果template下没有对应的文件,SpringBoot 还会去静态资源文件夹下找 对应的 HTML文件。
package cn.edu.zut.rsc.vote.controller;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.web.ErrorViewResolver;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus.Series;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
// Series 是一个枚举类型,默认有以下五个值:
// INFORMATIONAL(1),SUCCESSFUL(2),REDIRECTION(3),CLIENT_ERROR(4),SERVER_ERROR(5);
// 即 views = {"CLIENT_ERROR": "4xx", "SERVER_ERROR": "5xx"}
// 也就是所有 1xx 的 Http状态码,Series都将其定义为信息
// 所有 2xx 的 Http状态码,Series都将其定义为成功
// 所有 3xx 的 Http状态码,Series都将其定义为转发
// 所有 4xx 的 Http状态码,Series都将其定义为客户端错误
// 所有 5xx 的 Http状态码,Series都将其定义为服务端错误
Map<Series, String> views = new HashMap<Series, String>();
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private int order = Ordered.LOWEST_PRECEDENCE;
public DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(
applicationContext);
}
DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties,
TemplateAvailabilityProviders templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
/**
* 1.解析错误视图
* @param request
* @param status Http状态码
* @param model 错误信息
* @return
*/
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 通过 Http 状态码解析视图
ModelAndView modelAndView = resolve(String.valueOf(status), model);
// 查看 HttpStatus.series() 的源码发现就一行代码 HttpStatus.Series.valueOf(this)
// 而 Series.valueOf() 实际是先获取了一个 HttpStatus 的百位数值,也就是(1,2,3,4,5),再用这数值获取对应的 Series 返回
// 举个例子:如果是 status 是 404:404 的百位数值是4,则返回 CLIENT_ERROR,500的话:500的百位数值为5,返回 SERVER_ERROR
// 这里其实是 SpringBoot 提供的一种通配符机制,所有 4xx 和 5xx 状态码 经过这一步操作都转换成"4xx"或"5xx"
// 再调用 resolve("4xx", model),resolve("5xx", model) 获取对应的视图
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
/**
* 2.解析错误视图
* @param viewName 视图名,这里即 Http 状态码
* @param model 错误信息
* @return
*/
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 将 error 和 Http状态码拼接成视图名,如404: error/404,500: error/500
String errorViewName = "error/" + viewName;
// 使用模板引擎解析这个视图名,也就是说只要在template目录下存在这样一个页面(error/404.html)
// 模板引擎就会将这个页面解析成错误视图
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
// 如果解析成功则返回对应的视图
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
// 如果解析失败:没有使用模板引擎或 template 没有对应的 html 文件
return resolveResource(errorViewName, model);
}
/**
* 3.解析错误视图
* @param viewName 视图名,这里即 error/HttpStatus
* @param model 错误信息
* @return
*/
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 遍历项目静态资源路径
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
// 关联项目静态资源路径下对应的 error/HttpStatus.html
resource = resource.createRelative(viewName + ".html");
// 如果该文件存在,获取对应的HtmlResourceView初始化ModelAndView并返回
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
// 静态资源目录下没有找到对应的文件则返回null
return null;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
private static class HtmlResourceView implements View {
private Resource resource;
HtmlResourceView(Resource resource) {
this.resource = resource;
}
@Override
public String getContentType() {
return MediaType.TEXT_HTML_VALUE;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
FileCopyUtils.copy(this.resource.getInputStream(),
response.getOutputStream());
}
}
}
7. SpringBoot默认提供的空白错误页面,也就是文章最开始的截图。当视图解析失败时,SpringBoot就会返回这样一个默认的空白错误提示页面。
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final ErrorMvcAutoConfiguration.SpelView defaultErrorView = new ErrorMvcAutoConfiguration.SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// 将空白错误视图加入到 Spring 容器中
@Bean
@ConditionalOnMissingBean(BeanNameViewResolver.class)
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
8. 总结
1)发生错误时,可通过 HttpServletRequest.setAttribute("javax.servlet.error.status_code", status) 来修改 HttpStatus 状态码。
2)可在配置文件中配置 server.error.path(建议) 或 error.path(已经被废弃了) 来修改默认的 /error 错误转发处理的路径
3)有模板引擎的情况下,可以在 template 目录下新建 error 文件夹,创建对应错误状态码的Html文件,SpringBoot 会将错误自动解析到对应的错误状态码.html上;
没有使用模板引擎,在静态资源问价夹 {"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"} 下创建 error 文件夹并在 error 文件夹下创建对应状态码的 Html 文件也可解析得到。
注意:两种方法都必须先创建 error 文件夹,再在 error 文件夹下创建对应的 Html 文件。有模板引擎时,可以通过模板引擎将错误信息显示到页面上。