當前位置:首頁 > 公眾號精選 > 架構(gòu)師社區(qū)
[導讀]今天我們來放松下心情,不聊分布式,云原生,來聊一聊初學者接觸的最多的 java web 基礎。

今天我們來放松下心情,不聊分布式,云原生,來聊一聊初學者接觸的最多的 java web 基礎。幾乎所有人都是從 servlet,jsp,filter 開始編寫自己的第一個 hello world 工程。那時,還離不開 web.xml 的配置,在 xml 文件中編寫繁瑣的 servlet 和 filter 的配置。隨著 spring 的普及,配置逐漸演變成了兩種方式—java configuration 和 xml 配置共存。現(xiàn)如今,springboot 的普及,java configuration 成了主流,xml 配置似乎已經(jīng)“滅絕”了。不知道你有沒有好奇過,這中間都發(fā)生了哪些改變,web.xml 中的配置項又是被什么替代項取代了?

Spring揭秘--尋找遺失的web.xml servlet

servlet3.0 以前的時代

為了體現(xiàn)出整個演進過程,還是來回顧下 n 年前我們是怎么寫 servlet 和 filter 代碼的。

項目結(jié)構(gòu)(本文都采用 maven 項目結(jié)構(gòu))

.
├── pom.xml
├── src
    ├── main
    │   ├── java
    │   │   └── moe
    │   │       └── cnkirito
    │   │           ├── filter
    │   │           │   └── HelloWorldFilter.java
    │   │           └── servlet
    │   │               └── HelloWorldServlet.java
    │   └── resources
    │       └── WEB-INF
    │           └── web.xml
    └── test
        └── java
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("觸發(fā) hello world 過濾器...");
        filterChain.doFilter(servletRequest,servletResponse);
    } @Override public void destroy() {

    }
}

別忘了在 web.xml 中配置 servlet 和 filter

 <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>HelloWorldServletservlet-name> <servlet-class>moe.cnkirito.servlet.HelloWorldServletservlet-class> servlet> <servlet-mapping> <servlet-name>HelloWorldServletservlet-name> <url-pattern>/hellourl-pattern> servlet-mapping> <filter> <filter-name>HelloWorldFilterfilter-name> <filter-class>moe.cnkirito.filter.HelloWorldFilterfilter-class> filter> <filter-mapping> <filter-name>HelloWorldFilterfilter-name> <url-pattern>/hellourl-pattern> filter-mapping> web-app> 

這樣,一個 java web hello world 就完成了。當然,本文不是 servlet 的入門教程,只是為了對比。

servlet3.0 新特性

Spring揭秘--尋找遺失的web.xml servlet_3.0

Servlet 3.0 作為 Java EE 6 規(guī)范體系中一員,隨著 Java EE 6 規(guī)范一起發(fā)布。該版本在前一版本(Servlet 2.5)的基礎上提供了若干新特性用于簡化 Web 應用的開發(fā)和部署。其中一項新特性便是提供了無 xml 配置的特性。

servlet3.0 首先提供了 @WebServlet,@WebFilter 等注解,這樣便有了拋棄 web.xml 的第一個途徑,憑借注解聲明 servlet 和 filter 來做到這一點。

除了這種方式,servlet3.0 規(guī)范還提供了更強大的功能,可以在運行時動態(tài)注冊 servlet ,filter,listener。以 servlet 為例,過濾器與監(jiān)聽器與之類似。ServletContext 為動態(tài)配置 Servlet 增加了如下方法:

  • ServletRegistration.Dynamic addServlet(String servletName,Class servletClass)

  • ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)

  • ServletRegistration.Dynamic addServlet(String servletName, String className)

  • T createServlet(Classclazz)
  • ServletRegistration getServletRegistration(String servletName)

  • Map

其中前三個方法的作用是相同的,只是參數(shù)類型不同而已;通過 createServlet() 方法創(chuàng)建的 Servlet,通常需要做一些自定義的配置,然后使用 addServlet() 方法來將其動態(tài)注冊為一個可以用于服務的 Servlet。兩個 getServletRegistration() 方法主要用于動態(tài)為 Servlet 增加映射信息,這等價于在 web.xml 中使用標簽為存在的 Servlet 增加映射信息。

