springboot情操陶冶-web配置(二)


承接前文springboot情操陶冶-web配置(一),在分析mvc的配置之前先了解下其默認的錯誤界面是如何顯示的

404界面

springboot有個比較有趣的配置server.error.whitelabel.enabled,可用來管理404界面的顯示方式,是簡單的顯示還是詳細的顯示。
指定為false的時候,則會簡簡單單的顯示視圖找不到的錯誤信息,如下
404_noHandler
指定為true的時候(默認配置),則會顯示前文樣例中的錯誤信息,如下
404_page

源碼層分析

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));
	}

對上述代碼的注釋作下簡單的解釋,幫助讀者們理順下思路

  1. 首先其會調用所有實現了ErrorViewResolver接口的視圖解析器去找尋相應的錯誤視圖,並支持通過Order接口進行排序。所以此處默認情況下會調用DefaultErrorViewResolver來獲取view,具體的如果獲取可見上文的講解
  2. 如果上述找到了,那么也就么事了,但是如果還沒找到,則會默認指定名為error的視圖。
  3. 那么如何去解析默認名為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所采用的ApplicationContextAnnotationConfigServletWebServerApplicationContext,其父類在刷新上下文過程中的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文件即可,具體可參照本文的講述。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM