承接前文springboot情操陶冶-web配置(一),在分析mvc的配置之前先瞭解下其預設的錯誤介面是如何顯示的
404介面
springboot有個比較有趣的配置server.error.whitelabel.enabled,可用來管理404介面的顯示方式,是簡單的顯示還是詳細的顯示。
指定為false的時候,則會簡簡單單的顯示視圖找不到的錯誤資訊,如下
指定為true的時候(預設配置),則會顯示前文範例中的錯誤資訊,如下
源碼層分析
springboot安排了ErrorMvcAutoConfiguration自動設定類來處理錯誤頁面的相關資訊,筆者分幾個步驟來進行分析
No.1 腦殼上的註解看一發
@Configuration@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, ResourceProperties.class })public class ErrorMvcAutoConfiguration {}
可以看出其是排在WebMvcAutoConfiguration配置類之前的,那麼為什麼需要排在前面呢?看注釋是說這樣才可以使error視圖有效,那怎麼實現的呢?筆者帶著問題繼續往下探索
No.2 DefaultErrorViewResolverConfiguration內部類-錯誤視圖解析器註冊
@Configuration static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, ResourceProperties resourceProperties) { this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; } // 註冊了DefaultErrorViewResolver解析器 @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean public DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); } }
DefaultErrorViewResolver這個預設的錯誤視圖解析器很有意思,裡麵包含了一些預設的處理,也分幾個小步驟來吧,這樣會顯得清晰
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); }
應該是對HTTP狀態代碼的映射處理,以4開頭的是用戶端錯誤,5開頭的為服務端錯誤
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); }
上述的模板載入器主要是讀取所有spring.factories中的org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider對應的屬性值,本質也就是模板的渲染器,比如我們常用的freemarker、velocity、jsp等等
@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 優先根據狀態代碼來尋找view靜態資源,比如404則會尋找error/404視圖 ModelAndView modelAndView = resolve(String.valueOf(status), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { // 上述不存在則再尋找error/4xx或者error/5xx視圖 modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; // 通過模板載入器尋找是否含有符合要求的視圖資源 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } return resolveResource(errorViewName, model); } // 預設尋找staticLocation指定路徑的資源,比如classpath:/static/error/404.html 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()) { // view類型為HtmlResourceView,直接將html資源輸出到response對象中 return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; }
通過上述的代碼注釋,基本可以得知錯誤視圖的尋找規則,所以使用者可以簡單的在static目錄下配置對應狀態代碼的頁面比如error/404.html或者error/500.html;當然也可以配置統一的頁面error/4xx.html或者error/5xx.html
那如果我們啥也不指定,那上述的錯誤提示資訊是如何展示的呢?
No.3 WhitelabelErrorViewConfiguration-白板錯誤視圖配置
// server.error.whitelabel.enabled開關,預設是開啟的 @Configuration @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { // 熟悉的列印資訊 private final SpelView defaultErrorView = new 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>"); // 建立了名為error的視圖對象 @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // 與上面的View對象搭配使用 @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } }
上述就是我們開頭可見的錯誤資訊的處理處,詳細的使用者可自行查閱代碼
No.4 建構函式瞭解
public ErrorMvcAutoConfiguration(ServerProperties serverProperties, ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) { this.serverProperties = serverProperties; this.errorViewResolvers = errorViewResolversProvider.getIfAvailable(); }
上述的errorViewResolverProvider便會載入第二步驟的DefaultViewResolver,當然使用者也可以自訂去實現ErrorViewResolver介面。這些錯誤的視圖解析器將會在下一步驟的controller層被調用
No.5 error控制器註冊
@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes( this.serverProperties.getError().isIncludeException()); } // 建立BasicErrorController控制器用於響應server.error.path指定的路徑,預設為/error @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); }
此處的BasicErrorController對象則會預設響應/error的請求,其內部寫了一個返回html頁面的回應程式法
@RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); // 狀態代碼設定 response.setStatus(status.value()); // 調用errorViewResolvers集合去擷取對應的錯誤視圖 ModelAndView modelAndView = resolveErrorView(request, response, status, model); // 如果沒指定相應的視圖,則會採用預設的名為error的視圖 return (modelAndView != null ? modelAndView : new ModelAndView("error", model)); }
對上述代碼的注釋作下簡單的解釋,協助讀者們理順下思路
- 首先其會調用所有實現了ErrorViewResolver介面的視圖解析器去找尋相應的錯誤視圖,並支援通過Order介面進行排序。所以此處預設情況下會調用DefaultErrorViewResolver來擷取view,具體的如果擷取可見上文的講解
- 如果上述找到了,那麼也就麼事了,但是如果還沒找到,則會預設指定名為error的視圖。
- 那麼如何去解析預設名為error的視圖呢?答案在DispatcherServlet在最終確定渲染視圖的時候,會統一調用所有實現了ViewResolver介面的視圖解析器去擷取視圖對象,那麼第三步驟中的BeanNameViewResolver對象便會找尋到對應的SpelView視圖,由其來進行相應的渲染
在此處筆者回答下開頭的問題,為什麼ErrorMvcAutoConfiguration需要放在DispatcherServletAutoConfiguration之前,其實最主要的是後者並沒有去註冊BeanViewResolver,此處上了一份保險,好讓能正確的找到SpelView對象
error請求問題
經過上文的分析,我們知道了BasicErrorController用來處理訪問方式為GET [/error]的請求並處理得到相應的錯誤視圖,那麼最重要的問題來了,到底怎麼在出現資源找不到的時候去路由至此路徑上呢?筆者繼續帶著這個問題去探索
No.1 ErrorPageCustomizer-錯誤頁面配置
@Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties); } private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; protected ErrorPageCustomizer(ServerProperties properties) { this.properties = properties; } @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { // 預設路徑為/error ErrorPage errorPage = new ErrorPage( this.properties.getServlet().getServletPrefix() + this.properties.getError().getPath()); // 註冊 errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { return 0; } }
上述的errorPage貌似展示了一點資訊,可能是會去訪問/error的源頭,那麼ErrorPageCustomizer#registerErrorPages()是如何被調用的呢?繼續往下
No.2 ServletWebAutoConfiguration引入的時候還註冊了一個BeanPostProcessor
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { if (this.beanFactory == null) { return; } registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor", WebServerFactoryCustomizerBeanPostProcessor.class); // 就是這個 registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class); }
我們直接去關注其主要的方法
// 註冊了相應的錯誤介面 private void postProcessBeforeInitialization(ErrorPageRegistry registry) { for (ErrorPageRegistrar registrar : getRegistrars()) { registrar.registerErrorPages(registry); } } private Collection<ErrorPageRegistrar> getRegistrars() { if (this.registrars == null) { // Look up does not include the parent context this.registrars = new ArrayList<>(this.beanFactory .getBeansOfType(ErrorPageRegistrar.class, false, false).values()); this.registrars.sort(AnnotationAwareOrderComparator.INSTANCE); this.registrars = Collections.unmodifiableList(this.registrars); } return this.registrars; }
至於為什麼在該類中去註冊這個processor去執行註冊錯誤頁面,看來這個路徑的轉寄應該與web容器有關。其實追蹤源頭其實將錯誤介面註冊到了相應的web容器中(Tomcat),具體的讀者可自行去分析。
No.4 web容器載入(插曲,順帶提一下)
我們都知道springboot對環境為Servlet所採用的ApplicationContext為AnnotationConfigServletWebServerApplicationContext,其父類在重新整理上下文過程中的onRefresh()方法便去啟動了web容器
@Override protected void onRefresh() { super.onRefresh(); try { // 建立web伺服器 createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } } private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { // 預設為TomcatServletWebServerFactory ServletWebServerFactory factory = getWebServerFactory(); // 初始化servlet/filter等 this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); }
上述的代碼主要會在ServletContext上註冊Filters和Servlets集合并且註冊ErrorPages,限於代碼過長,讀者可自行分析。而具體的去啟動web容器則是在finishRefresh()方法中
@Override protected void finishRefresh() { super.finishRefresh(); // 啟動 WebServer webServer = startWebServer(); if (webServer != null) { publishEvent(new ServletWebServerInitializedEvent(webServer, this)); } }
No.5 StandardHostValve-錯誤介面應用
private void status(Request request, Response response) { int statusCode = response.getStatus(); .... // 優先尋找404對應的ErrorPage ErrorPage errorPage = context.findErrorPage(statusCode); if (errorPage == null) { // 0-預設的ErrorPage,此處便是上文註冊的 errorPage = context.findErrorPage(0); } if (errorPage != null && response.isErrorReportRequired()) { .... } }
此源碼來源於tomcat,這讓筆者想起了針對狀態代碼的page配置
<!--404 error page specified based on Tomcat--> <error-page> <error-code>404</error-code> <location>/404.html</location> </error-page>
小結
本文的內容較多,需要耐心閱讀,讀者只需要瞭解View視圖的解析載入便可通讀全文,如果想要自訂狀態代碼視圖則直接在classpath:/static/error目錄下建立相應的狀態代碼HTML檔案即可,具體可參照本文的講述。