以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contexInitialized 方法中調(diào)用,要么是在 ServletContainerInitializer 的 onStartup() 方法中調(diào)用。

ServletContainerInitializer 也是 Servlet 3.0 新增的一個接口,容器在啟動時使用 JAR 服務 API(JAR Service API) 來發(fā)現(xiàn) ServletContainerInitializer 的實現(xiàn)類,并且容器將 WEB-INF/lib 目錄下 JAR 包中的類都交給該類的 onStartup() 方法處理,我們通常需要在該實現(xiàn)類上使用 @HandlesTypes 注解來指定希望被處理的類,過濾掉不希望給 onStartup() 處理的類。

一個典型的 servlet3.0+ 的 web 項目結(jié)構(gòu)如下:

.
├── 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 ,它實現(xiàn)了javax.servlet.ServletContainerInitializer接口,用來在 web 容器啟動時加載指定的 servlet 和 filter,代碼如下:

public class CustomServletContainerInitializer implements ServletContainerInitializer { private final static String JAR_HELLO_URL = "/hello"; @Override public void onStartup(Set> c, ServletContext servletContext) {

    System.out.println("創(chuàng)建 helloWorldServlet...");

    ServletRegistration.Dynamic servlet = servletContext.addServlet(
            HelloWorldServlet.class.getSimpleName(),
            HelloWorldServlet.class);
    servlet.addMapping(JAR_HELLO_URL);

    System.out.println("創(chuàng)建 helloWorldFilter...");

    FilterRegistration.Dynamic filter = servletContext.addFilter(
            HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);

