1. 程式人生 > >Servlet工作原理解析(tomcat7、嵌入式服務器)

Servlet工作原理解析(tomcat7、嵌入式服務器)

現在 coyote lis 訪問 運行時 com 嵌入式服務器 循環 session

目錄

  • Servlet 容器Tomcat
    • Servlet 容器的啟動過程
    • Web 應用的初始化工作
  • Servlet 體系結構
    • 創建 Servlet 對象(如何被加載)
    • 初始化 Servlet(如何被初始化)
    • Servlet 如何工作(如何被調用)
    • Servlet 中的 Listener
    • Servlet 中的 Filter
    • Servlet 中的 url-pattern


Web 技術成為當今主流的互聯網 Web 應用技術之一,而 Servlet 是 Java Web 技術的核心基礎。要介紹 Servlet 必須要先把 Servlet 容器說清楚,Servlet 與 Servlet 容器的關系有點像槍和子彈的關系,槍是為子彈而生,而子彈又讓槍有了殺傷力。雖然它們是彼此依存的,但是又相互獨立發展,這一切都是為了適應工業化生產的結果。從技術角度來說是為了解耦,通過標準化接口來相互協作。

Servlet 容器作為一個獨立發展的標準化產品,目前它的種類很多,但是它們都有自己的市場定位,很難說誰優誰劣,各有特點。例如現在比較流行的 Jetty,在定制化和移動領域有不錯的發展,這裏還是以大家最為熟悉 Tomcat 為例來介紹 Servlet 容器如何管理 Servlet。

回到頂部

Servlet 容器Tomcat

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

技術分享圖片

Tomcat 的容器分為四個等級,真正管理 Servlet 的容器是 Context 容器,一個 Context 對應一個 Web 工程。

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 應用時將會創建一個 StandardContext 容器,並且給這個 Context 容器設置必要的參數,url 和 path 分別代表這個應用在 Tomcat 中的訪問路徑和這個應用實際的物理路徑。其中最重要的一個配置是 ContextConfig,這個類將會負責整個 Web 應用配置的解析工作(web.xml)。最後將這個 Context 容器加到父容器 Host 中。

Tomcat 的啟動邏輯是基於觀察者模式設計的,所有的容器都會繼承 Lifecycle 接口,它管理者容器的整個生命周期,所有容器的的修改和狀態的改變都會由它去通知已經註冊的觀察者(Listener)。

當 Context 容器初始化狀態設為 init 時,添加在 Context 容器的 Listener 將會被調用。ContextConfig 繼承了 LifecycleListener 接口,它是在調用上面的代碼 時被加入到 StandardContext 容器中。ContextConfig 類會負責整個 Web 應用的配置文件的解析工作。

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

  1. 創建用於解析 xml 配置文件的 contextDigester 對象
  2. 讀取默認 context.xml 配置文件,如果存在解析它
  3. 讀取默認 Host 配置文件,如果存在解析它
  4. 讀取默認 Context 自身的配置文件,如果存在解析它
  5. 設置 Context 的 DocBase

ContextConfig 的 init 方法完成後,Context 容器的會執行 startInternal 方法,這個方法啟動邏輯比較復雜,主要包括如下幾個部分:

  1. 創建讀取資源文件的對象
  2. 創建 ClassLoader 對象
  3. 設置應用的工作目錄
  4. 啟動相關的輔助類如:logger、realm、resources 等
  5. 修改啟動狀態,通知感興趣的觀察者(Web 應用的配置)
  6. 子容器的初始化
  7. 獲取 ServletContext 並設置必要的參數
  8. 初始化“load on startup”的 Servlet

Web 應用的初始化工作

Web 應用的初始化工作是在 ContextConfig 的 configureStart 方法中實現的,應用的初始化主要是要解析 web.xml 文件,這個文件描述了一個 Web 應用的關鍵信息,也是一個 Web 應用的入口。

Tomcat 如何找到web.xml文件

  1. 首先會找 globalWebXml ,這個文件的搜索路徑是在 engine 的工作目錄下尋找以下兩個文件中的任一個 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。
  2. 接著會找 hostWebXml ,這個文件可能會在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,
  3. 接著尋找應用的配置文件 examples/WEB-INF/web.xml。web.xml 文件中的各個配置項將會被解析成相應的屬性保存在 WebXml 對象中。
  4. 如果當前應用支持 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 指定。

