1.8、自动异常处理
Spring Boot 通过配置类 ErrorMvcAutoConfiguration 对异常处理提供了自动配置,该配置类向容器中注入了以下 4 个组件。
-
ErrorPageCustomizer:该组件会在在系统发生异常后,默认将请求转发到“/error”上。
-
BasicErrorController:处理默认的“/error”请求。
-
DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上。
-
DefaultErrorAttributes:错误属性处理工具,可以从请求中获取异常或错误信息
1.8.1、响应规则
ErrorMvcAutoConfiguration 向容器中注入了一个名为 ErrorPageCustomizer 的组件,它主要用于定制错误页面的响应规则。
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
通过 registerErrorPages() 方法来注册错误页面的响应规则。当系统中发生异常后,ErrorPageCustomizer 组件会自动生效
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
//服务器属性
private final ServerProperties properties;
//Servlet 路径
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//将请求转发到 /errror(this.properties.getError().getPath())
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
// 注册错误页面
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
1.8.2、错误控制器
ErrorMvcAutoConfiguration 还向容器中注入了一个错误控制器组件 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,Spring Boot 通过 BasicErrorController 进行统一的错误处理,主要用来处理 路径为 /error 的请求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//错误属性
private final ErrorProperties errorProperties;
.....构造方法....
//用于处理浏览器客户端的请求发生的异常
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//获取错误状态码
HttpStatus status = getStatus(request);
//根据错误信息来封装 model 数据,用于页面显示
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
//为响应对象设置错误状态码
response.setStatus(status.value());
//调用 resolveErrorView() 方法,使用视图解析器生成 ModelAndView 对象
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//设置错误相应的页面 error
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//用于处理机器客户端的请求发生的错误(例如安卓、IOS、Postman 等等)
@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);
}
}
当使用浏览器访问出现异常时,会进入 BasicErrorController 控制器中的 errorHtml() 方法进行处理,在 errorHtml() 方法中会调用父类(AbstractErrorController)的 resolveErrorView() 方法,来获取容器中所有的 ErrorViewResolver 对象(错误视图解析器,包括 DefaultErrorViewResolver 在内),一起来解析异常信息。
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,Map<String, Object> model) {
//获取容器中的所有的错误视图解析器来处理该异常信息
for (ErrorViewResolver resolver : this.errorViewResolvers) {
//调用视图解析器的 resolveErrorView 解析到错误视图页面
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
1.8.3、视图解析器
ErrorMvcAutoConfiguration 向容器中注入了一个默认的错误视图解析器组件 DefaultErrorViewResolver
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
当发出请求的客户端为浏览器时,Spring Boot 会获取容器中所有的 ErrorViewResolver 对象,并分别调用它们的 resolveErrorView() 方法对异常信息进行解析。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
//系列视图
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
............
//解决错误视图
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//尝试以错误状态码作为错误页面名进行解析
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//以 4xx 或 5xx 作为错误页面页面进行解析
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//错误模板页面,例如 error/404
String errorViewName = "error/" + viewName;
//当模板引擎可以解析这些模板页面时,就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext);
if (provider != null) {
//在模板能够解析到模板页面的情况下,返回 errorViewName 指定的视图
return new ModelAndView(errorViewName, model);
}
//若模板引擎不能解析,则去静态资源中查找 errorViewName 对应的页面
return resolveResource(errorViewName, model);
}
//在静态资源文件中寻找errorViewName 对应的页面
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//遍历所有静态资源文件夹
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
//若静态资源文件夹下存在以上错误页面,则直接返回
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
}
DefaultErrorViewResolver根据错误状态码(例如 404、500、400 等)生成一个错误视图 error/status,然后尝试解析,从 classpath 类路径下的 templates 目录下,查找 error/status.html,若模板引擎能够解析到 error/status 视图,则将视图和数据封装成 ModelAndView 返回并结束。若找不到,则依次从静态资源文件夹中查找 error/status.html,若在静态文件夹中找到了该错误页面,则返回并结束整个解析流,如果还是没有找到,则处理默认的 “/error ”请求,使用 Spring Boot 默认的错误页面(Whitelabel Error Page)。
1.8.4、错误属性处理
ErrorMvcAutoConfiguration 向容器中注入了一个组件默认错误属性处理工具 DefaultErrorAttributes,可以从请求中获取异常或错误信息,并将其封装为一个 Map 对象返回。
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
//获取错误属性
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (Boolean.TRUE.equals(this.includeException)) {
options = options.including(Include.EXCEPTION);
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
@Override
@Deprecated
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;
}
//添加错误状态码
private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE);
if (status == null) {
errorAttributes.put("status", 999);
errorAttributes.put("error", "None");
return;
}
errorAttributes.put("status", status);
try {
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
}
catch (Exception ex) {
// 无法获取原因
errorAttributes.put("error", "Http Status " + status);
}
}
//添加错误详细信息
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest,
boolean includeStackTrace) {
Throwable error = getError(webRequest);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = error.getCause();
}
errorAttributes.put("exception", error.getClass().getName());
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
addErrorMessage(errorAttributes, webRequest, error);
}
//添加错误/异常消息
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
BindingResult result = extractBindingResult(error);
if (result == null) {
addExceptionErrorMessage(errorAttributes, webRequest, error);
}
else {
addBindingResultErrorMessage(errorAttributes, result);
}
}
//添加导致请求处理失败的异常对象
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
Object message = getAttribute(webRequest, RequestDispatcher.ERROR_MESSAGE);
if (StringUtils.isEmpty(message) && error != null) {
message = error.getMessage();
}
if (StringUtils.isEmpty(message)) {
message = "No message available";
}
errorAttributes.put("message", message);
}
//添加绑定结果错误信息
private void addBindingResultErrorMessage(Map<String, Object> errorAttributes, BindingResult result) {
errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. "
+ "Error count: " + result.getErrorCount());
errorAttributes.put("errors", result.getAllErrors());
}
//错误/异常栈信息
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());
}
//错误/异常抛出时所请求的URL路径
private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
String path = getAttribute(requestAttributes, RequestDispatcher.ERROR_REQUEST_URI);
if (path != null) {
errorAttributes.put("path", path);
}
}
//错误的提示
@Override
public Throwable getError(WebRequest webRequest) {
Throwable exception = getAttribute(webRequest, ERROR_ATTRIBUTE);
return (exception != null) ? exception : getAttribute(webRequest,RequestDispatcher.ERROR_EXCEPTION);
}
}
在BasicErrorController处理错误时,会调用DefaultErrorAttributes的 getErrorAttributes() 方法获取错误或异常信息,并封装成 model 数据(Map 对象),返回到页面或 JSON 数据中。
- timestamp:时间戳;
- status:错误状态码
- error:错误的提示
- exception:导致请求处理失败的异常对象
- message:错误/异常消息
- trace: 错误/异常栈信息
- path:错误/异常抛出时所请求的URL路径