SpringBoot內置了Servlet容器,這樣項目的發布、部署就不需要額外的Servlet容器,直接啟動jar包即可。SpringBoot官方文檔上有一個小章節內置servlet容器支持用于說明內置Servlet的相關問題。
在SpringBoot源碼分析之SpringBoot的啟動過程文章中我們了解到如果是Web程序,那么會構造AnnotationConfigEmbeddedWebApplicationContext類型的Spring容器,在SpringBoot源碼分析之Spring容器的refresh過程文章中我們知道AnnotationConfigEmbeddedWebApplicationContext類型的Spring容器在refresh的過程中會在onRefresh方法中創建內置的Servlet容器。
接下來,我們分析一下內置的Servlet容器相關的知識點。
內置Servlet容器相關的接口和類
SpringBoot對內置的Servlet容器做了一層封裝:
public interface EmbeddedServletContainer {
// 啟動內置的Servlet容器,如果容器已經啟動,則不影響
void start() throws EmbeddedServletContainerException;
// 關閉內置的Servlet容器,如果容器已經關系,則不影響
void stop() throws EmbeddedServletContainerException;
// 內置的Servlet容器監聽的端口
int getPort();
}
它目前有3個實現類,分別是JettyEmbeddedServletContainer、TomcatEmbeddedServletContainer和UndertowEmbeddedServletContainer,分別對應Jetty、Tomcat和Undertow這3個Servlet容器。
EmbeddedServletContainerFactory接口是一個工廠接口,用于生產EmbeddedServletContainer:
public interface EmbeddedServletContainerFactory {
// 獲得一個已經配置好的內置Servlet容器,但是這個容器還沒有監聽端口。需要手動調用內置Servlet容器的start方法監聽端口
// 參數是一群ServletContextInitializer,Servlet容器啟動的時候會遍歷這些ServletContextInitializer,并調用onStartup方法
EmbeddedServletContainer getEmbeddedServletContainer(
ServletContextInitializer... initializers);
}
ServletContextInitializer表示Servlet初始化器,用于設置ServletContext中的一些配置,在使用EmbeddedServletContainerFactory接口的getEmbeddedServletContainer方法獲取Servlet內置容器并且容器啟動的時候調用onStartup方法:
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
EmbeddedServletContainerFactory是在EmbeddedServletContainerAutoConfiguration這個自動化配置類中被注冊到Spring容器中的(前期是Spring容器中不存在EmbeddedServletContainerFactory類型的bean,可以自己定義EmbeddedServletContainerFactory類型的bean):
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication // 在Web環境下才會起作用
@Import(BeanPostProcessorsRegistrar.class) // 會Import一個內部類BeanPostProcessorsRegistrar
public class EmbeddedServletContainerAutoConfiguration {
@Configuration
// Tomcat類和Servlet類必須在classloader中存在
@ConditionalOnClass({ Servlet.class, Tomcat.class })
// 當前Spring容器中不存在EmbeddedServletContainerFactory類型的實例
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
// 上述條件注解成立的話就會構造TomcatEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
return new TomcatEmbeddedServletContainerFactory();
}
}
@Configuration
// Server類、Servlet類、Loader類以及WebAppContext類必須在classloader中存在
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
WebAppContext.class })
// 當前Spring容器中不存在EmbeddedServletContainerFactory類型的實例
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
@Bean
public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
// 上述條件注解成立的話就會構造JettyEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
return new JettyEmbeddedServletContainerFactory();
}
}
@Configuration
// Undertow類、Servlet類、以及SslClientAuthMode類必須在classloader中存在
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
// 當前Spring容器中不存在EmbeddedServletContainerFactory類型的實例
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedUndertow {
@Bean
public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
// 上述條件注解成立的話就會構造JettyEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
return new UndertowEmbeddedServletContainerFactory();
}
}
// 在EmbeddedServletContainerAutoConfiguration自動化配置類中被導入,實現了BeanFactoryAware接口(BeanFactory會被自動注入進來)和ImportBeanDefinitionRegistrar接口(會被ConfigurationClassBeanDefinitionReader解析并注冊到Spring容器中)
public static class EmbeddedServletContainerCustomizerBeanPostProcessorRegistrar
implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
// 如果Spring容器中不存在EmbeddedServletContainerCustomizerBeanPostProcessor類型的bean
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(
EmbeddedServletContainerCustomizerBeanPostProcessor.class, true,
false))) {
// 注冊一個EmbeddedServletContainerCustomizerBeanPostProcessor
registry.registerBeanDefinition(
"embeddedServletContainerCustomizerBeanPostProcessor",
new RootBeanDefinition(
EmbeddedServletContainerCustomizerBeanPostProcessor.class));
}
}
}
}
EmbeddedServletContainerCustomizerBeanPostProcessor是一個BeanPostProcessor,它在postProcessBeforeInitialization過程中去尋找Spring容器中EmbeddedServletContainerCustomizer類型的bean,并依次調用EmbeddedServletContainerCustomizer接口的customize方法做一些定制化:
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// 在Spring容器中尋找ConfigurableEmbeddedServletContainer類型的bean,SpringBoot內部的3種內置Servlet容器工廠都實現了這個接口,該接口的作用就是進行Servlet容器的配置
// 比如添加Servlet初始化器addInitializers、添加錯誤頁addErrorPages、設置session超時時間setSessionTimeout、設置端口setPort等等
if (bean instanceof ConfigurableEmbeddedServletContainer) {
postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
}
return bean;
}
private void postProcessBeforeInitialization(
ConfigurableEmbeddedServletContainer bean) {
for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
// 遍歷獲取的每個定制化器,并調用customize方法進行一些定制
customizer.customize(bean);
}
}
private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
if (this.customizers == null) {
this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
// 找出Spring容器中EmbeddedServletContainerCustomizer類型的bean
this.applicationContext
.getBeansOfType(EmbeddedServletContainerCustomizer.class,
false, false)
.values());
// 定制化器做排序
Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
// 設置定制化器到屬性中
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
SpringBoot內置了一些EmbeddedServletContainerCustomizer,比如ErrorPageCustomizer、ServerProperties、TomcatWebSocketContainerCustomizer等。
定制器比如ServerProperties表示服務端的一些配置,以server為前綴,比如有server.port、server.contextPath、server.displayName等,它同時也實現了EmbeddedServletContainerCustomizer接口,其中customize方法的一部分代碼如下:
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
// 3種ServletContainerFactory都實現了ConfigurableEmbeddedServletContainer接口,所以下面的這些設置相當于對ServletContainerFactory進行設置
// 如果配置了端口信息
if (getPort() != null) {
container.setPort(getPort());
}
...
// 如果配置了displayName
if (getDisplayName() != null) {
container.setDisplayName(getDisplayName());
}
// 如果配置了server.session.timeout,session超時時間。注意:這里的Session指的是ServerProperties的內部靜態類Session
if (getSession().getTimeout() != null) {
container.setSessionTimeout(getSession().getTimeout());
}
...
// 如果使用的是Tomcat內置Servlet容器,設置對應的Tomcat配置
if (container instanceof TomcatEmbeddedServletContainerFactory) {
getTomcat().customizeTomcat(this,
(TomcatEmbeddedServletContainerFactory) container);
}
// 如果使用的是Jetty內置Servlet容器,設置對應的Tomcat配置
if (container instanceof JettyEmbeddedServletContainerFactory) {
getJetty().customizeJetty(this,
(JettyEmbeddedServletContainerFactory) container);
}
// 如果使用的是Undertow內置Servlet容器,設置對應的Tomcat配置
if (container instanceof UndertowEmbeddedServletContainerFactory) {
getUndertow().customizeUndertow(this,
(UndertowEmbeddedServletContainerFactory) container);
}
// 添加SessionConfiguringInitializer這個Servlet初始化器
// SessionConfiguringInitializer初始化器的作用是基于ServerProperties的內部靜態類Session設置Servlet中session和cookie的配置
container.addInitializers(new SessionConfiguringInitializer(this.session));
// 添加InitParameterConfiguringServletContextInitializer初始化器
// InitParameterConfiguringServletContextInitializer初始化器的作用是基于ServerProperties的contextParameters配置設置到ServletContext的init param中
container.addInitializers(new InitParameterConfiguringServletContextInitializer(
getContextParameters()));
}
ErrorPageCustomizer在ErrorMvcAutoConfiguration自動化配置里定義,是個內部靜態類:
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.properties);
}
private static class ErrorPageCustomizer
implements EmbeddedServletContainerCustomizer, Ordered {
private final ServerProperties properties;
protected ErrorPageCustomizer(ServerProperties properties) {
this.properties = properties;
}
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
// 添加錯誤頁ErrorPage,這個ErrorPage對應的路徑是 /error
// 可以通過配置修改 ${servletPath} + ${error.path}
container.addErrorPages(new ErrorPage(this.properties.getServletPrefix()
+ this.properties.getError().getPath()));
}
@Override
public int getOrder() {
return 0;
}
}
DispatcherServlet的構造
DispatcherServlet是SpringMVC中的核心分發器。它是在DispatcherServletAutoConfiguration這個自動化配置類里構造的(如果Spring容器內沒有自定義的DispatcherServlet),并且還會被加到Servlet容器中(通過ServletRegistrationBean完成)。
DispatcherServletAutoConfiguration這個自動化配置類存在2個條件注解@ConditionalOnWebApplication和@ConditionalOnClass(DispatcherServlet.class)都滿足條件,所以會被構造(存在@AutoConfigureAfter(EmbeddedServletContainerAutoConfiguration.class)注解,會在EmbeddedServletContainerAutoConfiguration自動化配置類構造后構造):
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(EmbeddedServletContainerAutoConfiguration.class)
public class DispatcherServletAutoConfiguration ...
DispatcherServletAutoConfiguration有個內部類DispatcherServletConfiguration,它會構造DispatcherServlet(使用了條件類DefaultDispatcherServletCondition,如果Spring容器已經存在自定義的DispatcherServlet類型的bean,該類就不會被構造,會直接使用自定義的DispatcherServlet):
@Configuration
// 條件類DefaultDispatcherServletCondition,是EmbeddedServletContainerAutoConfiguration的內部類
// DefaultDispatcherServletCondition條件類會去Spring容器中找DispatcherServlet類型的實例,如果找到了不會構造DispatcherServletConfiguration,否則就是構造DispatcherServletConfiguration,該類內部會構造DispatcherServlet
// 所以如果我們要自定義DispatcherServlet的話只需要自定義DispatcherServlet即可,這樣DispatcherServletConfiguration內部就不會構造DispatcherServlet
@Conditional(DefaultDispatcherServletCondition.class)
// Servlet3.0開始才有的類,支持以編碼的形式注冊Servlet
@ConditionalOnClass(ServletRegistration.class)
// spring.mvc 為前綴的配置
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {
@Autowired
private ServerProperties server;
@Autowired
private WebMvcProperties webMvcProperties;
@Autowired(required = false)
private MultipartConfigElement multipartConfig;
// Spring容器注冊DispatcherServlet
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
// 直接構造DispatcherServlet,并設置WebMvcProperties中的一些配置
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(
this.webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(
this.webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(
this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
return dispatcherServlet;
}
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
public ServletRegistrationBean dispatcherServletRegistration() {
// 直接使用DispatcherServlet和server配置中的servletPath路徑構造ServletRegistrationBean
// ServletRegistrationBean實現了ServletContextInitializer接口,在onStartup方法中對應的Servlet注冊到Servlet容器中
// 所以這里DispatcherServlet會被注冊到Servlet容器中,對應的urlMapping為server.servletPath配置
ServletRegistrationBean registration = new ServletRegistrationBean(
dispatcherServlet(), this.server.getServletMapping());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.multipartConfig);
}
return registration;
}
@Bean // 構造文件上傳相關的bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
return resolver;
}
}
ServletRegistrationBean實現了ServletContextInitializer接口,是個Servlet初始化器,onStartup方法代碼:
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
Assert.notNull(this.servlet, "Servlet must not be null");
String name = getServletName();
if (!isEnabled()) {
logger.info("Servlet " + name + " was not registered (disabled)");
return;
}
logger.info("Mapping servlet: '" + name + "' to " + this.urlMappings);
// 把servlet添加到Servlet容器中,Servlet容器啟動的時候會加載這個Servlet
Dynamic added = servletContext.addServlet(name, this.servlet);
if (added == null) {
logger.info("Servlet " + name + " was not registered "
+ "(possibly already registered?)");
return;
}
// 進行Servlet的一些配置,比如urlMapping,loadOnStartup等
configure(added);
}
類似ServletRegistrationBean的還有ServletListenerRegistrationBean和FilterRegistrationBean,它們都是Servlet初始化器,分別都是在Servlet容器中添加Listener和Filter。
1個小漏洞:如果定義了一個名字為dispatcherServlet的bean,但是它不是DispatcherServlet類型,那么DispatcherServlet就不會被構造,@RestController和@Controller注解的控制器就沒辦法生效:
@Bean(name = "dispatcherServlet")
public Object test() {
return new Object();
}
內置Servlet容器的創建和啟動
web程序對應的Spring容器是AnnotationConfigEmbeddedWebApplicationContext,繼承自EmbeddedWebApplicationContext。在onRefresh方法中會去創建內置Servlet容器:
@Override
protected void onRefresh() {
super.onRefresh();
try {
// 創建內置Servlet容器
createEmbeddedServletContainer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start embedded container",
ex);
}
}
private void createEmbeddedServletContainer() {
EmbeddedServletContainer localContainer = this.embeddedServletContainer;
ServletContext localServletContext = getServletContext();
// 內置Servlet容器和ServletContext都還沒初始化的時候執行
if (localContainer == null && localServletContext == null) {
// 從Spring容器中獲取EmbeddedServletContainerFactory,如果EmbeddedServletContainerFactory不存在或者有多個的話會拋出異常中止程序
EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
// 獲取Servlet初始化器并創建Servlet容器,依次調用Servlet初始化器中的onStartup方法
this.embeddedServletContainer = containerFactory
.getEmbeddedServletContainer(getSelfInitializer());
}
// 內置Servlet容器已經初始化但是ServletContext還沒初始化的時候執行
else if (localServletContext != null) {
try {
// 對已經存在的Servlet
容器依次調用Servlet初始化器中的onStartup方法
getSelfInitializer().onStartup(localServletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context",
ex);
}
}
initPropertySources();
}
getSelfInitializer方法獲得的Servlet初始化器內部會去構造一個ServletContextInitializerBeans(Servlet初始化器的集合),ServletContextInitializerBeans構造的時候會去Spring容器中查找ServletContextInitializer類型的bean,其中ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean會被找出(如果有定義),這3種ServletContextInitializer會在onStartup方法中將Servlet、Filter、Listener添加到Servlet容器中(如果我們只定義了Servlet、Filter或者Listener,ServletContextInitializerBeans內部會調用addAdaptableBeans方法把它們包裝成RegistrationBean):
// selfInitialize方法內部調用的getServletContextInitializerBeans方法獲得ServletContextInitializerBeans
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
return new ServletContextInitializerBeans(getBeanFactory());
}
private void addServletContextInitializerBean(String beanName,
ServletContextInitializer initializer, ListableBeanFactory beanFactory) {
if (initializer instanceof ServletRegistrationBean) {
Servlet source = ((ServletRegistrationBean) initializer).getServlet();
addServletContextInitializerBean(Servlet.class, beanName, initializer,
beanFactory, source);
}
else if (initializer instanceof FilterRegistrationBean) {
Filter source = ((FilterRegistrationBean) initializer).getFilter();
addServletContextInitializerBean(Filter.class, beanName, initializer,
beanFactory, source);
}
else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
String source = ((DelegatingFilterProxyRegistrationBean) initializer)
.getTargetBeanName();
addServletContextInitializerBean(Filter.class, beanName, initializer,
beanFactory, source);
}
else if (initializer instanceof ServletListenerRegistrationBean) {
EventListener source = ((ServletListenerRegistrationBean<?>) initializer)
.getListener();
addServletContextInitializerBean(EventListener.class, beanName, initializer,
beanFactory, source);
}
else {
addServletContextInitializerBean(ServletContextInitializer.class, beanName,
initializer, beanFactory, null);
}
}
Servlet容器創建完畢之后在finishRefresh方法中會去啟動:
@Override
protected void finishRefresh() {
super.finishRefresh();
// 調用startEmbeddedServletContainer方法
EmbeddedServletContainer localContainer = startEmbeddedServletContainer();
if (localContainer != null) {
// 發布EmbeddedServletContainerInitializedEvent事件
publishEvent(
new EmbeddedServletContainerInitializedEvent(this, localContainer));
}
}
private EmbeddedServletContainer startEmbeddedServletContainer() {
// 先得到在onRefresh方法中構造的Servlet容器embeddedServletContainer
EmbeddedServletContainer localContainer = this.embeddedServletContainer;
if (localContainer != null) {
// 啟動
localContainer.start();
}
return localContainer;
}
自定義Servlet、Filter、Listener
SpringBoot默認只會添加一個Servlet,也就是DispatcherServlet,如果我們想添加自定義的Servlet或者是Filter還是Listener,有以下幾種方法。
1.在Spring容器中聲明ServletRegistrationBean、FilterRegistrationBean或者ServletListenerRegistrationBean。原理在DispatcherServlet的構造章節中已經說明
@Bean
public ServletRegistrationBean customServlet() {
return new ServletRegistrationBean(new CustomServlet(), "/custom");
}
private static class CustomServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("receive by custom servlet");
}
}
2.@ServletComponentScan注解和@WebServlet、@WebFilter以及@WebListener注解配合使用。@ServletComponentScan注解啟用ImportServletComponentScanRegistrar類,是個ImportBeanDefinitionRegistrar接口的實現類,會被Spring容器所解析。ServletComponentScanRegistrar內部會解析@ServletComponentScan注解,然后會在Spring容器中注冊ServletComponentRegisteringPostProcessor,是個BeanFactoryPostProcessor,會去解析掃描出來的類是不是有@WebServlet、@WebListener、@WebFilter這3種注解,有的話把這3種類型的類轉換成ServletRegistrationBean、FilterRegistrationBean或者ServletListenerRegistrationBean,然后讓Spring容器去解析:
@SpringBootApplication
@ServletComponentScan
public class EmbeddedServletApplication { ... }
@WebServlet(urlPatterns = "/simple")
public class SimpleServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("receive by SimpleServlet");
}
}
3.在Spring容器中聲明Servlet、Filter或者Listener。因為在ServletContextInitializerBeans內部會去調用addAdaptableBeans方法把它們包裝成ServletRegistrationBean:
@Bean(name = "dispatcherServlet")
public DispatcherServlet myDispatcherServlet() {
return new DispatcherServlet();
}
Whitelabel Error Page原理
為什么SpringBoot的程序里Controller發生了錯誤,我們沒有進行異常的捕捉,會跳轉到Whitelabel Error Page頁面,這是如何實現的?
SpringBoot內部提供了一個ErrorController叫做BasicErrorController,對應的@RequestMapping地址為 "server.error.path" 配置 或者 "error.path" 配置,這2個配置沒配的話默認是/error,之前分析過ErrorPageCustomizer這個定制化器會把ErrorPage添加到Servlet容器中(這個ErrorPage的path就是上面說的那2個配置),這樣Servlet容器發生錯誤的時候就會訪問ErrorPage配置的path,所以程序發生異常且沒有被catch的話,就會走Servlet容器配置的ErrorPage。下面這段代碼是BasicErrorController對應的處理請求方法:
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
// 設置響應碼
response.setStatus(getStatus(request).value());
// 設置一些信息,比如timestamp、statusCode、錯誤message等
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
// 返回error視圖
return new ModelAndView("error", model);
}
這里名字為error視圖會被BeanNameViewResolver這個視圖解析器解析,它會去Spring容器中找出name為error的View,error這個bean在ErrorMvcAutoConfiguration自動化配置類里定義,它返回了一個SpelView視圖,也就是剛才見到的Whitelabel Error Page(error.whitelabel.enabled配置需要是true,否則WhitelabelErrorViewConfiguration自動化配置類不會被注冊):
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
// Whitelabel Error Page
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>");
@Bean(name = "error") // bean的名字是error
@ConditionalOnMissingBean(name = "error") // 名字為error的bean不存在才會構造
public View defaultErrorView() {
return this.defaultErrorView;
}
@Bean
@ConditionalOnMissingBean(BeanNameViewResolver.class)
public BeanNameViewResolver beanNameViewResolver() {
// BeanNameViewResolver會去Spring容器找對應bean的視圖
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
如果自定義了error頁面,比如使用freemarker模板的話存在/templates/error.ftl頁面,使用thymeleaf模板的話存在/templates/error.html頁面。那么Whitelabel Error Page就不會生效了,而是會跳到這些error頁面。這又是如何實現的呢?
這是因為ErrorMvcAutoConfiguration自動化配置類里的內部類 WhitelabelErrorViewConfiguration自動化配置類里有個條件類ErrorTemplateMissingCondition,它的getMatchOutcome方法:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 從spring.factories文件中找出key為TemplateAvailabilityProvider為類,TemplateAvailabilityProvider用來查詢視圖是否可用
List<TemplateAvailabilityProvider> availabilityProviders = SpringFactoriesLoader
.loadFactories(TemplateAvailabilityProvider.class,
context.getClassLoader());
// 遍歷各個TemplateAvailabilityProvider
for (TemplateAvailabilityProvider availabilityProvider : availabilityProviders)
// 如果error視圖可用
if (availabilityProvider.isTemplateAvailable("error",
context.getEnvironment(), context.getClassLoader(),
context.getResourceLoader())) {
// 條件不生效。WhitelabelErrorViewConfiguration不會被構造
return ConditionOutcome.noMatch("Template from "
+ availabilityProvider + " found for error view");
}
}
// 條件生效。WhitelabelErrorViewConfiguration被構造
return ConditionOutcome.match("No error template view detected");
}
比如FreeMarkerTemplateAvailabilityProvider這個TemplateAvailabilityProvider的邏輯如下:
public class FreeMarkerTemplateAvailabilityProvider
implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
// 判斷是否存在freemarker包中的Configuration類,存在的話才會繼續
if (ClassUtils.isPresent("freemarker.template.Configuration", classLoader)) {
// 構造屬性解析器
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment,
"spring.freemarker.");
// 設置一些配置
String loaderPath = resolver.getProperty("template-loader-path",
FreeMarkerProperties.DEFAULT_TEMPLATE_LOADER_PATH);
String prefix = resolver.getProperty("prefix",
FreeMarkerProperties.DEFAULT_PREFIX);
String suffix = resolver.getProperty("suffix",
FreeMarkerProperties.DEFAULT_SUFFIX);
// 查找對應的資源文件是否存在
return resourceLoader.getResource(loaderPath + prefix + view + suffix)
.exists();
}
return false;
}
}
所以BeanNameViewResolver不會被構造,Whitelabel Error Page也不會構造,而是直接去找自定義的error視圖。