回到頂部

Servlet 體系結構

技術分享圖片

從上圖可以看出 Servlet 規範就是基於這幾個類運轉的,與 Servlet 主動關聯的是三個類,分別是 ServletConfig、ServletRequest 和 ServletResponse。這三個類都是通過容器傳遞給 Servlet 的,其中 ServletConfig(StandardWrapperFacade) 是在 Servlet 初始化時就傳給 Servlet 了,而後兩個是在請求達到時調用 Servlet 時傳遞過來的。查看 ServletConfig 接口中聲明的方法發現,這些方法都是為了獲取這個 Servlet 的一些配置屬性,而這些配置屬性可能在 Servlet 運行時被用到。而 ServletContext(ApplicationContextFacade) 又是幹什麽的呢? Servlet 的運行模式是一個典型的“握手型的交互式”運行模式。所謂“握手型的交互式”就是兩個模塊為了交換數據通常都會準備一個交易場景,這個場景一直跟隨個這個交易過程直到這個交易完成為止。這個交易場景的初始化是根據這次交易對象指定的參數來定制的,這些指定參數通常就會是一個配置類。所以對號入座,交易場景就由 ServletContext 來描述,而定制的參數集合就由 ServletConfig 來描述,而 ServletRequest 和 ServletResponse 就是要交互的具體對象了。

ServletContext和ServletConfig 到底是個什麽對象呢?

StandardWrapper 和 StandardWrapperFacade 都實現了 ServletConfig 接口,傳給 Servlet 的是 StandardWrapperFacade 對象,這個類能夠保證從 StandardWrapper 中拿到 ServletConfig 所規定的數據,而又不把 ServletConfig 不關心的數據暴露給 Servlet。

ApplicationContext和ApplicationContextFacade都實現了ServletContext接口,傳給 Servlet 的是 ApplicationContextFacade 對象。這個類同樣保證 ServletContext 只能從容器中拿到它該拿的數據,它們都起到對數據的封裝作用,它們使用的都是門面設計模式。通過 ServletContext 可以拿到 Context 容器中一些必要信息,比如應用的工作路徑,容器支持的 Servlet 最小版本等。

ServletRequest 和 ServletResponse 到底是個什麽對象呢?

我們在創建自己的 Servlet 類時通常使用的都是 HttpServletRequest 和 HttpServletResponse,它們繼承了 ServletRequest 和 ServletResponse。

Tomcat 一接受到請求首先將會創建 org.apache.coyote.Request 和 org.apache.coyote.Response,這兩個類是 Tomcat 內部使用的描述一次請求和相應的信息類它們是一個輕量級的類,它們作用就是在服務器接收到請求後,經過簡單解析將這個請求快速的分配給後續線程去處理,所以它們的對象很小,很容易被 JVM 回收。接下去當交給一個用戶線程去處理這個請求時又創建 org.apache.catalina.connector. Request 和 org.apache.catalina.connector. Response 對象。這兩個對象一直穿越整個 Servlet 容器直到要傳給 Servlet,傳給 Servlet 的是 Request 和 Response 的門面類 RequestFacade 和 RequestFacade,這裏使用門面模式與前面一樣都是基於同樣的目的——封裝容器中的數據。一次請求對應的 Request 和 Response 的類轉化如下圖所示:

技術分享圖片

創建 Servlet 對象(如何被加載)

如果 Servlet 的 load-on-startup 配置項大於 0,那麽在 Context 容器啟動的時候就會被實例化,前面提到在解析配置文件時會讀取默認的 globalWebXml,在 conf 下的 web.xml 文件中定義了一些默認的配置項,其定義了兩個 Servlet,分別是:org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet 。它們的 load-on-startup 分別是 1 和 3,也就是當 Tomcat 啟動時這兩個 Servlet 就會被啟動。

創建 Servlet 實例的方法是從 Wrapper. loadServlet 開始的。loadServlet 方法要完成的就是獲取 servletClass 然後把它交給 InstanceManager去創建一個基於 servletClass.class 的對象。如果這個 Servlet 配置了 jsp-file,那麽這個 servletClass 就是 conf/web.xml 中定義的org.apache.jasper.servlet.JspServlet 了。

