這篇文章主要是帶着讀者經過分析Tomcat的源碼,深刻了解Tomcat對web.xml配置的組件的的處理,文章內容主要包括Tomcat對上下文參數(contextParams),過濾器(Filters),應用監聽器(listeners)以及Servlet的加載,初始化等等。java
在Java Web開發中咱們對web.xml這個配置文件並不陌生,也對web.xml中配置的經常使用組件很瞭解,我所指的即過濾器、監聽器、Servlet三大組件。包括他們的加載順序,初始化順序,我相信這對於全部Java Web開發者來講是必定要掌握的基礎知識。這也是開發Web中間件會常常用到的,隨便列舉一些例子:Spring、Struts、UrlRewrite、等等。web
下面跟着博主一塊兒來經過扒一扒Tomcat的源碼來深刻了解一下他們的相關知識吧,這比概念上去了解更深入一些。
apache
1、web.xml的解析tomcat
這個部分能夠在類ContextConfig類中找到相關源碼,Context即表明Servlet的上下文。裏面有個protected的方法webConfig,這個方法裏面主要作下面事情:數據結構
掃描應用打包的全部Jar來檢索Jar包裏面的web.xml配置並解析,放入內存。app
對這些已經檢索到的web配置進行排序。異步
基於SPI機制查找ServletContainerInitializer的實現,寫web中間件的同窗注意了,瞭解SPI以及 ServletContainerInitializer機制這對於你來講多是一個很好的知識點。jsp
處理/WEB-INF/classes下面的類的註解,某個版本Servlet支持註解方式的配置,能夠猜想相關事宜就是在這裏幹 的。async
處理Jar包中的註解類。ui
將web配置按照必定規則合併到一塊兒。
應用全局默認配置,還記得Tomcat包下面的conf文件夾下面有個web.xml配置文件吧。
將JSP轉換爲Servlet,這讓我想起了若干年前對JSP的理解。
將web配置應用到Servlet上下文,也即Servlet容器。
將配置信息保存起來以供其餘組件訪問,使得其餘組件不須要再次重複上面的步驟去獲取配置信息了。
檢索Jar包中的靜態資源。
將ServletContainerInitializer配置到上下文。
在上面這些步驟中,本片文章關係的入口在第9步,即Tomcat是如何將Web配置應用到上下文的。
2、根據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支持容器啓動時加載、是否異步配置以及配置覆蓋。
3、組件的初始化
下面轉入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; } }
4、過濾器的執行順序
過濾器裝配的時候主要涉及到三個數據結構: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也被放進去了。
5、過濾器鏈以及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日(元旦)