    EnumSetdispatcherTypes = 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。而方法入?yún)⒅?Set

這么聲明一個 ServletContainerInitializer 的實現(xiàn)類,web 容器并不會識別它,所以,需要借助 SPI 機制來指定該初始化類,這一步驟是通過在項目路徑下創(chuàng)建META-INF/services/javax.servlet.ServletContainerInitializer來做到的,它只包含一行內(nèi)容:

moe.cnkirito.CustomServletContainerInitializer 

使用 ServletContainerInitializer 和 SPI 機制,我們的 web 應用便可以徹底擺脫 web.xml 了。

Spring 是如何支持 servlet3.0 的?

回到我們的 spring 全家桶,可能已經(jīng)忘了具體是什么時候開始不寫 web.xml 了,我只知道現(xiàn)在的項目已經(jīng)再也看不到它了,spring 又是如何支持 servlet3.0 規(guī)范的呢?

尋找 spring 中 ServletContainerInitializer 的實現(xiàn)類并不困難,可以迅速定位到 SpringServletContainerInitializer 該實現(xiàn)類。

@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) throws ServletException {

        Listinitializers = new LinkedList(); if (webAppInitializerClasses != null) { for (Class waiClass : webAppInitializerClasses) { // Be defensive: Some servlet containers provide us with invalid classes, // no matter what @HandlesTypes says... // 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); // 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 的流程來說非常重要。

英文注釋是 spring 源碼中自帶的,它提示我們由于 servlet 廠商實現(xiàn)的差異,onStartup 方法會加載我們本不想處理的 class,所以進行了特判。

spring 與我們之前的 demo 不同,并沒有在 SpringServletContainerInitializer 中直接對 servlet 和 filter 進行注冊,而是委托給了一個陌生的類 WebApplicationInitializer ,WebApplicationInitializer 類便是 spring 用來初始化 web 環(huán)境的委托者類,它通常有三個實現(xiàn)類:

Spring揭秘--尋找遺失的web.xml WebApplicationInitializer

你一定不會對 dispatcherServlet 感到陌生,AbstractDispatcherServletInitializer#registerDispatcherServlet 便是無 web.xml 前提下創(chuàng)建 dispatcherServlet 的關鍵代碼。

可以去項目中尋找一下 org.springframework:spring-web:version 的依賴,它下面就存在一個 servletContainerInitializer 的擴展,指向了 SpringServletContainerInitializer,這樣只要在 servlet3.0 環(huán)境下部署,spring 便可以自動加載進行初始化:

Spring揭秘--尋找遺失的web.xml SpringServletContainerInitializer

注意,上述這一切特性從 spring 3 就已經(jīng)存在了,而如今 spring 5 已經(jīng)伴隨 springboot 2.0 一起發(fā)行了。

SpringBoot 如何加載 Servlet?

讀到這兒,你已經(jīng)閱讀了全文的 1/2。springboot 對于 servlet 的處理才是重頭戲,其一,是因為 springboot 使用范圍很廣,很少有人用 spring 而不用 springboot 了;其二,是因為它沒有完全遵守 servlet3.0 的規(guī)范!

是的,前面所講述的 servlet 的規(guī)范,無論是 web.xml 中的配置,還是 servlet3.0 中的 ServletContainerInitializer 和 springboot 的加載流程都沒有太大的關聯(lián)。按照慣例,先賣個關子,先看看如何在 springboot 中注冊 servlet 和 filter,再來解釋下 springboot 的獨特之處。

注冊方式一:servlet3.0注解+@ServletComponentScan

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,可以采用這樣的方式,比較直觀。

注冊方式二:RegistrationBean

@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 容器的注冊。這種注冊方式也值得推崇。

Spring揭秘--尋找遺失的web.xml RegistrationBean

從圖中可以看出 RegistrationBean 的地位,它的幾個實現(xiàn)類作用分別是:幫助容器注冊 filter,servlet,listener,最后的 DelegatingFilterProxyRegistrationBean 使用的不多,但熟悉 SpringSecurity 的朋友不會感到陌生,SpringSecurityFilterChain 就是通過這個代理類來調(diào)用的。另外 RegistrationBean 實現(xiàn)了 ServletContextInitializer 接口,這個接口將會是下面分析的核心接口,大家先混個眼熟,了解下它有一個抽象實現(xiàn) RegistrationBean 即可。

SpringBoot中servlet加載流程的源碼分析

暫時只介紹這兩種方式,下面解釋下之前賣的關子,為什么說 springboot 沒有完全遵守 servlet3.0 規(guī)范。討論的前提是 springboot 環(huán)境下使用內(nèi)嵌的容器,比如最典型的 tomcat。高能預警,以下內(nèi)容比較燒腦,覺得看起來吃力的朋友可以跳過本節(jié)直接看下一節(jié)的總結(jié)!

Initializer被替換為TomcatStarter

當使用內(nèi)嵌的 tomcat 時,你會發(fā)現(xiàn) springboot 完全走了另一套初始化流程,完全沒有使用前面提到的 SpringServletContainerInitializer,實際上一開始我在各種 ServletContainerInitializer 的實現(xiàn)類中打了斷點,最終定位到,根本沒有運行到 SpringServletContainerInitializer 內(nèi)部,而是進入了 TomcatStarter 這個類中。

Spring揭秘--尋找遺失的web.xml TomcatStarter

并且,仔細掃了一眼源碼的包,并沒有發(fā)現(xiàn)有 SPI 文件對應到 TomcatStarter。于是我猜想,內(nèi)嵌 tomcat 的加載可能不依賴于 servlet3.0 規(guī)范和 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 ajavax.servlet.ServletContainerInitializerfor the WAR that is not executed when you runjava -jar.

See theorg.springframework.boot.context.embedded.ServletContextInitializerfor an option that works with Spring Beans.

springboot 這么做是有意而為之。springboot 考慮到了如下的問題,我們在使用 springboot 時,開發(fā)階段一般都是使用內(nèi)嵌 tomcat 容器,但部署時卻存在兩種選擇:一種是打成 jar 包,使用 java -jar 的方式運行;另一種是打成 war 包,交給外置容器去運行。前者就會導致容器搜索算法出現(xiàn)問題,因為這是 jar 包的運行策略,不會按照 servlet3.0 的策略去加載 ServletContainerInitializer!最后作者還提供了一個替代選項:ServletContextInitializer,注意是 ServletContextInitializer!它和 ServletContainerInitializer 長得特別像,別搞混淆了,前者 ServletContextInitializer 是 org.springframework.boot.web.servlet.ServletContextInitializer,后者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer,前文還提到 RegistrationBean 實現(xiàn)了 ServletContextInitializer 接口。

TomcatStarter中的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> classes, ServletContext servletContext) throws ServletException { for (ServletContextInitializer initializer : this.initializers) {
            initializer.onStartup(servletContext);
         }
   }
}

經(jīng)過刪減源碼后,可以看出 TomcatStarter 的主要邏輯,它其實就是負責調(diào)用一系列 ServletContextInitializer 的 onStartup 方法,那么在 debug 中,ServletContextInitializer[] initializers 到底包含了哪些類呢?會不會有我們前面介紹的 RegisterBean 呢?

Spring揭秘--尋找遺失的web.xml initializers

太天真了,RegisterBean 并沒有出現(xiàn)在 TomcatStarter 的 debug 信息中,initializers 只包含了三個類,其中只有第一個類看上去比較核心,注意第一個類不是 EmbeddedWebApplicationContext!而是這個類中的 $1 匿名類,為了搞清楚 springboot 如何加載 filter servlet listener ,看來還得研究下 EmbeddedWebApplicationContext 的結(jié)構(gòu)。

EmbeddedWebApplicationContext中的6層迭代加載

ApplicationContext 大家應該是比較熟悉的,這是 spring 一個比較核心的類,一般我們可以從中獲取到那些注冊在容器中的托管 Bean,而這篇文章,主要分析的便是它在內(nèi)嵌容器中的實現(xiàn)類:EmbeddedWebApplicationContext,重點分析它加載 filter servlet listener 這部分的代碼。這里是整個代碼中迭代層次最深的部分,做好心理準備起航,來看看 EmbeddedWebApplicationContext 是怎么獲取到所有的 servlet filter listener 的!以下方法均出自于 EmbeddedWebApplicationContext。

第一層:onRefresh()

onRefresh 是 ApplicationContext 的生命周期方法,EmbeddedWebApplicationContext 的實現(xiàn)非常簡單,只干了一件事:

@Override protected void onRefresh() { super.onRefresh(); try {
      createEmbeddedServletContainer();//第二層的入口 } catch (Throwable ex) { throw new ApplicationContextException("Unable to start embedded container",
            ex);
   }
}

