今天咱們來放鬆下心情,不聊分佈式,雲原生,來聊一聊初學者接觸的最多的 java web 基礎。幾乎全部人都是從 servlet,jsp,filter 開始編寫本身的第一個 hello world 工程。那時,還離不開 web.xml 的配置,在 xml 文件中編寫繁瑣的 servlet 和 filter 的配置。隨着 spring 的普及,配置逐漸演變成了兩種方式—java configuration 和 xml 配置共存。現現在,springboot 的普及,java configuration 成了主流,xml 配置彷佛已經「滅絕」了。不知道你有沒有好奇過,這中間都發生了哪些改變,web.xml 中的配置項又是被什麼替代項取代了?
java
爲了體現出整個演進過程,仍是來回顧下 n 年前咱們是怎麼寫 servlet 和 filter 代碼的。git
項目結構(本文都採用 maven 項目結構)github
public class HelloWorldServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/plain"); PrintWriter out = resp.getWriter(); out.println("hello world"); }
public class HelloWorldFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("觸發 hello world 過濾器..."); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }
別忘了在 web.xml 中配置 servlet 和 filterweb
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>moe.cnkirito.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>moe.cnkirito.filter.HelloWorldFilter</filter-class> </filter> <filter-mapping> <filter-name>HelloWorldFilter</filter-name> <url-pattern>/hello</url-pattern> </filter-mapping> </web-app>
這樣,一個 java web hello world 就完成了。固然,本文不是 servlet 的入門教程,只是爲了對比。算法
Servlet 3.0 做爲 Java EE 6 規範體系中一員,隨着 Java EE 6 規範一塊兒發佈。該版本在前一版本(Servlet 2.5)的基礎上提供了若干新特性用於簡化 Web 應用的開發和部署。其中一項新特性即是提供了無 xml 配置的特性。spring
servlet3.0 首先提供了 @WebServlet,@WebFilter 等註解,這樣便有了拋棄 web.xml 的第一個途徑,憑藉註解聲明 servlet 和 filter 來作到這一點。數組
除了這種方式,servlet3.0 規範還提供了更強大的功能,能夠在運行時動態註冊 servlet ,filter,listener。以 servlet 爲例,過濾器與監聽器與之相似。ServletContext 爲動態配置 Servlet 增長了以下方法:tomcat
其中前三個方法的做用是相同的,只是參數類型不一樣而已;經過 createServlet() 方法建立的 Servlet,一般須要作一些自定義的配置,而後使用 addServlet() 方法來將其動態註冊爲一個能夠用於服務的 Servlet。兩個 getServletRegistration() 方法主要用於動態爲 Servlet 增長映射信息,這等價於在 web.xml 中使用 標籤爲存在的 Servlet 增長映射信息。springboot
以上 ServletContext 新增的方法要麼是在 ServletContextListener 的 contexInitialized 方法中調用,要麼是在 ServletContainerInitializer 的 onStartup() 方法中調用。app
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 │ │ └── moe │ │ └── cnkirito │ │ ├── CustomServletContainerInitializer.java │ │ ├── filter │ │ │ └── HelloWorldFilter.java │ │ └── servlet │ │ └── HelloWorldServlet.java │ └── resources │ └── META-INF │ └── services │ └── javax.servlet.ServletContainerInitializer └── test └── java |
我並未對 HelloWorldServlet 和 HelloWorldFilter 作任何改動,而是新增了一個 CustomServletContainerInitializer ,它實現了 javax.servlet.ServletContainerInitializer
接口,用來在 web 容器啓動時加載指定的 servlet 和 filter,代碼以下:
public class CustomServletContainerInitializer implements ServletContainerInitializer { private final static String JAR_HELLO_URL = "/hello"; @Override public void onStartup(Set<Class<?>> c, ServletContext servletContext) { System.out.println("建立 helloWorldServlet..."); ServletRegistration.Dynamic servlet = servletContext.addServlet( HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class); servlet.addMapping(JAR_HELLO_URL); System.out.println("建立 helloWorldFilter..."); FilterRegistration.Dynamic filter = servletContext.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, JAR_HELLO_URL); } }
對上述代碼進行一些解讀。ServletContext 咱們稱之爲 servlet 上下文,它維護了整個 web 容器中註冊的 servlet,filter,listener,以 servlet 爲例,可使用 servletContext.addServlet 等方法來添加 servlet。而方法入參中 Set<Class<?>> c 和 @HandlesTypes 註解在 demo 中我並未使用,感興趣的朋友能夠 debug 看看到底獲取了哪些 class ,通常正常的流程是使用 @HandlesTypes 指定須要處理的 class,然後對 Set<Class<?>> 進行判斷是否屬於該 class,正如前文所言,onStartup 會加載不須要被處理的一些 class。
這麼聲明一個 ServletContainerInitializer 的實現類,web 容器並不會識別它,因此,須要藉助 SPI 機制來指定該初始化類,這一步驟是經過在項目路徑下建立 META-INF/services/javax.servlet.ServletContainerInitializer
來作到的,它只包含一行內容
moe.cnkirito.CustomServletContainerInitializer
使用 ServletContainerInitializer 和 SPI 機制,咱們的 web 應用即可以完全擺脫 web.xml 了。
回到咱們的 spring 全家桶,可能已經忘了具體是何時開始不寫 web.xml 了,我只知道如今的項目已經再也看不到它了,spring 又是如何支持 servlet3.0 規範的呢?
尋找 spring 中 ServletContainerInitializer 的實現類並不困難,能夠迅速定位到 SpringServletContainerInitializer 該實現類。
@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>(); 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) 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); // <2> for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } } }
查看其 java doc,描述以下:
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.
注意我在源碼中標註兩個序號,這對於咱們理解 spring 裝配 servlet 的流程來講很是重要。
<1> 英文註釋是 spring 源碼中自帶的,它提示咱們因爲 servlet 廠商實現的差別,onStartup 方法會加載咱們本不想處理的 class,因此進行了特判。
<2> spring 與咱們以前的 demo 不一樣,並無在 SpringServletContainerInitializer 中直接對 servlet 和 filter 進行註冊,而是委託給了一個陌生的類 WebApplicationInitializer ,WebApplicationInitializer 類即是 spring 用來初始化 web 環境的委託者類,它一般有三個實現類:
你必定不會對 dispatcherServlet 感到陌生,AbstractDispatcherServletInitializer#registerDispatcherServlet 即是無 web.xml 前提下建立 dispatcherServlet 的關鍵代碼。
能夠去項目中尋找一下 org.springframework:spring-web:version 的依賴,它下面就存在一個 servletContainerInitializer 的擴展,指向了 SpringServletContainerInitializer,這樣只要在 servlet3.0 環境下部署,spring 即可以自動加載進行初始化:
注意,上述這一切特性從 spring 3 就已經存在了,而現在 spring 5 已經伴隨 springboot 2.0 一塊兒發行了。
讀到這兒,你已經閱讀了全文的 1/2。springboot 對於 servlet 的處理纔是重頭戲,其一,是由於 springboot 使用範圍很廣,不多有人用 spring 而不用 springboot 了;其二,是由於它沒有徹底遵照 servlet3.0 的規範!
是的,前面所講述的 servlet 的規範,不管是 web.xml 中的配置,仍是 servlet3.0 中的 ServletContainerInitializer 和 springboot 的加載流程都沒有太大的關聯。按照慣例,先賣個關子,先看看如何在 springboot 中註冊 servlet 和 filter,再來解釋下 springboot 的獨特之處。
springboot 依舊兼容 servlet3.0 一系列以 @Web* 開頭的註解:@WebServlet,@WebFilter,@WebListener
@WebServlet("/hello") public class HelloWorldServlet extends HttpServlet{}
@WebFilter("/hello/*") public class HelloWorldFilter implements Filter {} |
不要忘記讓啓動類去掃描到這些註解
@SpringBootApplication @ServletComponentScan public class SpringBootServletApplication { public static void main(String[] args) { SpringApplication.run(SpringBootServletApplication.class, args); } } |
我認爲這是幾種方式中最爲簡潔的方式,若是真的有特殊需求,須要在 springboot 下注冊 servlet,filter,能夠採用這樣的方式,比較直觀。
@Bean public ServletRegistrationBean helloWorldServlet() { ServletRegistrationBean helloWorldServlet = new ServletRegistrationBean(); myServlet.addUrlMappings("/hello"); myServlet.setServlet(new HelloWorldServlet()); return helloWorldServlet; } @Bean public FilterRegistrationBean helloWorldFilter() { FilterRegistrationBean helloWorldFilter = new FilterRegistrationBean(); myFilter.addUrlPatterns("/hello/*"); myFilter.setFilter(new HelloWorldFilter()); return helloWorldFilter; } |
ServletRegistrationBean 和 FilterRegistrationBean 都集成自 RegistrationBean ,RegistrationBean 是 springboot 中普遍應用的一個註冊類,負責把 servlet,filter,listener 給容器化,使他們被 spring 託管,而且完成自身對 web 容器的註冊。這種註冊方式也值得推崇。
從圖中能夠看出 RegistrationBean 的地位,它的幾個實現類做用分別是:幫助容器註冊 filter,servlet,listener,最後的 DelegatingFilterProxyRegistrationBean 使用的很少,但熟悉 SpringSecurity 的朋友不會感到陌生,SpringSecurityFilterChain 就是經過這個代理類來調用的。另外 RegistrationBean 實現了 ServletContextInitializer 接口,這個接口將會是下面分析的核心接口,你們先混個眼熟,瞭解下它有一個抽象實現 RegistrationBean 便可。
暫時只介紹這兩種方式,下面解釋下以前賣的關子,爲何說 springboot 沒有徹底遵照 servlet3.0 規範。討論的前提是 springboot 環境下使用內嵌的容器,好比最典型的 tomcat。高能預警,如下內容比較燒腦,以爲看起來吃力的朋友能夠跳過本節直接看下一節的總結!
當使用內嵌的 tomcat 時,你會發現 springboot 徹底走了另外一套初始化流程,徹底沒有使用前面提到的 SpringServletContainerInitializer,實際上一開始我在各類 ServletContainerInitializer 的實現類中打了斷點,最終定位到,根本沒有運行到 SpringServletContainerInitializer 內部,而是進入了 TomcatStarter 這個類中。
而且,仔細掃了一眼源碼的包,並無發現有 SPI 文件對應到 TomcatStarter。因而我猜測,內嵌 tomcat 的加載可能不依賴於 servlet3.0 規範和 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 考慮到了以下的問題,咱們在使用 springboot 時,開發階段通常都是使用內嵌 tomcat 容器,但部署時卻存在兩種選擇:一種是打成 jar 包,使用 java -jar 的方式運行;另外一種是打成 war 包,交給外置容器去運行。前者就會致使容器搜索算法出現問題,由於這是 jar 包的運行策略,不會按照 servlet3.0 的策略去加載 ServletContainerInitializer!最後做者還提供了一個替代選項:ServletContextInitializer,注意是 ServletContextInitializer!它和 ServletContainerInitializer 長得特別像,別搞混淆了,前者 ServletContextInitializer 是 org.springframework.boot.web.servlet.ServletContextInitializer,後者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer,前文還提到 RegistrationBean 實現了 ServletContextInitializer 接口。
TomcatStarter 中的 org.springframework.boot.context.embedded.ServletContextInitializer
是 springboot 初始化 servlet,filter,listener 的關鍵。
class TomcatStarter implements ServletContainerInitializer { private final ServletContextInitializer[] initializers; TomcatStarter(ServletContextInitializer[] initializers) { this.initializers = initializers; } @Override public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } } |
通過刪減源碼後,能夠看出 TomcatStarter 的主要邏輯,它其實就是負責調用一系列 ServletContextInitializer 的 onStartup 方法,那麼在 debug 中,ServletContextInitializer[] initializers 到底包含了哪些類呢?會不會有咱們前面介紹的 RegisterBean 呢?
太天真了,RegisterBean 並無出如今 TomcatStarter 的 debug 信息中,initializers 只包含了三個類,其中只有第一個類看上去比較核心,注意第一個類不是 EmbeddedWebApplicationContext!而是這個類中的 $1 匿名類,爲了搞清楚 springboot 如何加載 filter servlet listener ,看來還得研究下 EmbeddedWebApplicationContext 的結構。
ApplicationContext 你們應該是比較熟悉的,這是 spring 一個比較核心的類,通常咱們能夠從中獲取到那些註冊在容器中的託管 Bean,而這篇文章,主要分析的即是它在內嵌容器中的實現類:EmbeddedWebApplicationContext,重點分析它加載 filter servlet listener 這部分的代碼。這裏是整個代碼中迭代層次最深的部分,作好心理準備起航,來看看 EmbeddedWebApplicationContext 是怎麼獲取到全部的 servlet filter listener 的!如下方法均出自於 EmbeddedWebApplicationContext。
第一層:onRefresh()
onRefresh 是 ApplicationContext 的生命週期方法,EmbeddedWebApplicationContext 的實現很是簡單,只幹了一件事:
@Override protected void onRefresh() { super.onRefresh(); try { createEmbeddedServletContainer();//第二層的入口 } catch (Throwable ex) { throw new ApplicationContextException("Unable to start embedded container", ex); } } |
createEmbeddedServletContainer 鏈接到了第二層
第二層:createEmbeddedServletContainer()
看名字 spring 是想建立一個內嵌的 servlet 容器,ServletContainer 其實就是 servlet filter listener 的總稱。
private void createEmbeddedServletContainer() { EmbeddedServletContainer localContainer = this.embeddedServletContainer; ServletContext localServletContext = getServletContext(); if (localContainer == null && localServletContext == null) { EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory(); this.embeddedServletContainer = containerFactory .getEmbeddedServletContainer(getSelfInitializer());//第三層的入口 } else if (localServletContext != null) { try { getSelfInitializer().onStartup(localServletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); } |
凡是帶有 servlet,initializer 字樣的方法都是咱們須要留意的,getSelfInitializer() 便涉及到了咱們最爲關心的初始化流程。
第三層:getSelfInitializer()
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return new ServletContextInitializer() { @Override public void onStartup(ServletContext servletContext) throws ServletException { selfInitialize(servletContext); } }; } private void selfInitialize(ServletContext servletContext) throws ServletException { prepareEmbeddedWebApplicationContext(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 信息中,第一個 ServletContextInitializer 就是出如今 EmbeddedWebApplicationContext 中的一個匿名類,沒錯了,就是這裏的 getSelfInitializer() 方法建立的!解釋下這裏的 getSelfInitializer() 和 selfInitialize(ServletContext servletContext) 爲何要這麼設計:這是典型的回調式方式,當匿名 ServletContextInitializer 類被 TomcatStarter 的 onStartup 方法調用,設計上是觸發了 selfInitialize(ServletContext servletContext) 的調用。因此這下就清晰了,爲何 TomcatStarter 中沒有出現 RegisterBean ,實際上是隱式觸發了 EmbeddedWebApplicationContext 中的 selfInitialize 方法。selfInitialize 方法中的 getServletContextInitializerBeans() 成了關鍵。
第四層:getServletContextInitializerBeans()
/** * Returns {@link ServletContextInitializer}s that should be used with the embedded * Servlet context. 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 的。
第五層:ServletContextInitializerBeans的構造方法
public ServletContextInitializerBeans(ListableBeanFactory beanFactory) { this.initializers = new LinkedMultiValueMap<Class<?>, ServletContextInitializer>(); addServletContextInitializerBeans(beanFactory);// 第六層的入口 addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = new ArrayList<ServletContextInitializer>(); for (Map.Entry<?, List<ServletContextInitializer>> entry : this.initializers .entrySet()) { AnnotationAwareOrderComparator.sort(entry.getValue()); sortedInitializers.addAll(entry.getValue()); } this.sortedList = Collections.unmodifiableList(sortedInitializers); } |
第六層:addServletContextInitializerBeans(beanFactory)
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) { for (Entry<String, ServletContextInitializer> initializerBean : getOrderedBeansOfType( beanFactory, ServletContextInitializer.class)) { addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory); } } |
getOrderedBeansOfType 方法即是去容器中尋找註冊過得 ServletContextInitializer ,這時候就能夠把以前那些 RegisterBean 所有加載出來了,而且 RegisterBean 還實現了 Ordered 接口,在這兒用於排序。再也不往下迭代了。
若是你對具體的代碼流程不感興趣,能夠跳過上述的6層分析,直接看本節的結論。總結以下:
研究完了上述 springboot 啓動的內部原理,能夠發現 ServletContextInitializer 實際上是 spring 中 ServletContainerInitializer 的代理,雖然 springboot 中 Servlet3.0 不起做用了,但它的代理仍是會被加載的,因而咱們有了第三種方式註冊 servlet。
@Configuration public class CustomServletContextInitializer implements ServletContextInitializer { private final static String JAR_HELLO_URL = "/hello"; @Override public void onStartup(ServletContext servletContext) throws ServletException { System.out.println("建立 helloWorldServlet..."); ServletRegistration.Dynamic servlet = servletContext.addServlet( HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class); servlet.addMapping(JAR_HELLO_URL); System.out.println("建立 helloWorldFilter..."); FilterRegistration.Dynamic filter = servletContext.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, JAR_HELLO_URL); } } |
雖然 ServletCantainerInitializer 不能被內嵌容器加載,ServletContextInitializer 卻能被 springboot 的 EmbeddedWebApplicationContext 加載到,從而裝配其中的 servlet 和 filter。實際開發中,仍是以一,二兩種方法來註冊爲主,這裏只是提供一個可能性,來讓咱們理解 springboot 的加載流程。
天然是被 new 出來的,在 TomcatEmbeddedServletContainerFactory#configureContext 中能夠看到,TomcatStarter 是被主動實例化出來的,而且還傳入了 ServletContextInitializer 的數組,和上面分析的同樣,一共有三個 ServletContextInitializer,包含了 EmbeddedWebApplicationContext 中的匿名實現。
protected void configureContext(Context context, ServletContextInitializer[] initializers) { TomcatStarter starter = new TomcatStarter(initializers); if (context instanceof TomcatEmbeddedContext) { // Should be true ((TomcatEmbeddedContext) context).setStarter(starter); } context.addServletContainerInitializer(starter, NO_CLASSES); ... } } |
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Configuration @ConditionalOnWebApplication @Import(BeanPostProcessorsRegistrar.class) public class EmbeddedServletContainerAutoConfiguration { /** * Nested configuration if Tomcat is being used. */ @Configuration @ConditionalOnClass({ Servlet.class, Tomcat.class }) @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat { @Bean public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() { return new TomcatEmbeddedServletContainerFactory(); } } } |
只要類路徑下存在 Tomcat 類,以及在 web 環境下,就會觸發 springboot 的自動配置。
存在 web.xml 配置的 java web 項目,servlet3.0 的 java web 項目,springboot 內嵌容器的 java web 項目加載 servlet,filter,listener 的流程都是有所差別的,理解清楚這其中的原來,其實並不容易,至少得搞懂 servlet3.0 的規範,springboot 內嵌容器的加載流程等等前置邏輯。
最後感謝下小馬哥的點撥,在此以前誤覺得: TomcatStarter 既然繼承了 ServletContainerInitializer,應該也是符合 servlet3.0 規範的,但實際上並無被 SPI 加載。