天天看點

從servlet容器說起1 Servlet容器的啟動過程2 Web 應用的初始化工作

要介紹 Servlet 必須要先把 Servlet 容器說清楚,Servlet 與 Servlet 容器的關系有點像槍和子彈的關系,槍為彈而生,而彈又讓槍有了殺傷力。雖然它們是彼此依存的,但是又互相獨立發展,這一切都是為了适應工業化生産。從技術角度來說是為了解耦,通過标準化接口來互相協作。既然接口是連接配接 Servlet 與 Servlet 容器的關鍵,那我們就從它們的接口說起。

Servlet 容器作為一個獨立發展的标準化産品,目前種類很多,但是它們都有自己的市場定位,很難說誰優誰劣。以大家最為熟悉 Tomcat 為例來介紹 Servlet 容器如何管理 Servlet。Tomcat 本身也很複雜,我們隻從 Servlet 與 Servlet 容器的接口部分開始介紹,關于 Tomcat 的詳細介紹可以參考我的網上其他博文

在Tomcat 的容器等級中,Context 容器直接管理 Servlet 在容器中的包裝類 Wrapper,是以 Context 容器如何運作将直接影響 Servlet 的工作方式。

從servlet容器說起1 Servlet容器的啟動過程2 Web 應用的初始化工作

Tomcat容器模型

從上圖可以看出 Tomcat 的容器分為四個等級,真正管理 Servlet 的容器是 Context 容器,一個 Context 對應一個 Web 工程,在 Tomcat 的配置檔案中可以很容易發現這一點,如下:

從servlet容器說起1 Servlet容器的啟動過程2 Web 應用的初始化工作

Context.xml配置檔案内容

下面詳細介紹 Tomcat 解析 Context 容器的過程,包括如何建構 Servlet

1 Servlet容器的啟動過程

Tomcat7 開始支援嵌入式功能,增加了一個啟動類 org.apache.catalina.startup.Tomcat

建立一個執行個體對象并調用 start 方法就可以很容易啟動 Tomcat,我們還可以通過這個對象來增加和修改 Tomcat 的配置參數,如可以動态增加 Context、Servlet 等.

下面我們就利用這個 Tomcat 類來管理新增的一個 Context 容器,我們就選擇 Tomcat7 自帶的 examples Web 工程,并看看它是如何加到這個 Context 容器中的.

Tomcat tomcat = getTomcatInstance(); 
 File appDir = new File(getBuildDirectory(), "webapps/examples"); 
 tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); 
 tomcat.start(); 
 ByteChunk res = getUrl("http://localhost:" + getPort() + 
               "/examples/servlets/servlet/HelloWorldExample"); 
 assertTrue(res.toString().indexOf("<h1>Hello World!</h1>") > 0);
           

上段建立一個 Tomcat 執行個體并新增一個 Web 應用,然後啟動 Tomcat 并調用其中的一個 HelloWorldExample Servlet,看有沒有正确傳回預期的資料。

Tomcat 的 addWebapp 方法的代碼如下:

public Context addWebapp(Host host, String url, String path) { 
        silence(url); 
        Context ctx = new StandardContext(); 
        ctx.setPath( url ); 
        ctx.setDocBase(path); 
        if (defaultRealm == null) { 
            initSimpleAuth(); 
        } 
        ctx.setRealm(defaultRealm); 
        ctx.addLifecycleListener(new DefaultWebXmlListener()); 
        ContextConfig ctxCfg = new ContextConfig(); 
        ctx.addLifecycleListener(ctxCfg); 
        ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML"); 
        if (host == null) { 
            getHost().addChild(ctx); 
        } else { 
            host.addChild(ctx); 
        } 
        return ctx; 
 }
           
  • 前面已經介紹了一個 Web 應用對應一個 Context 容器,也就是 Servlet 運作時的 Servlet 容器,
  • 添加一個 Web 應用時将會建立一個 StandardContext 容器,并且給這個 Context 容器設定必要的參數,url 和 path 分别代表這個應用在 Tomcat 中的通路路徑和這個應用實際的實體路徑,這個兩個參數與清單 1 中的兩個參數是一緻的。
  • 其中最重要的一個配置是 ContextConfig,這個類将會負責整個 Web 應用配置的解析工作,後面将會詳細介紹。
  • 最後将這個 Context 容器加到父容器 Host 中。

接下去将會調用start 方法啟動 Tomcat,它的啟動邏輯基于觀察者模式,所有的容器都會繼承 Lifecycle 接口,它管理者容器的整個生命周期,所有容器的修改和狀态的改變都會由它去通知已經注冊的觀察者(Listener)

從servlet容器說起1 Servlet容器的啟動過程2 Web 應用的初始化工作

Tomcat 主要類的啟動時序圖

上圖描述了 Tomcat 啟動過程中,主要類之間的時序關系,下面我們将會重點關注添加 examples 應用所對應的 StandardContext 容器的啟動過程

當 Context 容器初始化狀态設為 init 時,添加在 Contex 容器的 Listener 将會被調用。