createEmbeddedServletContainer 連接到了第二層

第二層:createEmbeddedServletContainer()

看名字 spring 是想創(chuàng)建一個內(nèi)嵌的 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 就是出現(xiàn)在 EmbeddedWebApplicationContext 中的一個匿名類,沒錯了,就是這里的 getSelfInitializer() 方法創(chuàng)建的!解釋下這里的 getSelfInitializer() 和 selfInitialize(ServletContext servletContext) 為什么要這么設計:這是典型的回調(diào)式方式,當匿名 ServletContextInitializer 類被 TomcatStarter 的 onStartup 方法調(diào)用,設計上是觸發(fā)了 selfInitialize(ServletContext servletContext) 的調(diào)用。所以這下就清晰了,為什么 TomcatStarter 中沒有出現(xiàn) RegisterBean ,其實是隱式觸發(fā)了 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 CollectiongetServletContextInitializerBeans() { return new ServletContextInitializerBeans(getBeanFactory());//第五層的入口 }

沒錯了,注釋都告訴我們,這個 ServletContextInitializerBeans 是用來加載 Servlet 和 Filter 的。

第五層:ServletContextInitializerBeans的構(gòu)造方法

public ServletContextInitializerBeans(ListableBeanFactory beanFactory) { this.initializers = new LinkedMultiValueMap, ServletContextInitializer>();
   addServletContextInitializerBeans(beanFactory);// 第六層的入口 addAdaptableBeans(beanFactory);
   ListsortedInitializers = new ArrayList(); for (Map.Entry 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 initializerBean : getOrderedBeansOfType(
         beanFactory, ServletContextInitializer.class)) {
      addServletContextInitializerBean(initializerBean.getKey(),
            initializerBean.getValue(), beanFactory);
   }
}

getOrderedBeansOfType 方法便是去容器中尋找注冊過得 ServletContextInitializer ,這時候就可以把之前那些 RegisterBean 全部加載出來了,并且 RegisterBean 還實現(xiàn)了 Ordered 接口,在這兒用于排序。不再往下迭代了。

EmbeddedWebApplicationContext加載流程總結(jié)

如果你對具體的代碼流程不感興趣,可以跳過上述的6層分析,直接看本節(jié)的結(jié)論??偨Y(jié)如下:

  • EmbeddedWebApplicationContext 的 onRefresh 方法觸發(fā)配置了一個匿名的 ServletContextInitializer。

  • 這個匿名的 ServletContextInitializer 的 onStartup 方法會去容器中搜索到了所有的 RegisterBean 并按照順序加載到 ServletContext 中。

  • 這個匿名的 ServletContextInitializer 最終傳遞給 TomcatStarter,由 TomcatStarter 的 onStartup 方法去觸發(fā) ServletContextInitializer 的 onStartup 方法,最終完成裝配!

Spring揭秘--尋找遺失的web.xml getServletContextInitializerBeans

第三種注冊 Servlet 的方式

研究完了上述 springboot 啟動的內(nèi)部原理,可以發(fā)現(xiàn) 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("創(chuàng)建 helloWorldServlet...");

        ServletRegistration.Dynamic servlet = servletContext.addServlet(
                HelloWorldServlet.class.getSimpleName(),
                HelloWorldServlet.class);
        servlet.addMapping(JAR_HELLO_URL);

        System.out.println("創(chuàng)建 helloWorldFilter...");

        FilterRegistration.Dynamic filter = servletContext.addFilter(
                HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);

        EnumSetdispatcherTypes = EnumSet.allOf(DispatcherType.class);
        dispatcherTypes.add(DispatcherType.REQUEST);
        dispatcherTypes.add(DispatcherType.FORWARD);

        filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
    }
}

