一、前言
在基于SpringBoot开发的过程中,经常遇见“Whitelabel Error Page”的错误页面,比如:
404错误:
500错误:
在实际开发中,不止上面的这两种情况,还有很多其他情况,这里不再一一列举,这些错误页面是如何产生的,是如果工作的呢?其实这就是SpringBoot提供的默认的异常处理机制。我们这一节就来学习SpringBoot默认的异常处理机制。
二、主要接口或类
在SpringBoot实现的异常处理逻辑中,有几个常用的接口或类,我们这里先简单了解一下:
- ErrorMvcAutoConfiguration 配置类,初始化异常处理的配置类
- ErrorViewResolver 异常视图解析器接口,默认的实现类是DefaultErrorViewResolver。
- ErrorAttributes 获取异常属性的接口,默认实现类是DefaultErrorAttributes。
- ErrorController 异常处理器接口类,标记类,提供了一个抽象实现类AbstractErrorController,而该抽象类又有一个实现类BasicErrorController,该类就是SpringBoot提供的默认异常处理器。
SpringBoot实现异常处理的逻辑,就是通过ErrorMvcAutoConfiguration 实现初始化,然后交由异常处理器ErrorController 实现,而ErrorController 处理器则借助ErrorAttributes获取异常中的相关信息,而借助ErrorViewResolver 实现错误视图的解析。
三、初始化
在前面《基于@ControllerAdvice注解实现全局异常处理用法和原理的探究》一篇内容中,我们使用到了@ControllerAdvice注解实现全局异常处理,在很多博文中,都把这个归类到了SpringBoot处理异常的方式,其实,该方法不仅可以在SpringBoot项目中可以使用,而且可以在只使用了SpringMVC(和Spring)的地方使用,只不过初始化方式和基于SpringBoot实现的有所差异而已。
ErrorViewResolver 、ErrorAttributes 和ErrorController 三个的实现类的初始化过程都是在ErrorMvcAutoConfiguration 配置类中完成的。
首先,ErrorAttributes的实现类DefaultErrorAttributes、ErrorController 的实现类
BasicErrorController都通过@Bean注解的方式实现,如下所示:
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@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()));
}
}
然后ErrorViewResolver 实现类DefaultErrorViewResolver 是在内部配置类DefaultErrorViewResolverConfiguration 中进行注入的,也是通过@Bean注解实现的,如下所示:
//ErrorMvcAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
}
除了上述配置的视图解析器DefaultErrorViewResolver外,还配置了默认视图,这个也是我们在开篇时候,提到的异常页面的生成的地方,配置如下:
@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;
}
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
在配置类WhitelabelErrorViewConfiguration 中使用了StaticView视图,该视图就是SpringBoot定义的默认视图,实现逻辑如下:
》》》看着下面那些HTML标签内容是不是有种很熟悉的感觉?这就是开篇出现错误页面的视图。
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";
}
}
至此,在SpringBoot异常处理过程中需要的实例对象,都已经完成了初始化的过程。
四、触发时机
在前面已经完成了初始化的过程,那么当发生异常的时候,是如何触发异常的处理呢?其实这里的逻辑和上一篇《基于@ControllerAdvice注解实现全局异常处理用法和原理的探究》中的逻辑类似,我们这里简单梳理一下。
首先,我们还是从DispatcherServlet的doDispatch()方法开始进行分析,然后无论是否发生异常,都会调用processDispatchResult()方法(区别在于dispatchException变量是否为空),如果dispatchException不为空(发生了异常);
在processDispatchResult()方法中就会继续调用processHandlerException()方法进行异常处理,在processHandlerException()方法中,会迭代变量handlerExceptionResolvers中的异常解析器,然后会调用其中HandlerExceptionResolverComposite组合类型的解析器的resolveException()方法进行处理;
在上一篇中执行HandlerExceptionResolverComposite实例的resolveException()方法的时候,因为我们通过@ControllerAdvice注解定义了全局的异常处理器类,所以调用的是ExceptionHandlerExceptionResolver实例对象的resolveException()方法进行处理,如果我们没有自定义异常解析器类,这个时候就会调用DefaultHandlerExceptionResolver实例对象的resolveException()方法,而resolveException()方法是定义在抽象类AbstractHandlerExceptionResolver中的方法,而ExceptionHandlerExceptionResolver和DefaultHandlerExceptionResolver都继承了该抽象类,所以执行的resolveException()方法还是抽象类中的方法,而在resolveException()方法中调用的doResolveException()方法就不一样了,这里个方法是在子类中实现的,所以这里将会调用DefaultHandlerExceptionResolver类中的实现方法,逻辑如下:
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return handleHttpRequestMethodNotSupported(
(HttpRequestMethodNotSupportedException) ex, request, response, handler);
}
// 省略……
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
// 省略……
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
}
}
return null;
}
在上述doResolveException()方法中,根据不同类型的异常,调用对应的handlerXXX()方法进行处理异常,并返回ModelAndView对象,我们这里以NoHandlerFoundException异常为例(没有对应处理器,一般会抛出状态为404的异常),分析handleNoHandlerFoundException()方法如何生成对应的视图,实现如下:
protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
pageNotFoundLogger.warn(ex.getMessage());
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return new ModelAndView();
}
通过handleNoHandlerFoundException()方法我们知道返回了一个空的ModelAndView对象,即在processHandlerException()方法中,经过HandlerExceptionResolverComposite组合类型的解析器的resolveException()方法进行处理,返回了一个空的ModelAndView对象,但是虽然该对象是空对象,但是并不是空(exMv != null),所以则是就跳出了循环处理过程,并经过后续判断,直接返回了null。
//DispatcherServlet.java
@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);
// HandlerExceptionResolverComposite组合类型的解析器的resolveException()方法进行处理
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) {
//当发生NoHandlerFoundException异常时,exMV != null 但是exMv.isEmpty()==true,所以该方法直接返回了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;
}
经过上述方法处理后,又回到了processDispatchResult()方法,进行后续处理,又回到了doDispatch()方法继续处理,最后到了processRequest()方法,重新出发了一次新的request请求,如下所示:
//FrameworkServlet.java
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//省略……
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
//触发ServletRequestHandledEvent事件,重新开始一次新的请求
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
这次新的请求,因为是没有对应的处理器,所以触发的ServletRequestHandledEvent事件,statusCode状态码为404,内容如下:
因为statusCode状态码为404,经过Servlet容器转发,这次请求就会请求”/error”错误页面,SpringMVC根据该url就会找到对应的处理器BasicErrorController,进行处理,实现如下:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@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);
}
}
其实,到这个方法,就说明我们已经找到了SpringBoot默认的处理错误的处理器BasicErrorController,这个就是我们非常熟悉的用法了。不过细心一看,好像前面提到的StaticView 视图没有提到了,其实这就是SpringMVC的处理机制了,我们再深入了解一下。
其实,新的请求还是通过DispatcherServlet的doDispatch()进行处理,然后进入
processDispatchResult()方法继续处理,这次exception 就等于null了,所以就不会再走异常处理逻辑了,直接进行视图处理逻辑,即执行render()方法。
//DispatcherServlet.java
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
//下面这部分就是第一次请求时,处理异常的逻辑,但是因为返回的mv是空的,所以触发了第二次请求,这个时候exception 就等于null了,所以就不会再走异常处理逻辑了
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);
}
}
//视图处理逻辑
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);
}
}
render()方法处理视图,其中又通过resolveViewName()方法进行视图解析,逻辑如下:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
//省略……
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
//省略……
}
在resolveViewName()方法中,根据viewName实现了视图的解析,最终得到了对应的View实例,这里其实就是前面提到的StaticView,实现如下:
自此,我们就把SpringBoot默认的异常处理机制中,默认错误页面是如何使用的分析完成了。后续我们将继续深入学习关于异常处理的其他内容。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68731.html