1. 程式人生 > 實用技巧 >ServletContainerInitializer深入剖析

ServletContainerInitializer深入剖析

ServletContainerInitializer深入剖析

思考:

ServletContainerInitizalizer是用來註冊那些動態生成的servlet、listener、filter或沒配置在web.xml裡或jar包下的servlet等嗎?

官方文件:

https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContainerInitializer.html

案例專案地址:

https://gitee.com/LiuDaiHua/servlet3.0

瞭解

文件註釋

ServletContainerInitializer註釋(自譯文)

onStartup註釋

HandlesTypes註解註釋

Servlet3.0下大背景

如何使用ServletContainerInitializer

題外話

實戰

最後一個疑問

瞭解

文件註釋

public interface ServletContainerInitializer {

    /**
     * Receives notification during startup of a web application of the classes
     * within the web application that matched the criteria defined via the
     * {
@link javax.servlet.annotation.HandlesTypes} annotation. * * @param c The (possibly null) set of classes that met the specified * criteria * @param ctx The of the web application in which the * classes were discovered * * @throws ServletException If an error occurs
*/ void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException; }

ServletContainerInitializer註釋(自譯文)

該介面將在Web應用程式的啟動階段通知 ”庫/執行時“,並允許這些”庫或執行時“ 在啟動階段動態註冊WEB三大元件

可以使用HandlesTypes對該介面的實現進行註釋,該註解只有一個value屬性,該屬性接收的值是類陣列,你可以使用該註解將需要進行擴充套件的應用程式集傳遞進來,這些傳遞進來的應用程式集將在onStartup方法的第一個引數中被接收,你可以在onStartup中對進行一些操作。例如像SpringMVC那樣。

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

當然如果該介面的實現沒有使用HandlesTypes註解,或者沒有一個應用程式類與註解中指定的類相匹配,那麼容器會將一個空的類集傳遞給onStartup的第一個引數。當在檢查應用程式類中是否存在該註解指定的類時,如果沒找到,容器可能會遇到類載入問題。

該介面的實現必須在JAR檔案中META-INF/services目錄中宣告,並以此介面的全限定類名稱命名。例如:

在容器啟動時將使用執行時服務發現機制(SPI)或語義上等同於SPI機制的特定於容器的機制來發現,無論是哪種情況,被排除在絕對順序之外的 WEB片段JAR檔案中的ServletContainerInitializer服務 必須被忽略,並且發現這些服務的順序必須遵循應用程式的類載入委託模型。

可以通過WEB片段排序來控制每個JAR檔案的ServletContainerIntializer服務執行順序。如果定義了絕對排序,那麼只有包含在排序中的jar才會被處理。要完全禁用該服務,可以定義一個空的絕對順序。

思考:這個絕對排序在哪定義?如果有多個JAR都實現了ServletContainerInitializer先載入誰的?

onStartup註釋

ServletContainerInitalizer只有一個方法onStartup

它有兩個引數:

第一個引數:滿足指定條件的類集(可能為空)。

第二個引數:web應用程式的ServletContext。

這個方法的註釋如下:

通知這個ServletContainerInitializer啟動。將傳入表示應用程式的ServletContext。

如果這個ServletContainerInitializer的實現被繫結到應用程式的WEB-INF/lib目錄的JAR檔案中【意思是你在自己的JAR檔案裡META-INF/service/javax.servlet.ServletContainerInitializer中宣告的ServletContainerInitializer的實現在自己的JAR檔案中】,那麼它的onStartup方法只會在繫結應用程式啟動期間被呼叫一次。如果這個ServletContainerInitializer的實現繫結在任何WEB-INF/lib目錄之外的JAR檔案中【意思是你在自己的JAR檔案裡宣告的ServletContainerInitializer的實現在不在自己的JAR檔案中】,但是仍然像上面描述的那樣可以發現,那麼每次啟動應用程式時都會呼叫它的onStartup方法。

HandlesTypes註解註釋

在來看一下HandlesTypes註解註釋:

這個註釋用於宣告ServletContainerInitializer可以處理的類型別。value值表示一個應用程式類陣列,這些應用程式類將被傳遞給ServletContainerInitializer。

Servlet3.0下大背景

要知道為什麼出現ServletContainerInitializer,你需要了解Servlet3.0到底發生了什麼,因為這是一個Servlet3.0的介面。在Servlet3.0中一個重要的特徵就是允許在容器啟動的時候動態註冊WEB三大元件,而在Servlet3.0文章中我們是使用監聽器動態註冊WEB三大元件的。但是如果我有一個web片段作為一個元件想提供其他人的專案使用,我們直接使用註解的方式註冊能不能被其他人的專案掃描到呢?

如下:

假設我現在某一個web片段專案servlet-container-initializer-one,在這個專案(使用Servlet4.0)裡,我只提供一個Servlet如下:

