要介紹 Servlet 必須要先把 Servlet 容器說清楚,Servlet 與 Servlet 容器的關系有點像槍和子彈的關系,槍為彈而生,而彈又讓槍有了殺傷力。雖然它們是彼此依存的,但是又互相獨立發展,這一切都是為了适應工業化生産。從技術角度來說是為了解耦,通過标準化接口來互相協作。既然接口是連接配接 Servlet 與 Servlet 容器的關鍵,那我們就從它們的接口說起。
Servlet 容器作為一個獨立發展的标準化産品,目前種類很多,但是它們都有自己的市場定位,很難說誰優誰劣。以大家最為熟悉 Tomcat 為例來介紹 Servlet 容器如何管理 Servlet。Tomcat 本身也很複雜,我們隻從 Servlet 與 Servlet 容器的接口部分開始介紹,關于 Tomcat 的詳細介紹可以參考我的網上其他博文
在Tomcat 的容器等級中,Context 容器直接管理 Servlet 在容器中的包裝類 Wrapper,是以 Context 容器如何運作将直接影響 Servlet 的工作方式。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuUGOwAjNwATOzMzN1QjZ1E2MiBjY3gjMwcjMjZjY3Q2YfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
Tomcat容器模型
從上圖可以看出 Tomcat 的容器分為四個等級,真正管理 Servlet 的容器是 Context 容器,一個 Context 對應一個 Web 工程,在 Tomcat 的配置檔案中可以很容易發現這一點,如下:
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)
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 到底起到什麼作用了。