初始化 Servlet(如何被初始化)

初始化 Servlet 在 StandardWrapper 的 initServlet 方法中,這個方法很簡單就是調用 Servlet 的 init 的方法,同時把包裝了 StandardWrapper 對象的 StandardWrapperFacade 作為 ServletConfig 傳給 Servlet。

如果該 Servlet 關聯的是一個 jsp 文件,那麽前面初始化的就是 JspServlet,接下去會模擬一次簡單請求,請求調用這個 jsp 文件,以便編譯這個 jsp 文件為 class,並初始化這個 class。

這樣 Servlet 對象就初始化完成了,事實上 Servlet 從被 web.xml 中解析到完成初始化,這個過程非常復雜,中間有很多過程,包括各種容器狀態的轉化引起的監聽事件的觸發、各種訪問權限的控制和一些不可預料的錯誤發生的判斷行為等等。這裏只抓了一些關鍵環節進行闡述,有個總體脈絡。

Servlet 如何工作(如何被調用)

當用戶從瀏覽器向服務器發起一個請求,通常會包含如下信息:http://hostname: port /contextpath/servletpath,hostname 和 port 是用來與服務器建立 TCP 連接,而後面的 URL 才是用來選擇服務器中那個子容器服務用戶的請求。

Tomcat7.0 中這種映射工作有專門一個類來完成的,這個就是 org.apache.tomcat.util.http.mapper,這個類保存了 Tomcat 的 Container 容器中的所有子容器的信息,當 org.apache.catalina.connector. Request(由org.apache.coyote.Request而來) 類在進入 Container 容器之前,mapper 將會根據這次請求的 hostnane 和 contextpath 將 host 和 context 容器設置到 Request 的 mappingData 屬性中。所以當 Request 進入 Container 容器之前,它要訪問那個子容器這時就已經確定了。

mapper 中怎麽會有容器的完整關系,在MapperListener 類的初始化過程,下面是 MapperListener 的 init 方法代碼 :

技術分享圖片
public void init() { 
        findDefaultHost(); 
        Engine engine = (Engine) connector.getService().getContainer(); 
        engine.addContainerListener(this); 
        Container[] conHosts = engine.findChildren(); 
        for (Container conHost : conHosts) { 
            Host host = (Host) conHost; 
            if (!LifecycleState.NEW.equals(host.getState())) { 
                host.addLifecycleListener(this); 
                registerHost(host); 
            } 
        } 
 }
技術分享圖片

這段代碼的作用就是將 MapperListener 類作為一個監聽者加到整個 Container 容器中的每個子容器中,這樣只要任何一個容器發生變化,MapperListener 都將會被通知,相應的保存容器關系的 MapperListener 的 mapper 屬性也會修改。for 循環中就是將 host 及下面的子容器註冊到 mapper 中。

請求到達最終的 Servlet 之前還要完成一些步驟,必須要執行 Filter 鏈,以及要通知你在 web.xml 中定義的 listener。假設已經完成了這些步驟,接下去就要執行 Servlet 的 service 方法了,通常情況下,我們自己定義的 servlet 並不是直接去實現 javax.servlet.servlet 接口,而是去繼承更簡單的 HttpServlet 類或者 GenericServlet 類,我們可以有選擇的覆蓋相應方法去實現我們要完成的工作(去實現 service 方法)。

當 Servlet 從 Servlet 容器中移除時,也就表明該 Servlet 的生命周期結束了,這時 Servlet 的 destroy 方法將被調用,做一些掃尾工作。

Servlet 中的 Listener

目前 Servlet 中提供了 6 種兩類事件的觀察者接口,它們分別是:4 個 EventListeners 類型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 個 LifecycleListeners 類型的,ServletContextListener、HttpSessionListener。如下圖所示:

它們基本上涵蓋了整個 Servlet 生命周期中,你感興趣的每種事件。這些 Listener 的實現類可以配置在 web.xml 中的 <listener> 標簽中。當然也可以在應用程序中動態添加 Listener,需要註意的是 ServletContextListener 在容器啟動之後就不能再添加新的,因為它所監聽的事件已經不會再出現。