ContextConfig 繼承了 LifecycleListener 接口,它是在調用Tomcat 的 addWebapp 方法時被加入到 StandardContext 容器中的。

ContextConfig 類會負責整個 Web 應用的配置檔案的解析工作。

ContextConfig 的 init 方法将會主要完成以下工作

  • 建立用于解析 xml 配置檔案的 contextDigester 對象
  • 讀取預設 context.xml 配置檔案,如果存在則解析它
  • 讀取預設的 Host 配置檔案,如果存在則解析它
  • 讀取預設的 Context 自身的配置檔案,如果存在則解析它
  • 設定 Context 的 DocBase

ContextConfig 的 init 方法完成後,Context 容器就會執行 startInternal 方法

  • 建立讀取資源檔案的對象
  • 建立 ClassLoader 對象
  • 設定應用的工作目錄
  • 啟動相關的輔助類如:logger、resources
  • 修改啟動狀态,通知感興趣的觀察者(Web 應用的配置)
  • 子容器的初始化
  • 擷取 ServletContext 并設定必要的參數
  • 初始化“load on startup”的 Servlet

2 Web 應用的初始化工作

在 ContextConfig 的 configureStart 方法中實作的,

應用的初始化主要是解析 web.xml 檔案,這個檔案描述了Web 應用的關鍵資訊,也是一個 Web 應用的入口。

Tomcat 首先會找 globalWebXml,這個檔案的搜尋路徑是在 engine 的工作目錄下尋找以下兩個檔案中的任一個 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。

接着會找 hostWebXml 這個檔案可能會在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,

接着尋找應用的配置檔案 examples/WEB-INF/web.xml

web.xml 檔案中的各個配置項将會被解析成相應的屬性儲存在 WebXml 對象中。

如果目前應用支援 Servlet3.0,解析還将完成額外 9 項工作,這個額外的 9 項工作主要是為 Servlet3.0 新增的特性,包括 jar 包中的 META-INF/web-fragment.xml 的解析以及對 annotations 的支援。

接下去将會将 WebXml 對象中的屬性設定到 Context 容器中,這裡包括建立 Servlet 對象、filter、listener 等。

這段代碼在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的代碼片段

for (ServletDef servlet : servlets.values()) { 
            Wrapper wrapper = context.createWrapper(); 
            String jspFile = servlet.getJspFile(); 
            if (jspFile != null) { 
                wrapper.setJspFile(jspFile); 
            } 
            if (servlet.getLoadOnStartup() != null) { 
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); 
            } 
            if (servlet.getEnabled() != null) { 
                wrapper.setEnabled(servlet.getEnabled().booleanValue()); 
            } 
            wrapper.setName(servlet.getServletName()); 
            Map<String,String> params = servlet.getParameterMap(); 
            for (Entry<String, String> entry : params.entrySet()) { 
                wrapper.addInitParameter(entry.getKey(), entry.getValue()); 
            } 
            wrapper.setRunAs(servlet.getRunAs()); 
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); 
            for (SecurityRoleRef roleRef : roleRefs) { 
                wrapper.addSecurityReference( 
                        roleRef.getName(), roleRef.getLink()); 
            } 
            wrapper.setServletClass(servlet.getServletClass()); 
            MultipartDef multipartdef = servlet.getMultipartDef(); 
            if (multipartdef != null) { 
                if (multipartdef.getMaxFileSize() != null && 
                        multipartdef.getMaxRequestSize()!= null && 
                        multipartdef.getFileSizeThreshold() != null) { 
                    wrapper.setMultipartConfigElement(new 
 MultipartConfigElement( 
                            multipartdef.getLocation(), 
                            Long.parseLong(multipartdef.getMaxFileSize()), 
                            Long.parseLong(multipartdef.getMaxRequestSize()), 
                            Integer.parseInt( 
                                    multipartdef.getFileSizeThreshold()))); 
                } else { 
                    wrapper.setMultipartConfigElement(new 
 MultipartConfigElement( 
                            multipartdef.getLocation())); 
                } 
            } 
            if (servlet.getAsyncSupported() != null) { 
                wrapper.setAsyncSupported( 
                        servlet.getAsyncSupported().booleanValue()); 
            } 
            context.addChild(wrapper); 
 }
           

這段代碼描述了如何将 Servlet 包裝成 Context 容器中的 StandardWrapper,為什麼要将Servlet包裝成StandardWrapper 而不直接是 Servlet 對象呢?StandardWrapper 是 Tomcat 容器的一部分,它具有容器的特征,而 Servlet 作為一個獨立的 web 開發标準,不應該強耦合在 Tomcat 中

除了将 Servlet 包裝成 StandardWrapper 并作為子容器添加到 Context 中,其它的所有 web.xml 屬性都被解析到 Context 中,是以說 Context 容器才是真正運作 Servlet 的 Servlet 容器。

一個 Web 應用對應一個 Context 容器,容器的配置屬性由應用的 web.xml 指定,這樣我們就能了解 web.xml 到底起到什麼作用了。

繼續閱讀