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日(元旦)