承接前文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()) {
....
// 在custom方法中會調用RequestDispatcher對象進行后端路由重置到/error請求
if (custom(request, response, errorPage)) {
response.setErrorReported();
try {
response.finishResponse();
} catch (ClientAbortException e) {
// Ignore
} catch (IOException e) {
container.getLogger().warn("Exception Processing " + errorPage, e);
}
}
}
}
此源碼來源於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文件即可,具體可參照本文的講述。