-----轉自許令波老師Servlet 工做原理解析 感受寫的很不錯,保存下來,留着之後溫習java
要介紹 Servlet 必需要先把 Servlet 容器說清楚,Servlet 與 Servlet 容器的關係有點像槍和子彈的關係,槍是爲子彈而生,而子彈又讓槍有了殺傷力。雖然它們是彼此依存的,可是又相互獨立發展,這一切都是爲了適應工業化生產的結果。從技術角度來講是爲了解耦,經過標準化接口來相互協做。既然接口是鏈接 Servlet 與 Servlet 容器的關鍵,那咱們就從它們的接口提及。web
前面說了 Servlet 容器做爲一個獨立發展的標準化產品,目前它的種類不少,可是它們都有本身的市場定位,很難說誰優誰劣,各有特色。例如如今比較流行的 Jetty,在定製化和移動領域有不錯的發展,咱們這裏仍是以你們最爲熟悉 Tomcat 爲例來介紹 Servlet 容器如何管理 Servlet。Tomcat 自己也很複雜,咱們只從 Servlet 與 Servlet 容器的接口部分開始介紹,關於 Tomcat 的詳細介紹能夠參考個人另一篇文章《 Tomcat 系統架構與模式設計分析》。apache
Tomcat 的容器等級中,Context 容器是直接管理 Servlet 在容器中的包裝類 Wrapper,因此 Context 容器如何運行將直接影響 Servlet 的工做方式。設計模式
從上圖能夠看出 Tomcat 的容器分爲四個等級,真正管理 Servlet 的容器是 Context 容器,一個 Context 對應一個 Web 工程,在 Tomcat 的配置文件中能夠很容易發現這一點,以下tomcat
<Context path="/projectOne " docBase="D:\projects\projectOne" reloadable="true" />
下面詳細介紹一下 Tomcat 解析 Context 容器的過程,包括如何構建 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);
清單 1 的代碼是建立一個 Tomcat 實例並新增一個 Web 應用,而後啓動 Tomcat 並調用其中的一個 HelloWorldExample Servlet,看有沒有正確返回預期的數據。
Tomcat 的 addWebapp 方法的代碼以下:app
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 中。
接下去將會調用 Tomcat 的 start 方法啓動 Tomcat,若是你清楚 Tomcat 的系統架構,你會容易理解 Tomcat 的啓動邏輯,Tomcat 的啓動邏輯是基於觀察者模式設計的,全部的容器都會繼承 Lifecycle 接口,它管理者容器的整個生命週期,全部容器的的修改和狀態的改變都會由它去通知已經註冊的觀察者(Listener),關於這個設計模式能夠參考《 Tomcat 的系統架構與設計模式,第二部分:設計模式》。Tomcat 啓動的時序圖能夠用圖 2 表示。
上圖描述了 Tomcat 啓動過程當中,主要類之間的時序關係,下面咱們將會重點關注添加 examples 應用所對應的 StandardContext 容器的啓動過程。webapp
當 Context 容器初始化狀態設爲 init 時,添加在 Contex 容器的 Listener 將會被調用。ContextConfig 繼承了 LifecycleListener 接口,它是在調用清單 3 時被加入到 StandardContext 容器中。ContextConfig 類會負責整個 Web 應用的配置文件的解析工做。jsp
ContextConfig 的 init 方法將會主要完成如下工做:ui
ContextConfig 的 init 方法完成後,Context 容器的會執行 startInternal 方法,這個方法啓動邏輯比較複雜,主要包括以下幾個部分:
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 到底起到什麼做用了。
前面已經完成了 Servlet 的解析工做,而且被包裝成 StandardWrapper 添加在 Context 容器中,可是它仍然不能爲咱們工做,它尚未被實例化。下面咱們將介紹 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。Tomcat 容器爲什麼要傳 StandardWrapperFacade 給 Servlet 對象將在後面作詳細解析。
若是該 Servlet 關聯的是一個 jsp 文件,那麼前面初始化的就是 JspServlet,接下去會模擬一次簡單請求,請求調用這個 jsp 文件,以便編譯這個 jsp 文件爲 class,並初始化這個 class。
這樣 Servlet 對象就初始化完成了,事實上 Servlet 從被 web.xml 中解析到完成初始化,這個過程很是複雜,中間有不少過程,包括各類容器狀態的轉化引發的監聽事件的觸發、各類訪問權限的控制和一些不可預料的錯誤發生的判斷行爲等等。咱們這裏只抓了一些關鍵環節進行闡述,試圖讓你們有個整體脈絡。
下面是這個過程的一個完整的時序圖,其中也省略了一些細節。
具體文章見:http://www.ibm.com/developerworks/cn/java/j-lo-servlet/