package com.wonders.servlet;

@WebServlet("/tankServlet")
public class TankServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("I'am Tank!");
    }
}

現在我將我的WEB專案打成一個JAR供他人使用。

假設A使用者要使用我的WEB片段專案,他自己的WEB專案中只有一個HelloServlet如下:

@WebServlet("/HelloServlet")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("123");
    }
}

現在A使用者要使用我的WEB片段,他需要在他專案的WEB-INF/lib目錄下引入我的JAR即可【Servlet3.0新特性之元件可插性】,專案結構如下圖:

現在測試,啟動servlet-container-initializer-test看看能不能訪問我WEB片段專案裡的tankServlet。

啟動資訊裡說掃描到了這個JAR,先訪問A使用者自己的Servlet:

如上圖是沒問題的,在訪問我的WEB片段中的servlet:

可以看到也是沒問題的,也就是說,只要我的WEB片段裡使用註解方式註冊的(或者是在web-fragment.xml裡註冊的,這兩個本質是一樣的),都會被掃描到,並加入容器之中,不管是註解方式還是在web-fragment.xml宣告方式這兩種註冊WEB三大元件都稱不上是動態註冊,因為它們是執行時就已經存在了的,動態註冊指的是那些執行時不存在,在執行時加入的WEB元件。對於執行時已經存在的只要把我的JAR包放入classpath下使用者A就必須載入我JAR裡註冊的三大元件【強迫性讓容器載入所有執行時已存在的WEB三大元件】。如果我不能使用註解和web-fragment.xml這兩種方式提前寫死,那怎麼辦?

Servlet3.0也早已考慮到這種情況,既然必須在執行時動態註冊,那Servlet3.0就提供了動態註冊WEB三大元件的方式,動態註冊WEB三大元件的前提是必須在啟動時註冊。在Servlet3.0文章中我們是使用一個監聽器去動態註冊我們的WEB三大元件的,使用監聽器監聽容器啟動事件,然後在註冊所需的WEB元件或執行其他事情。到此貌似我們根本不需要這個ServletContainerInitializer,因為使用監聽器就完全也可以做到Web應用程式的啟動階段通知 ”庫/執行時“,並允許這些”庫或執行時“ 在啟動階段動態註冊WEB三大元件。那為什麼Servlet設計者們又專門設計一個ServletContainerInitializer呢?而且還專門把它限定在JAR檔案中使用?

到目前理解來看,我想大概是為了使整個軟體的設計更加靈活吧,使用這麼一個介面的好處是:在一個統一的入口處動態的註冊WEB元件以及其他相關任務,避免了在xml中書寫的煩雜,又避免了都使用註解時對開發者來說難以一眼概覽整個專案,不使用Listener大概是因為解耦和單一職責。

如何使用ServletContainerInitializer

題外話

你要知道ServletContainerInitializer是一個規範,它規定了容器啟動的時候必須掃描JAR包裡去查詢該介面的實現並執行該實現的onStartUp方法。也就是說像Tomcat、WebLogic、Jetty等這些servlet容器必須遵循這一規範。這個規範一邊約定容器的開發商必須這麼幹,還約定開發人員必須在JAR檔案的META-INF/service下建立javax.servlet.ServletContainerInitializer檔案並在該檔案內寫上對該介面實現類的全限定名稱。這樣兩邊都對起來後,你使用Tomcat容器啟動你的WEB專案時你lib目錄下的JAR檔案裡該介面的實現才會被發現,然後tomcat會執行該實現類的onStartup方法。

實戰

1、在剛才建立的servlet-container-initializer-one專案中先建立一個等待被動態註冊的Servlet(注意它沒有加註解,也沒在web-fragment.xml中宣告),我們馬上要在ServletContainerInitializer的實現類的動態註冊它:

package com.wonders.servlet;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("one-MyServlet");
    }
}

2、然後建立ServletContainerInitializer的實現類:

package com.wonders;

@HandlesTypes({})
public class OneContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        System.out.println("OneContainerInitializer被呼叫了");
        ServletRegistration.Dynamic dynamic = ctx.addServlet("my-servlet", "com.wonders.servlet.MyServlet");
        dynamic.addMapping("/myServlet");
    }
}

在這個HandlesTypes中我們沒有傳入任何類。

3、然後開始重要的一步,建立META-INF/services/javax.servlet.ServletContainerInitializer檔案:

注意是在src目錄下,在該檔案內寫上對該介面的實現類的全限定名稱:

com.wonders.OneContainerInitializer

4、打成JAR包,File -> Project Structure -> Artifacts -> + -> 選中JAR -> From Module ... -> 選中我們的servlet-container-initializer-one -> ok

注意在Output Layout列表下如果有很多和該jar包的依賴,但是在你將要匯入的WEB專案裡這些依賴已經有了比如說servlet-api.jar,jsp-api.jar等你可以刪掉,然後點選Applay。

