該系列文檔是本人在學習 Spring MVC 的源碼過程當中總結下來的,可能對讀者不太友好,請結合個人源碼註釋 Spring MVC 源碼分析 GitHub 地址 進行閱讀html
Spring 版本:5.2.4.RELEASEjava
在開始 Spring MVC
的分析以前,先來聊一聊 Java 初學者接觸的最多的 Java Web 基礎。還記得個人第一個 Web 工程是由 Servlet、Velocity 和 Filter 來完成的,那時幾乎全部人都是根據 Servlet、JSP 和 Filter 來編寫本身的第一個 Hello World 工程。那時,還離不開 web.xml 配置文件,須要對 Servlet 和 Filter 進行配置,相對來講比較繁瑣。隨着 Spring
體系的快速發展,配置逐漸演變成了 Java Configuration 和 XML 配置兩種方式的共存。現現在,Spring Boot
和 Spring Cloud
在許多中大型企業中被普及,Java Configuration 成爲了主流,XML 配置的方式也逐漸「消失」在咱們的視野裏面。不知道如今的小夥伴是否還記得那個 web.xml 文件,這中間都發生過什麼變化,其中的 Servlet 和 Filter 配置項被什麼取代了?git
Servlet:Java Servlet 爲 Web 開發人員提供了一種簡單,一致的機制,以擴展 Web 服務器的功能並訪問現有的業務系統。實現了 Servlet 接口的類在 Servlet 容器中可用於處理請求併發送響應。github
Tomcat:Tomcat 是 Web 應用服務器,是一個 Servlet 容器,實現了對 Servlet 和 JSP 的支持。web
若是應用程序是以 war 包的方式放入 Tomcat 的 webapps 文件夾下面,那麼在 Tomcat 啓動時會加載 war 包,生成對應的一個文件夾,Tomcat 則會去對 webapps 文件夾下面的每個文件夾(咱們的應用程序)生成一個部署任務,去解析對應的 WEB-INF/web.xml 文件,將配置的 Servlet 加載到 Servlet 容器中。當 Tomcat 監聽到某端口的 HTTP 請求時,則會將請求解析成 Request 對象,而後交由相應的 Servlet 進行處理,最後將處理結果轉換成 HTTP 響應。算法
爲何是 webapps 目錄和 WEB-INF/web.xml 文件,能夠看一下 Tomcat 的 conf/server.xml 和 conf/context.xml 兩個配置文件,以下:spring
<!-- server.xml --> <!-- appBase 屬性指定應用程序所在目錄 --> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <!-- context.xml --> <Context> <!-- Default set of monitored resources. If one of these changes, the web application will be reloaded. --> <WatchedResource>WEB-INF/web.xml</WatchedResource> <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource> <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource> </Context>
爲了體現出整個演進過程,先來回顧下當初咱們是怎麼寫 Servlet 和 Filter 代碼來完成本身的第一個 Hello World 工程apache
項目結構數組
. ├── pom.xml ├── src ├── main │ ├── java │ │ └── cn │ │ └── edu │ │ └── shopping │ │ ├── filter │ │ │ └── HelloWorldFilter.java │ │ └── servlet │ │ └── HelloWorldServlet.java │ └── webapp │ └── WEB-INF │ └── web.xml └── test └── java
cn.edu.shopping.servlet.HelloWorldServlet.java
瀏覽器
public class HelloWorldServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); PrintWriter writer = response.getWriter(); writer.println("Hello World"); } }
cn.edu.shopping.filter.HelloWorldFilter.java
public class HelloWorldFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("觸發 Hello World 過濾器..."); chain.doFilter(request, response); } @Override public void destroy() { } }
在 web.xml 中配置 Servlet 和 Filter
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>cn.edu.shopping.servlet.HelloWorldServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <filter> <filter-name>HelloWorldFilter</filter-name> <filter-class>cn.edu.shopping.filter.HelloWorldFilter</filter-class> </filter> <filter-mapping> <filter-name>HelloWorldFilter</filter-name> <url-pattern>/hello</url-pattern> </filter-mapping> </web-app>
上述就是我當初第一個 Hello World 工程,配置 Tomcat 後啓動,在瀏覽器裏面輸入 http://127.0.0.1:8080/hello
可看到 「Hello World」,在控制檯會打印「觸發 Hello World 過濾器...」
參考 IBM 的 Servlet 3.0 新特性詳解 文章
Servlet 3.0 做爲 Java EE 6 規範體系中一員,隨着 Java EE 6 規範一塊兒發佈。該版本在前一版本(Servlet 2.5)的基礎上提供了若干新特性用於簡化 Web 應用的開發和部署。其中有幾項特性的引入讓開發者感到很是興奮,同時也得到了 Java 社區的一片讚譽之聲:
經過 Servlet3.0 首先提供了 @WebServlet
、@WebFilter
和 @WebListener
等註解,能夠替代 web.xml 文件中的 Servlet 和 Filter 等配置項
除了以上的新特性以外,ServletContext 對象的功能在新版本中也獲得了加強。如今,該對象支持在運行時動態部署 Servlet、過濾器、監聽器,以及爲 Servlet 和過濾器增長 URL 映射等。以 Servlet 爲例,過濾器與監聽器與之相似。ServletContext 爲動態配置 Servlet 增長了以下方法:
ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)
ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
ServletRegistration.Dynamic addServlet(String servletName, String className)
其中前三個方法的做用是相同的,只是參數類型不一樣而已;經過 createServlet() 方法建立的 Servlet,一般須要作一些自定義的配置,而後使用 addServlet() 方法來將其動態註冊爲一個能夠用於服務的 Servlet。兩個 getServletRegistration() 方法主要用於動態爲 Servlet 增長映射信息,這等價於在 web.xml( 抑或 web-fragment.xml) 中使用 標籤爲存在的 Servlet 增長映射信息。
以上 ServletContext 新增的方法要麼是在 ServletContextListener
的 contexInitialized 方法中調用,要麼是在 ServletContainerInitializer
的 onStartup()
方法中調用。
ServletContainerInitializer 也是 Servlet 3.0 新增的一個接口,容器在啓動時使用 JAR 服務 API(JAR Service API) 來發現 ServletContainerInitializer 的實現類,而且容器將 WEB-INF/lib 目錄下 JAR 包中的類都交給該類的 onStartup() 方法處理,咱們一般須要在該實現類上使用 @HandlesTypes 註解來指定但願被處理的類,過濾掉不但願給 onStartup() 處理的類。
一個典型的 Servlet3.0+ 的 Web 項目結構以下:
. ├── pom.xml ├── src ├── main │ ├── java │ │ └── cn │ │ └── edu │ │ └── shopping │ │ ├── CustomServletContainerInitializer.java │ │ ├── filter │ │ │ └── HelloWorldFilter.java │ │ └── servlet │ │ └── HelloWorldServlet.java │ ├── resources │ │ └── META-INF │ │ └── services │ │ └── javax.servlet.ServletContainerInitializer │ └── webapp │ └── WEB-INF │ └── web.xml └── test └── java
HelloWorldFilter 和 HelloWorldServlet 沒有變更,新增了一個 CustomServletContainerInitializer 對象,它實現了 javax.servlet.ServletContainerInitializer
接口,用來在 Web 容器啓動時加載須要的 Servlet 和 Filter,代碼以下:
public class CustomServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException { System.out.println("建立 Hello World Servlet..."); javax.servlet.ServletRegistration.Dynamic servlet = ctx.addServlet( HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class); servlet.addMapping("/hello"); System.out.println("建立 Hello World Filter..."); javax.servlet.FilterRegistration.Dynamic filter = ctx.addFilter(HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class); EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class); dispatcherTypes.add(DispatcherType.REQUEST); dispatcherTypes.add(DispatcherType.FORWARD); filter.addMappingForUrlPatterns(dispatcherTypes, true, "/hello"); } }
在實現的 onStartup 方法中向 ServletContext 對象(Servlet 上下文)添加以前在 web.xml 中配置的 HelloWorldFilter 和 HelloWorldServlet,這樣一來就能夠去除 web.xml 文件了。
方法入參中的 Set<Class<?>> c
是和 @HandlesTypes 註解結合使用的,指定須要處理的 Calss 類,能夠參考 Spring 中的 SpringServletContainerInitializer
使用方法
這麼聲明一個 ServletContainerInitializer 的實現類,Web 容器並不會識別它,須要藉助 SPI 機制來指定該初始化類,經過在項目 ClassPath 路徑下建立 META-INF/services/javax.servlet.ServletContainerInitializer
文件來作到的,內容以下:
cn.edu.shopping.CustomServletContainerInitializer
這樣一來,使用 ServletContainerInitializer 和 SPI 機制則能夠拜託 web.xml 了。
回到 Spring 全家桶,你可能已經忘何時開始不寫 web.xml 了,如今的項目基本看不到它了,Spring 又是如何支持 Servlet3.0 規範的呢?
在 Spring 的 spring-web
子工程的 ClassPath 下面的有一個 META-INF/services/javax.servlet.ServletContainerInitializer
文件,以下:
org.springframework.web.SpringServletContainerInitializer
org.springframework.web.SpringServletContainerInitializer
類,代碼以下:
/** * Servlet 3.0 {@link ServletContainerInitializer} designed to support code-based * configuration of the servlet container using Spring's {@link WebApplicationInitializer} * SPI as opposed to (or possibly in combination with) the traditional * {@code web.xml}-based approach. */ @HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List<WebApplicationInitializer> initializers = new LinkedList<>(); if (webAppInitializerClasses != null) { for (Class<?> waiClass : webAppInitializerClasses) { // Be defensive: Some servlet containers provide us with invalid classes, // no matter what @HandlesTypes says... // <1> if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { try { initializers.add((WebApplicationInitializer) ReflectionUtils.accessibleConstructor(waiClass).newInstance()); } catch (Throwable ex) { throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); } } } } if (initializers.isEmpty()) { servletContext.log("No Spring WebApplicationInitializer types detected on classpath"); return; } servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath"); AnnotationAwareOrderComparator.sort(initializers); for (WebApplicationInitializer initializer : initializers) { // <2> initializer.onStartup(servletContext); } } }
注意我在源碼中標註兩個序號,這對於咱們理解 Spring 裝配 Servlet 的流程來講很是重要
<1>
提示咱們因爲 Servlet 廠商實現的差別,onStartup 方法會加載咱們本不想處理的 Class 對象,因此進行了特判。
<2>
Spring 與咱們上述提供的 Demo 不一樣,並無在 SpringServletContainerInitializer 中直接對 Servlet 和 Filter 進行註冊,而是委託給了一個陌生的類 WebApplicationInitializer ,這個類即是 Spring 用來初始化 Web 環境的委託者類,它的實現類:
你必定不會對 DispatcherServlet 感到陌生,他就是 Spring MVC 中的核心類,AbstractDispatcherServletInitializer
即是無 web.xml
前提下,建立 DispatcherServlet 的關鍵類,代碼以下:
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { // 調用父類啓動的邏輯 super.onStartup(servletContext); // 註冊 DispacherServlt registerDispatcherServlet(servletContext); } protected void registerDispatcherServlet(ServletContext servletContext) { // 得到 Servlet 名 String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return null or empty"); // <1> 建立 WebApplicationContext 對象 WebApplicationContext servletAppContext = createServletApplicationContext(); Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null"); // <2> 建立 FrameworkServlet 對象 FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null"); dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet); if (registration == null) { throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " + "Check if there is another servlet registered under the same name."); } registration.setLoadOnStartup(1); registration.addMapping(getServletMappings()); registration.setAsyncSupported(isAsyncSupported()); // <3> 註冊過濾器 Filter[] filters = getServletFilters(); if (!ObjectUtils.isEmpty(filters)) { for (Filter filter : filters) { registerServletFilter(servletContext, filter); } } customizeRegistration(registration); } }
<1>
處,調用 createServletApplicationContext()
方法,建立 WebApplicationContext 對象,代碼以下:
// AbstractAnnotationConfigDispatcherServletInitializer.java @Override protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); Class<?>[] configClasses = getServletConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { context.register(configClasses); } return context; }
<2>
處,調用 createDispatcherServlet(WebApplicationContext servletAppContext)
方法,建立 FrameworkServlet 對象,代碼以下:
// AbstractDispatcherServletInitializer.java protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) { return new DispatcherServlet(servletAppContext); }
servletAppContext
方法參數,這就是該 DispatcherServlet 的 Servlet WebApplicationContext 容器注意,上述這一切特性從 Spring 3 就已經存在了,而現在 Spring 5 已經伴隨 SpringBoot 2.0 一塊兒發行了
讀到這兒,你已經閱讀了全文的 1/2。SpringBoot 對於 Servlet 的處理纔是重頭戲,由於 SpringBoot 使用範圍很廣,不多有人用 Spring 而不用 SpringBoot 了
是的,前面所講述的 Servlet 的規範,不管是 web.xml 中的配置,仍是 Servlet3.0 中的 ServletContainerInitializer 和 SpringBoot 的加載流程都沒有太大的關聯。按照慣例,先賣個關子,先看看如何在 SpringBoot 中註冊 Servlet 和 Filter,再來解釋下 SpringBoot 的獨特之處
SpringBoot 依舊兼容 Servlet 3.0 一系列以 @Web*
開頭的註解:@WebServlet
,@WebFilter
,@WebListener
@WebServlet("/hello") public class HelloWorldServlet extends HttpServlet{} @WebFilter("/hello/*") public class HelloWorldFilter implements Filter {}
在啓動類上面添加 @ServletComponentScan
註解去掃描到這些註解
@SpringBootApplication @ServletComponentScan public class SpringBootServletApplication { public static void main(String[] args) { SpringApplication.run(SpringBootServletApplication.class, args); } }
這種方式相對來講比較簡介直觀,其中 org.springframework.boot.web.servlet.@ServletComponentScan
註解經過 @Import(ServletComponentScanRegistrar.class)
方式,它會將掃描到的 @WebServlet
、@WebFilter
、@WebListener
的註解對應的類,最終封裝成 FilterRegistrationBean、ServletRegistrationBean、ServletListenerRegistrationBean 對象,註冊到 Spring 容器中。也就是說,和註冊方式二:RegistrationBean統一了
@Configuration public class WebConfig { @Bean public ServletRegistrationBean<HelloWorldServlet> helloWorldServlet() { ServletRegistrationBean<HelloWorldServlet> servlet = new ServletRegistrationBean<>(); servlet.addUrlMappings("/hello"); servlet.setServlet(new HelloWorldServlet()); return servlet; } @Bean public FilterRegistrationBean<HelloWorldFilter> helloWorldFilter() { FilterRegistrationBean<HelloWorldFilter> filter = new FilterRegistrationBean<>(); filter.addUrlPatterns("/hello/*"); filter.setFilter(new HelloWorldFilter()); return filter; } }
ServletRegistrationBean 和 FilterRegistrationBean 都繼成 RegistrationBean,它是 SpringBoot 中普遍應用的一個註冊類,負責把 Servlet,Filter,Listener 給容器化,使它們被 Spring 託管,而且完成自身對 Web 容器的註冊,這種註冊方式值得推崇
從圖中能夠看出 RegistrationBean 的地位,它的幾個實現類做用分別是:
暫時只介紹上面兩種方式,接下來開始討論 SpringBoot 中 Servlet 的加載流程,討論的前提是 SpringBoot 環境下使用內嵌的容器,好比最典型的 Tomcat
當使用內嵌的 Tomcat 時,你在 SpringServletContainerInitializer 上面打斷點,會發現根本不會進入該類的內部,由於 SpringBoot 徹底走了另外一套初始化流程,而是進入了 org.springframework.boot.web.embedded.tomcat.TomcatStarter
這個類
仔細掃一眼源碼包,並無發現有 SPI 文件對應到 TomcatStarter,也就是說沒有經過 SPI 機制加載這個類,爲何沒有這麼作呢?能夠翻閱 Spring Github 中的 issue,其中有 Spring 做者確定的答覆:https://github.com/spring-projects/spring-boot/issues/321
This was actually an intentional design decision. The search algorithm used by the containers was problematic. It also causes problems when you want to develop an executable WAR as you often want a
javax.servlet.ServletContainerInitializer
for the WAR that is not executed when you runjava -jar
.See the
org.springframework.boot.context.embedded.ServletContextInitializer
for an option that works with Spring Beans.
SpringBoot 這麼作是有意而爲之,咱們在使用 SpringBoot 時,開發階段通常都是使用內嵌 Tomcat 容器,但部署時卻存在兩種選擇:一種是打成 jar 包,使用 java -jar
的方式運行;另外一種是打成 war 包,交給外置容器去運行。
前者就會致使容器搜索算法出現問題,由於這是 jar 包的運行策略,不會按照 Servlet 3.0 的策略去加載 ServletContainerInitializer!
最後做者還提供了一個替代選項:ServletContextInitializer,它和 ServletContainerInitializer 長得特別像,別搞混淆了!
org.springframework.boot.web.servlet.ServletContextInitializer
javax.servlet.ServletContainerInitializer
,前文提到的 RegistrationBean 就實現了 ServletContextInitializer 接口TomcatStarter 中 org.springframework.boot.context.embedded.ServletContextInitializer[] initializers
屬性,是 SpringBoot 初始化 Servlet,Filter,Listener 的關鍵,代碼以下:
class TomcatStarter implements ServletContainerInitializer { private static final Log logger = LogFactory.getLog(TomcatStarter.class); private final ServletContextInitializer[] initializers; private volatile Exception startUpException; TomcatStarter(ServletContextInitializer[] initializers) { this.initializers = initializers; } @Override public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException { try { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } catch (Exception ex) { this.startUpException = ex; // Prevent Tomcat from logging and re-throwing when we know we can // deal with it in the main thread, but log for information here. if (logger.isErrorEnabled()) { logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: " + ex.getMessage()); } } } public Exception getStartUpException() { return this.startUpException; } }
在 onStartup(Set<Class<?>> classes, ServletContext servletContext)
方法中,負責調用一系列的 ServletContextInitializer 對象的 onStartup 方法
那麼在 debug 的過程當中,構造方法中的 ServletContextInitializer[] initializers
入參到底包含了哪些類呢?會不會有咱們前面介紹的 RegistrationBean 呢?
RegistrationBean 並無出如今 TomcatStarter 的 debug 信息中,initializers
包含了三個類,其中只有第 3
個類看上去比較核心,ServletWebServerApplicationContext 的 子類 AnnotationConfigServletWebServerApplicationContext 對象,爲了搞清楚 SpringBoot 如何加載 Filter、Servlet、Listener ,看來還得研究下 ServletWebServerApplicationContext 對象
上面是基於 SpringBoot 2.0.3.RELEASE 版本作的總體分析,若是是其餘版本,可能會存在部分差別,不過原理都相同,不會有太大的變化
ApplicationContext 你們應該是比較熟悉的,這是 Spring 一個比較核心的類,通常咱們能夠從中獲取到那些註冊在容器中的託管 Bean,而這篇文章,主要分析的即是它在內嵌容器中的實現類:org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
,重點分析它加載 Filter、Servlet 和 Listener 這部分的代碼。
這裏是整個代碼中迭代層次最深的部分,作好心理準備起航,來看看 ServletWebServerApplicationContext 是怎麼獲取到全部的 Filter、Servlet 和 Listener 對象的,如下方法大部分出自於 ServletWebServerApplicationContext
onRefresh()
方法,是 ApplicationContext 的生命週期方法,ServletWebServerApplicationContext 的實現很是簡單,只幹了一件事:
@Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); //第二層的入口 } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } }
看名字 Spring 是想建立一個內嵌的 Web 容器,代碼以下:
private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); 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(); }
凡是帶有 Servlet,Initializer 字樣的方法,都是咱們須要留意的。其中 getSelfInitializer()
方法,便涉及到了咱們最爲關心的初始化流程,因此接着鏈接到了第三層
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return this::selfInitialize; } private void selfInitialize(ServletContext servletContext) throws ServletException { prepareWebApplicationContext(servletContext); ConfigurableListableBeanFactory beanFactory = getBeanFactory(); ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes( beanFactory); WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, getServletContext()); existingScopes.restore(); WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, getServletContext()); // 第四層的入口 for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } }
還記得前面 TomcatStarter 的 debug 信息中,第 3
個 ServletContextInitializer 就是在 ServletWebServerApplicationContext 這裏的 getSelfInitializer()
方法中建立的
解釋下這裏的 getSelfInitializer()
和 selfInitialize(ServletContext servletContext)
方法,爲何要這麼設計?
這是典型的回調式方式,當匿名 ServletContextInitializer 類被 TomcatStarter 的 onStartup()
方法調用,設計上是觸發了 selfInitialize(ServletContext servletContext)
方法的調用
因此這下就清晰了,爲何 TomcatStarter 中沒有出現 RegistrationBean ,實際上是隱式觸發了 ServletWebServerApplicationContext 中的 selfInitialize(ServletContext servletContext)
方法。這樣,在 selfInitialize(ServletContext servletContext)
方法中,調用 getServletContextInitializerBeans()
方法,得到 ServletContextInitializer 數組就成了關鍵
/** * Returns {@link ServletContextInitializer}s that should be used with the embedded web server. * By default this method will first attempt to find {@link ServletContextInitializer}, * {@link Servlet}, {@link Filter} and certain {@link EventListener} beans. * @return the servlet initializer beans */ protected Collection<ServletContextInitializer> getServletContextInitializerBeans() { return new ServletContextInitializerBeans(getBeanFactory()); //第五層的入口 }
從註釋中能夠知曉這個 ServletContextInitializerBeans 類,就是用來加載 Servlet 和 Filter 的
org.springframework.boot.web.servlet.ServletContextInitializerBeans
public ServletContextInitializerBeans(ListableBeanFactory beanFactory) { this.initializers = new LinkedMultiValueMap<>(); addServletContextInitializerBeans(beanFactory); // 第六層的入口 addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = this.initializers.values() .stream() .flatMap((value) -> value.stream() .sorted(AnnotationAwareOrderComparator.INSTANCE)) .collect(Collectors.toList()); this.sortedList = Collections.unmodifiableList(sortedInitializers); }
// ServletContextInitializerBeans.java private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) { for (Entry<String, ServletContextInitializer> initializerBean : getOrderedBeansOfType( beanFactory, ServletContextInitializer.class)) { addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory); } }
getOrderedBeansOfType(beanFactory, ServletContextInitializer.class)
方法,即是去 Spring 容器中尋找註冊過的 ServletContextInitializer 對象們,這時候就能夠把以前那些 RegistrationBean 所有加載出來了,而且 RegistrationBean 還實現了 Ordered 接口,在這兒用於排序
若是你對具體的代碼流程不感興趣,能夠跳過上述的 6 層分析,直接看本節的結論,總結以下:
從上圖中能夠看到,咱們配置的 Filter 和 Servlet 註冊類都獲取到了,而後調用其 onStartup 方法,進去後你會發現調用 ServletContext 對象的 addServlet 方法註冊 Servlet,能夠返回 Servlet3.0 新特性小節中回顧一下
研究完了上述 SpringBoot 加載 Servlet 的內部原理,能夠發現 ServletContextInitializer 實際上是 Spring 中 ServletContainerInitializer 的代理,雖然 SpringBoot 中 Servlet3.0 不起做用了,但它的代理仍是會被加載的,因而咱們有了第三種方式註冊 servlet
@Configuration public class CustomServletContextInitializer implements ServletContextInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { System.out.println("建立 Hello World Servlet..."); javax.servlet.ServletRegistration.Dynamic servlet = ctx.addServlet( HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class); servlet.addMapping("/hello"); System.out.println("建立 Hello World Filter..."); javax.servlet.FilterRegistration.Dynamic filter = ctx.addFilter(HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class); EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class); dispatcherTypes.add(DispatcherType.REQUEST); dispatcherTypes.add(DispatcherType.FORWARD); filter.addMappingForUrlPatterns(dispatcherTypes, true, "/hello"); } }
雖然 ServletCantainerInitializer 不能被內嵌容器加載,ServletContextInitializer 卻能被 SpringBoot 的 ServletWebServerApplicationContext 加載到,從而裝配其中的 Servlet 和 Filter。實際開發中,仍是以一,二兩種方式來註冊爲主,這裏只是提供一個可能性,來讓咱們理解 SpringBoot 的加載流程
天然是被 new
出來的,在 TomcatServletWebServerFactory#configureContext
中能夠看到,TomcatStarter 是被主動實例化出來的,而且還傳入了 ServletContextInitializer 的數組,和上面分析的同樣,一共有三個 ServletContextInitializer,包含了 ServletWebServerApplicationContext 中的匿名實現
protected void configureContext(Context context, ServletContextInitializer[] initializers) { // <1> TomcatStarter starter = new TomcatStarter(initializers); // <2> if (context instanceof TomcatEmbeddedContext) { // Should be true ((TomcatEmbeddedContext) context).setStarter(starter); } // ... 省略相關代碼 }
<1>
處,建立了 TomcatStarter 對象。<2>
處,經過 context instanceof TomcatEmbeddedContext
判斷使用的是內嵌的 Tomcat ,因此將 TomcatStarter 做爲 Initializer若是對 <2>
處的邏輯感興趣的胖友,能夠在如下方法上打斷點進行調試
TomcatServletWebServerFactory#getWebServer(ServletContextInitializer... initializers)
TomcatStarter#onStartup(Set<Class<?>> classes, ServletContext servletContext)
ServletWebServerApplicationContext#createWebServer
執行順序:三、一、2
@Configuration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(ServletRequest.class) @ConditionalOnWebApplication(type = Type.SERVLET) // 這個就是咱們 SpringBoot 中 application.yml 配置文件中 server.* 配置類,也就是 Tomcat 相關配置 @EnableConfigurationProperties(ServerProperties.class) @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class }) public class ServletWebServerFactoryAutoConfiguration { @Bean public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer( ServerProperties serverProperties) { return new ServletWebServerFactoryCustomizer(serverProperties); } @Bean // 保證存在 Tomcat 的 Class 對象 @ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat") public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer( ServerProperties serverProperties) { return new TomcatServletWebServerFactoryCustomizer(serverProperties); } // 省略 WebServerFactoryCustomizerBeanPostProcessor 類 }
其中 @Import
註解會注入 ServletWebServerFactoryConfiguration
的幾個靜態內部類,以下:
class ServletWebServerFactoryConfiguration { @Configuration @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory() { return new TomcatServletWebServerFactory(); } } // 省略 EmbeddedJetty、EmbeddedUndertow }
這樣一來,只要 classpath 下存在 javax.servlet.Servlet
、org.apache.catalina.startup.Tomcat
、org.apache.coyote.UpgradeProtocol
類,而且不存在 ServletWebServerFactory
類型的 Bean 則會注入 EmbeddedTomcat 配置類,也就建立一個 TomcatServletWebServerFactory 類型的 Bean
存在 web.xml
配置的 Java Web 項目,Servlet3.0 的 Java Web 項目,Spring Boot 內嵌容器的 Java Web 項目加載 Servlet,這三種項目,Servlet,Filter,Listener 的流程都是有所差別的。理解清楚這其中的由來,其實並不容易,至少得搞懂 Servlet3.0 的規範,SpringBoot 內嵌容器的加載流程等等前置邏輯
簡化了整個 SpringBoot 加載 Servlet 的流程,以下圖所示:
參考文章:芋道源碼《精盡 MyBatis 源碼分析》