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

    這篇文章主要是帶着讀者經過分析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,這個方法裏面主要作下面事情:數據結構

  1. 掃描應用打包的全部Jar來檢索Jar包裏面的web.xml配置並解析,放入內存。app

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

  3. 基於SPI機制查找ServletContainerInitializer的實現,寫web中間件的同窗注意了,瞭解SPI以及                           ServletContainerInitializer機制這對於你來講多是一個很好的知識點。jsp

  4. 處理/WEB-INF/classes下面的類的註解,某個版本Servlet支持註解方式的配置,能夠猜想相關事宜就是在這裏幹          的。async

  5. 處理Jar包中的註解類。ui

  6. 將web配置按照必定規則合併到一塊兒。

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

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

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

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

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

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

從上面的代碼咱們至少能夠總結下面值得注意的兩點:

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

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

3、組件的初始化

下面轉入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;
    }
}

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裏面撈出了兩句英文,你們慢慢體會:

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

相關文章
相關標籤/搜索