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註釋(自譯文)
如何使用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先載入誰的?
ServletContainerInitalizer只有一個方法onStartup
通知這個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方法。
這個註釋用於宣告ServletContainerInitializer可以處理的類型別。value值表示一個應用程式類陣列,這些應用程式類將被傳遞給ServletContainerInitializer。
要知道為什麼出現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!"); } }
假設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片段裡使用註解方式註冊的(或者是在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"); } }
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目錄下,排序結果如下圖:
由於我沒找到官方給的相關這個絕對排序的說明(這個絕對排序有可能需要在哪裡配置),不過我現在可以大膽的猜測到,如果我們沒有指定絕對排序,那麼預設排序載入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,有疑問就去追尋答案,望能與讀者共勉。