技術分享圖片

如Spring的org.springframework.web.context.ContextLoaderListener就實現了一個ServletContextListener,當容器加載時啟動spring容器。ContextLoaderListener在contextInitialized方法中初始化Spring容器(加載applicationContext.xml)。

Servlet 中的 Filter

Filter可以完成與Servlet同樣的工作,甚至比其更加靈活,因為它除了提供request和response對象外,還提供了一個FilterChain對象,這個對象可以讓我們更加靈活地控制請求的流轉。

在Tomcat中,FilterConfig和FilterChain的實現類分別是ApplicationFilterConfig和ApplicationFilterChain,而Filter的實現類有用戶自定義,只要實現Filter接口中定義的三個接口就行,這三個接口在與Servlet中的類似,只不過還有一個ApplicationFilterChain類,它可以將多個Filter串聯起來,組成一個鏈,下面是Filter類中的三個接口方法:

1.init(FilterConfig):初始化接口,在用戶自定義的Filter初始化時被調用,它與Servlet的init方法的作用是一樣的,FilterConfig與ServletConfig也類似,除了都能取到容器的環境類ServletContext對象外,還能獲取在<filter>下配置的<init-param>參數值。

2.doFilter(ServletRequest, ServletResponse, FilterChain):在每個用戶請求進來時這個方法都會被調用,並在Servlet的service方法前被調用。而FilterChain就代表當前的整個請求鏈,所以可以通過FilterChain.doFilter()將請求繼續傳遞下去。如果想攔截這個請求,就不調用FilterChain.doFilter(),那麽這個請求就不返回了。所以Filter是一種責任鏈模式。

3.destroy:當Filter對象被銷毀時,這個方法被調用。註意,當Web容器調用這個方法之後,容器會再調用一次doFilter方法。

Filter類的核心還是傳遞的FilterChain對象,這個對象保存了到最終Servlet對象的所有Filter對象,這些對象都保存在ApplicationFilterChain對象的filters數組中。在FilterChain鏈上每執行一個Filter對象,數組的當前計數都會加1,直到計數等於數組的長度,當FilterChain上所有的Filter對象執行完成後,就會執行最終的Servlet。所以在ApplicationFilterChain對象中會持有Servlet對象的引用。

Servlet 中的 url-pattern

在web.xml中<servlet-mapping>和<filter-mapping>都有配置項,它們的作用都是匹配一次請求是否會執行這個Servlet或者Filter。

Servlet的匹配是通過org.apache.tomcat.util.http.Mapper類完成的,這個類會根據請求到的URL來匹配在每個Servlet中配置的,所以它在一個請求被創建時就已經匹配了。servlet是通過org.apache.tomcat.util.http.mapper.Mapper.internalMapWrapper方法中匹配的,servlet的匹配規則是精確匹配,路徑匹配,後綴匹配,按順序匹配,一次請求只能成功匹配到一個servlet。

Filter的url-pattern匹配是在創建ApplicationFilterChain對象時進行的,它會把所有定義的Filter的url-pattern與當前的URL匹配,如果匹配成功就將這個Filter保存到ApplicationFilterChain的filters數組中,然後在FilterChain中依次調用。filter是通過ApplicationFilterFactory.matchFiltersURL方法中匹配的。filter的匹配規則是,只要匹配成功,所有符合規則的filter都會被執行;

容器啟動時會檢查url-pattern是否符合規則,在web.xml加載時,會首先檢查配置是否符合規則,這個檢查是在StandardContext的validateURLPattern方法中檢查的,如果檢查不成功,Context容器啟動會失敗,並且會報java.lang.IllegalArgumentException:Invalid/a/*.htm in Servlet mapping錯誤。
<url-pattern>的解析工作,對servlet和filter是一樣的,匹配規則有如下3種:
(1)精確匹配:如/foo.htm只會匹配foo.htm這個URL。
(2)路徑匹配:如/foo/*會匹配以foo為前綴的URL。
(3)後綴匹配:如*.htm會匹配所有以.htm為後綴的URL。

Servlet工作原理解析(tomcat7、嵌入式服務器)