雖然 ServletCantainerInitializer 不能被內(nèi)嵌容器加載,ServletContextInitializer 卻能被 springboot 的 EmbeddedWebApplicationContext 加載到,從而裝配其中的 servlet 和 filter。實際開發(fā)中,還是以一,二兩種方法來注冊為主,這里只是提供一個可能性,來讓我們理解 springboot 的加載流程。

加載流程拾遺

  1. TomcatStarter 既然不是通過 SPI 機制裝配的,那是怎么被 spring 使用的?

自然是被 new 出來的,在 TomcatEmbeddedServletContainerFactory#configureContext 中可以看到,TomcatStarter 是被主動實例化出來的,并且還傳入了 ServletContextInitializer 的數(shù)組,和上面分析的一樣,一共有三個 ServletContextInitializer,包含了 EmbeddedWebApplicationContext 中的匿名實現(xiàn)。

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);
   ...
   }
}
  1. TomcatEmbeddedServletContainerFactory 又是如何被聲明的?

@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 環(huán)境下,就會觸發(fā) springboot 的自動配置。

總結(jié)

存在 web.xml 配置的 java web 項目,servlet3.0 的 java web 項目,springboot 內(nèi)嵌容器的 java web 項目加載 servlet,filter,listener 的流程都是有所差異的,理解清楚這其中的原來,其實并不容易,至少得搞懂 servlet3.0 的規(guī)范,springboot 內(nèi)嵌容器的加載流程等等前置邏輯。

最后感謝下小馬哥的點撥,在此之前誤以為: TomcatStarter 既然繼承了 ServletContainerInitializer,應該也是符合 servlet3.0 規(guī)范的,但實際上并沒有被 SPI 加載。


免責聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉
關閉