點選Build -> Build Artifacts -> 選中servlet-container-initializer:jar -> build即可。然後到剛才我們配置的Output directory目錄下找這個JAR。

如上我匯入到servlet3-java裡了,找到這個jar包

5、在需要匯入該JAR的WEB專案中建立WEB-INF/lib,將該jar包放入lib目錄下,我的WEB專案是servlet-container-initializer-test如下:

6、啟動servlet-container-initializer-test檢視控制檯是否輸出我們OneContainerInitializer實現類onStartup裡列印的那一句話:

雖然字元編碼不對,但是打印出來就說明tomcat容器遵循了ServletContainerInitializer規範把我們的實現類OneContainerInitializer的onStartup執行了。

7、訪問看看我們在onStartup方法裡動態註冊MyServlet能不能被訪問:

可以看到能成功訪問,說明我們動態註冊的Servlet起效了。上圖中我是使用Servlet4.0+Tomcat的HTTP/2協議,所以時https,埠是8443,如果不是在tomcat中配置該協議,請使用http和8080埠訪問。

最後一個疑問

在上面文件註釋章節丟擲了一個疑問:

思考:這個絕對排序在哪定義?如果有多個JAR都實現了ServletContainerInitializer先載入誰的?

為了探索這個疑問,我又建立了一個WEB片段專案servlet-container-initializer-two

其大致內容和servlet-container-initializer-one類似,我們這次主要探索排序問題,將其也打成JAR包丟到servlet-container-initializer-test的lib目錄下,新建立的JAR排序在servlet-container-initializer-one.jar之後。啟動Tomcat,控制檯得到如下圖結果:

繼續在建立了一個WEB片段專案,這次建立的專案名稱打成的JAR要排序在lib目錄下的servlet-container-initializer-one之前,所以我們建立servlet-container-initializer-before-one,內容也類似servlet-container-initializer-one,最後打成JAR包丟到lib目錄下,排序結果如下圖:

啟動Tomcat容器,控制檯列印結果如下:

由於我沒找到官方給的相關這個絕對排序的說明(這個絕對排序有可能需要在哪裡配置),不過我現在可以大膽的猜測到,如果我們沒有指定絕對排序,那麼預設排序載入ServletContainerInitializer實現類的規則是按照JAR包名稱載入順序進行載入的!

最後不甘心到此結束,自己嘗試在web.xml輸入order標籤發現還真有一個<absolute-ordering>標籤,一看這個名字我感覺自己肯定找對了,檢視該標籤可以發現它只有兩個子標籤name和other,然後檢視官方文件找關於該標籤的說明name指的是WEB片段專案web-fragment.xml中指定的name屬性,它指定了WEB片段的載入順序以及是否被載入。由於我之前建立的servlet-container-initializer-one、servlet-container-initializer-two、servlet-container-initializer-before-one這三個WEB片段專案都是省略了這個檔案(在Servlet3.0中新增了web-fragment.xml,但是可以像省略web.xml一樣省略該檔案)。那沒辦法了,我們在之前的三個WEB片段裡都指定一個web-fragment.xml吧,並給他們每一個都指定一個名字:分別叫one、tow、beforeOne

<?xml version="1.0" encoding="UTF-8"?>
<web-fragment id="WebFragment_ID" version="3.1" 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-fragment_3_1.xsd">
    <name>one</name>
</web-fragment>

重新打JAR包,然後丟到servlet-container-initializer-test的lib目錄下,並在servlet-container-initializer-test的WEB-INF目錄下新增web.xml,在其中配置:

<?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" metadata-complete="true">
    <absolute-ordering>
        <name>two</name>
        <name>one</name>
    </absolute-ordering>
</web-app>

這次我故意省略了beforeOne看它會不會被載入,然後故意調換了tow和one的載入順序,看看它們會不會載入順序發生變化。再次啟動Tomcat容器,得到如下結果輸出:

先載入了two後加載了one,沒有在web.xml指定絕對排序的JAR不會被載入,果然如上面文件註釋章節所訴。

至此一切真相大白,再回頭想Servlet的設計者們為什麼要建立ServletContainerInitializer現在也有點恍然大悟的感覺,SPI和API對比下來,SPI進行了解耦,而ServletContainerInitializer又是使用了PI機制的典型案例,當不需要引入這些動態註冊的WEB元件時只需刪除javax.servlet.ServletContainerInitializer即可,而不需要改變原有專案中的類,亦或許Servlet的設計者們考慮的更多。

關於ServletContainerInitializer的研究就先告一段落了。接下來將繼續回到SpringBoot之內嵌式Tomcat原始碼的研究中去了,回憶走過了路線:springboot-java8-servlet3.0-servlet4.0-ServletContainerInitializer,有疑問就去追尋答案,望能與讀者共勉。