划重点
- 异常处理自动配置类为
ErrorMvcAutoConfiguration
- 异常信息保存在
DefaultErrorAttributes
中 - 异常捕获在我们最熟悉的
doDispatch()
方法 - 异常处理首先会遍历
处理异常解析器
,判断是否自定义了异常处理,也就是@ExceptionHandler
、ResponseStatus
等,这样我们不仅可以自定义异常处理逻辑,而且可以设定返回结果 - 当没有自定义异常处理类,则会默认请求
/error
,该请求会跳转到自动配置类里面注册的BasicErrorController
,该控制类负责跳转到自定义的异常页面或者默认白页,这种我们是不可以自定义异常处理逻辑的 - 上面两者的区别,第一种是可以自定义异常处理逻辑,然后返回视图或者JSON,而第二种只能跳转到定义的异常页面,无法自定义处理逻辑
- 当存在自定义异常页面则正确跳转,否则就会跳转到默认的白页
whitepage
- 我们之前经常看到的
whitepage
,都是在后台拼接成的网页内容,然后返回,处理类为StaticView
一、默认规则
1.官方解释
默认情况下,Spring Boot 提供了一个`/error`以合理方式处理所有错误的映射,并将其注册为 servlet 容器中的“全局”错误页面。对于机器客户端,它会生成一个 JSON 响应,其中包含错误、HTTP 状态和异常消息的详细信息。对于浏览器客户端,有一个“whitelabel”错误视图,它以 HTML 格式呈现相同的数据
server.error如果要自定义默认错误处理行为,可以设置许多属性。请参阅附录的“服务器属性”部分。
要完全替换默认行为,您可以实现ErrorController并注册该类型的 bean 定义或添加类型的 beanErrorAttributes以使用现有机制但替换内容。
2.自定义错误页面
如果想要根据错误状态码来自定义HTML错误页面,那么可以将文件添加到/error
目录。错误页面可以是静态 HTML(即添加到任何静态资源目录下),也可以使用模板构建。文件名应该是准确的状态码或序列掩码。
即在静态目录
或templates
下创建/error
目录,错误页面存放在这里,文件名以状态码定义,例如:404.html
,500.html
,5xx.html
二、异常处理自动配置
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 {}
DefaultErrorAttributes 错误属性
定义错误页面包含哪些数据
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) // 存在用自定义的,没有则用默认的
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
BasicErrorController 错误处理器
向容器中注册该Bean
@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()));
}
注册的Bean是一个Controller,@RequestMapping
里规定了映射路径,优先取server.error.path
,没有则用 /error
处理默认的 /error 路径的请求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {}
DefaultErrorViewResolverConfiguration 错误视图解析器
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ WebProperties.class, WebMvcProperties.class })
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final Resources resources;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, WebProperties webProperties) {
this.applicationContext = applicationContext;
this.resources = webProperties.getResources();
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
// 默认的错误视图解析器
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
}
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();
// 向容器中注册一个id=error的视图
// 出现异常时,首先会去找自定义的异常页面,不存在时则会返回一个error视图,及默认的视图
@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;
}
}
StaticView 白页内容
静态视图,我们的错误视图是在这里拼接好的
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";
}
}
三、异常处理流程
doDispatch()
在该方法中,主体都被try/catch
包围
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
// 定义1个异常信息
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 当执行目标方法出现异常,会立刻被catch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
// 获取异常信息
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 处理结果 这里会将异常信息dispatchException传入,根据此来判断是否存在异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
processDispatchResult()
根据异常信息判断是否存在出现了异常
- 存在则跳转到异常页面,返回一个
ModelAndView
,就和正常视图一样,不过从外面看就像异常处理,从源码看就是捕获到,返回到异常视图 - 不存在异常则顺利执行
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
// 判断是否存在异常信息
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 处理异常
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
processHandlerException()
通过处理异常解析器,遍历处理异常
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
// 处理器异常解析器
if (this.handlerExceptionResolvers != null) {
// 遍历所有的异常解析器
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
// 处理异常信息
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
// 默认没有任何人可以处理异常 异常会被抛出
throw ex;
}
HandlerExceptionResolver
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
第一个异常解析器
- 将异常信息存储到
request
域中 - 该解析器永远返回一个null
- 因为异常处理是捕获到异常后,将请求转发到
/error
,但是异常信息需要携带过去,所以在这里只是设置了一个属性
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
private static final String ERROR_INTERNAL_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
@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域中
request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
}
// 省略部分代码...
}
第二个异常解析器组合
- 遍历所有的处理异常解析器
- 具体内容放到下一章
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
@Nullable
private List<HandlerExceptionResolver> resolvers;
@Override
@Nullable
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (this.resolvers != null) {
// 遍历所有的异常解析器
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}
}
四、自定义异常处理逻辑
捕获到异常,可以添加自定义代码逻辑,并且返回结果可以是ModelAndView
或JSON
@ExceptionHandler
标注捕获的异常
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
// 异常类型
Class<? extends Throwable>[] value() default {};
}
@ControllerAdvice
标注异常处理类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
// 异常类
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
ExceptionHandlerExceptionResolver
该类负责处理 @ExceptionHandler
@Override
@Nullable
// 返回结果也是一个ModelAndView
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 判断是否需要解析异常
if (shouldApplyTo(request, handler)) {
prepareResponse(ex, response);
// 解析异常
ModelAndView result = doResolveException(request, response, handler, ex);
if (result != null) {
// Print debug message when warn logger is not enabled.
if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
}
// Explicitly configured warn logger in logException method.
logException(ex, request);
}
return result;
}
else {
return null;
}
}
@ResponseStatus
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseStatus {
// 异常状态码
@AliasFor("code")
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
@AliasFor("value")
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
// 异常原因
String reason() default "";
}
ResponseStatusExceptionResolver
把responsestatus注解的信息底层调用,response.sendError(statusCode, resolvedReason);
,tomcat发送的/error
DefaultHandlerExceptionResolver
Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
自定义异常解析器
- 创建类实现
HandlerExceptionResolver
接口 - 可以通过
@Order
来修改优先级 - 通过
response.sendError()
来将请求转给Controller
五、异常请求 /error
当上面的异常解析器没有办法处理异常,那么这个异常不能没人处理吧,系统重定向到默认请求/error
,该控制为BasicErrorController
,在自动配置中注册组件
BasicErrorController
上面已经解释了,在异常处理自动配置类里面会注入该Bean,该Bean也是一个Controller,所以当之前的代码出现任何异常,都会跳转到/error
请求,由该Controller负责处理
它相当于是最底层的一个异常处理,我们可以自定义异常处理,来处理,但是如果没有处理,则最终都会到这里
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // 会调用配置的server.error.path,没有则使用默认的/error
public class BasicErrorController extends AbstractErrorController {}
浏览器请求
通过浏览器请求,当出现异常时,会跳转到异常页面,那么也就是errorHtml()
errorHtml()
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 这里规定了响应的媒体类型,必须是text/html,也就是针对浏览器的响应
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);
// 存在自定义异常页面直接跳转
// 跳转到error,也就是自带的白页 error是在自动配置类里面设置的
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
resolveErrorView()
遍历视图解析器,默认只有1个视图解析器DefaultErrorViewResolver
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
// 遍历错误试图解析器 默认只有1个 也就是在ErrorMvcAutoConfiguration中注册的DefaultErrorViewResolver
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
DefaultErrorViewResolver
- 尝试解析具体状态的异常页面,例如:404.html,500.html等,获取到直接返回
- 尝试从静态路径中获取异常页面,获取到直接返回
- 尝试匹配4xx.html,5xx.html等异常页面
@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.html,5xx.html的页面
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
// 解析试图
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 错误视图名为 error/ + 状态码,也就对应着我们的错误页面是以状态码命名
String errorViewName = "error/" + viewName;
// 通过模板引擎判断是否存在异常页面
// 如果使用的400.html、500.html等直接返回
// 如果使用的是4xx.html、5xxx.html等返回null
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {// 不为空 代表具体的异常页面存在 直接返回
return new ModelAndView(errorViewName, model);
}
// 匹配不到具体的异常页面,则尝试从静态路径中查找
return resolveResource(errorViewName, model);
}
// 解析来源
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 遍历所有的静态路径
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
// 视图名拼接上.html 也就变成了最终的400.html
resource = resource.createRelative(viewName + ".html");
// 判断异常页面是否存在
if (resource.exists()) {
// 存在则创建试图返回
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
PostMan请求
当通过客户端请求,那么当出现异常的时候,返回的就是JSON串
error()
@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);
}