Tomcat源碼深析之web.xml組件的處理
這篇文章主要是帶著讀者通過分析Tomcat的源碼,深入了解Tomcat對web.xml配置的組件的的處理,文章內容主要包括Tomcat對上下文參數(contextParams),過濾器(Filters),應用監聽器(listeners)以及Servlet的加載,初始化等等。
在Java Web開發中我們對web.xml這個配置文件并不陌生,也對web.xml中配置的常用組件很了解,我所指的即過濾器、監聽器、Servlet三大組件。包括他們的加載順序,初始化順序,我相信這對于所有Java Web開發者來說是一定要掌握的基礎知識。這也是開發Web中間件會經常用到的,隨便列舉一些例子:Spring、Struts、UrlRewrite、等等。
下面跟著博主一起來通過扒一扒Tomcat的源碼來深入了解一下他們的相關知識吧,這比概念上去了解更深刻一些。
一、web.xml的解析
這個部分可以在類ContextConfig類中找到相關源碼,Context即代表Servlet的上下文。里面有個protected的方法webConfig,這個方法里面主要做下面事情:
-
掃描應用打包的所有Jar來檢索Jar包里面的web.xml配置并解析,放入內存。
-
對這些已經檢索到的web配置進行排序。
-
基于SPI機制查找ServletContainerInitializer的實現,寫web中間件的同學注意了,了解SPI以及 ServletContainerInitializer機制這對于你來說可能是一個很好的知識點。
-
處理/WEB-INF/classes下面的類的注解,某個版本Servlet支持注解方式的配置,可以猜測相關事宜就是在這里干 的。
-
處理Jar包中的注解類。
-
將web配置按照一定規則合并到一起。
-
應用全局默認配置,還記得Tomcat包下面的conf文件夾下面有個web.xml配置文件吧。
-
將JSP轉換為Servlet,這讓我想起了若干年前對JSP的理解。
-
將web配置應用到Servlet上下文,也即Servlet容器。
-
將配置信息保存起來以供其他組件訪問,使得其他組件不需要再次重復上面的步驟去獲取配置信息了。
-
檢索Jar包中的靜態資源。
-
將ServletContainerInitializer配置到上下文。
在上面這些步驟中,本片文章關系的入口在第9步,即Tomcat是如何將Web配置應用到上下文的。
二、根據web.xml配置裝配Servlet上下文
我們跟著WebXml的configureContext進入方法的實現,這里我按順序摘抄幾個源碼片段并說明:
for (Entry<String, String> entry : contextParams.entrySet()) {
context.addParameter(entry.getKey(), entry.getValue());
}for (FilterDef filter : filters.values()) {
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false");
}
context.addFilterDef(filter);
}
for (FilterMap filterMap : filterMaps) {
context.addFilterMap(filterMap);
}for (String listener : listeners) {
context.addApplicationListener(listener);
}for (ServletDef servlet : servlets.values()) {
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored
// jsp-file gets passed to the JSP Servlet as an init-param
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());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
for (Entry<String, String> entry : servletMappings.entrySet()) {
context.addServletMapping(entry.getKey(), entry.getValue());
} 從上面的代碼我們至少可以總結下面值得注意的兩點:
-
Servlet容器對上下文參數、監聽器、過濾器、Servlet的裝配順序為:上下文參數->過濾器->監聽器->Servlet。
-
Servlet支持容器啟動時加載、是否異步配置以及配置覆蓋。
三、組件的初始化
下面轉入StandardContext這個類,StandardContext是Servlet上下文的標準實現,標準實現在Tomcat里面有一個系列,包括StandardServer、StandardService、StandardEngine、StandardHost等等,這些都是Tomcat不同級別的容器的標準實現。
我們可以直接定位到startInternal這個方法的實現,我們看下我們關系的部分步驟:
-
第一個是(Set up the context init params),這里我就不翻譯了。
-
解析來的是Call ServletContainerInitializer,這里是值得web中間件開發者注意的,我們可以通過自定義ServletContainerInitializer服務來做一些組件初始化之前的事情,如在這個環節動態裝配組件?獲取容器上下文?
-
Configure and call application event listeners,包括下面的error信息(Error listenerStart)這里是很重要的一步,有經驗的開發者肯定會對這個error信息有點熟悉,應用起不來?呵呵……,提一下熟悉Spring的同學都知道ContextLoaderListener這個東西,Spring就是將對Spring容器的初始化工作放在的這個監聽器里面實現的,包括對Spring配置文件的解析,容器初始化……
-
Configure and call application filters,和error信息:Error filterStart。這里是對Filter進行了初始化。
-
Load and initialize all "load on startup" servlets。這里對配置了load on startup的Servlet進行初始化。
我想介紹的主要就是上面的五個步驟了,總結一下主要組件的初始化順序為:上下文參數->監聽器->過濾器->Servlet。
這里有一點不舒服的地方是Tomcat對著三個組件的裝配和初始化順序有點差別。無恥的貼一點代碼一起欣賞下:
// Create context attributes that will be required
if (ok) {
getServletContext().setAttribute(
JarScanner.class.getName(), getJarScanner());
}
// Set up the context init params
mergeParameters();
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error( "Error listenerStart");
ok = false;
}
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error("Error filterStart");
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error("Error loadOnStartup");
ok = false;
}
} 四、過濾器的執行順序
過濾器裝配的時候主要涉及到三個數據結構:filters、filterMaps、以及filterMappingNames。我們分析下,如果根據請求來執行過濾器鏈的話,那么我們肯定是需要映射規則的,因此我們鎖定filterMaps這個數據,查找下findFilterMaps這個方法哪里調用就好了。果然我們在ApplicaitonFilterFactory里面找到了下面這個片段:
// Acquire the filter mappings for this Context StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps();
這里其實是按照FilterMapping的配置來構造過濾器鏈的,那么我們深刻的了解到了一點,請求過濾鏈的順序為FilterMapping的配置順序。其中有行代碼值得注意:
filterChain.setServlet(servlet);
在創建過濾器鏈的方法實現里面,Servlet也被放進去了。
五、過濾器鏈以及Servlet的最終執行
我們拿到過濾器鏈之后順藤摸瓜,找到調用createFilterChain的地方,在StandardWrapperValve類里面(這里又是一個標準實現)。貼一個片段:
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
//TODO SERVLET3 - async
((AsyncContextImpl)request.getAsyncContext()).doInternalDispatch();
} else if (comet) {
filterChain.doFilterEvent(request.getEvent());
request.setComet(true);
} else {
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
} 上面的Comments可告訴我們過濾器鏈在這里執行,而且值得注意的是Servlet也是在這里面執行的。我從ApplicationFilterChain里面撈出了兩句英文,大家慢慢體會:
-
Call the next filter if there is one.
-
We fell off the end of the chain -- call the servlet instance.
Tomcat的過濾器鏈是一種典型的責任鏈模式的實踐,組織的也還算精巧,到此我們的分析已經結束了,相信通過對源碼的分析,我們可以對web.xml有了更深刻的了解。
本片文章是基于apache-tomcat7.0.56版本源代碼,由作者原創,如果有我沒有講到的地方歡迎大家在評論里面補充。
作者:陸晨
于2016年1月1日(元旦)