Tomcat源碼深析之web.xml組件的處理

jopen 8年前發布 | 11K 次閱讀 Tomcat 應用服務器

    這篇文章主要是帶著讀者通過分析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,這個方法里面主要做下面事情:

  1. 掃描應用打包的所有Jar來檢索Jar包里面的web.xml配置并解析,放入內存。

  2. 對這些已經檢索到的web配置進行排序。

  3. 基于SPI機制查找ServletContainerInitializer的實現,寫web中間件的同學注意了,了解SPI以及                           ServletContainerInitializer機制這對于你來說可能是一個很好的知識點。

  4. 處理/WEB-INF/classes下面的類的注解,某個版本Servlet支持注解方式的配置,可以猜測相關事宜就是在這里干          的。

  5. 處理Jar包中的注解類。

  6. 將web配置按照一定規則合并到一起。

  7. 應用全局默認配置,還記得Tomcat包下面的conf文件夾下面有個web.xml配置文件吧。

  8. 將JSP轉換為Servlet,這讓我想起了若干年前對JSP的理解。

  9. 將web配置應用到Servlet上下文,也即Servlet容器。

  10. 將配置信息保存起來以供其他組件訪問,使得其他組件不需要再次重復上面的步驟去獲取配置信息了。

  11. 檢索Jar包中的靜態資源。

  12. 將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());
}

從上面的代碼我們至少可以總結下面值得注意的兩點:

  1. Servlet容器對上下文參數、監聽器、過濾器、Servlet的裝配順序為:上下文參數->過濾器->監聽器->Servlet。

  2. Servlet支持容器啟動時加載、是否異步配置以及配置覆蓋。

三、組件的初始化

下面轉入StandardContext這個類,StandardContext是Servlet上下文的標準實現,標準實現在Tomcat里面有一個系列,包括StandardServer、StandardService、StandardEngine、StandardHost等等,這些都是Tomcat不同級別的容器的標準實現。

我們可以直接定位到startInternal這個方法的實現,我們看下我們關系的部分步驟:

  1. 第一個是(Set up the context init params),這里我就不翻譯了。

  2. 解析來的是Call ServletContainerInitializer,這里是值得web中間件開發者注意的,我們可以通過自定義ServletContainerInitializer服務來做一些組件初始化之前的事情,如在這個環節動態裝配組件?獲取容器上下文?

  3. Configure and call application event listeners,包括下面的error信息(Error listenerStart)這里是很重要的一步,有經驗的開發者肯定會對這個error信息有點熟悉,應用起不來?呵呵……,提一下熟悉Spring的同學都知道ContextLoaderListener這個東西,Spring就是將對Spring容器的初始化工作放在的這個監聽器里面實現的,包括對Spring配置文件的解析,容器初始化……

  4. Configure and call application filters,和error信息:Error filterStart。這里是對Filter進行了初始化。

  5. 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里面撈出了兩句英文,大家慢慢體會:

  1. Call the next filter if there is one.

  2. We fell off the end of the chain -- call the servlet instance.

Tomcat的過濾器鏈是一種典型的責任鏈模式的實踐,組織的也還算精巧,到此我們的分析已經結束了,相信通過對源碼的分析,我們可以對web.xml有了更深刻的了解。

本片文章是基于apache-tomcat7.0.56版本源代碼,由作者原創,如果有我沒有講到的地方歡迎大家在評論里面補充。


作者:陸晨

于2016年1月1日(元旦)

來自: http://my.oschina.net/andylucc/blog/596046

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!