Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫

預流發表於2018-02-09

前一篇文章分析到了org.apache.catalina.deploy.WebXml類的 configureContext 方法,可以看到在這個方法中通過各種 setXXX、addXXX 方法的呼叫,使得每個應用中的 web.xml 檔案的解析後將應用內部的表示 Servlet、Listener、Filter 的配置資訊與表示一個 web 應用的 Context 物件關聯起來。

這裡列出 configureContext 方法中與 Servlet、Listener、Filter 的配置資訊設定相關的呼叫程式碼:

for (FilterDef filter : filters.values()) {
    if (filter.getAsyncSupported() == null) {
        filter.setAsyncSupported("false");
    }
    context.addFilterDef(filter);
}
for (FilterMap filterMap : filterMaps) {
    context.addFilterMap(filterMap);
}
複製程式碼

這是設定 Filter 相關配置資訊的。

for (String listener : listeners) {
    context.addApplicationListener(
            new ApplicationListener(listener, false));
}
複製程式碼

這是給應用新增 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 的相關配置資訊的。

以上是在各個 web 應用的 web.xml 檔案中(如果是 servlet 3,還會包括將這些配置資訊放在類的註解中,所以解析 web.xml 檔案之前可能會存在各個 web.xml 檔案資訊的合併步驟,這些動作的程式碼在前一篇文章中講 ContextConfig 類的 webConfig 方法中)的相關配置資訊的設定,但需要注意的是,這裡僅僅是將這些配置資訊儲存到了 StandardContext 的相應例項變數中,真正在一次請求訪問中用到的 Servlet、Listener、Filter 的例項並沒有構造出來,以上方法呼叫僅構造了代表這些例項的封裝類的例項,如 StandardWrapper、ApplicationListener、FilterDef、FilterMap。

那麼一個 web 應用中的 Servlet、Listener、Filter 的例項究竟在什麼時候構造出來的呢?答案在org.apache.catalina.core.StandardContext類的 startInternal 方法中:

Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
這 303 行可以講的東西有很多,為了不偏離本文主題只抽出與現在要討論的問題相關的程式碼來分析。

第 125 行會釋出一個CONFIGURE_START_EVENT事件,按前一篇博文所述,這裡即會觸發對 web.xml 的解析。第 205、206 行設定例項管理器為 DefaultInstanceManager(這個類在後面談例項構造時會用到)。第 237 行會呼叫 listenerStart 方法,第 255 行呼叫了 filterStart 方法,第 263 行呼叫了 loadOnStartup 方法,這三處呼叫即觸發 Listener、Filter、Servlet 真正物件的構造,下面逐個分析這些方法。

listenerStart 方法的完整程式碼較長,這裡僅列出與 Listenner 物件構造相關的程式碼:

Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
先從 Context 物件中取出例項變數 applicationListeners(該變數的值在 web.xml 解析時設定),第 12 行通過呼叫instanceManager.newInstance(listener.getClassName()),前面在看 StandardContext 的 startInternal 方法第 205 行時看到 instanceManager 被設定為 DefaultInstanceManager 物件,所以這裡實際會執行 DefaultInstanceManager 類的 newInstance 方法:

public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException {
    Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
    return newInstance(clazz.newInstance(), clazz);
}
複製程式碼

所以instanceManager.newInstance(listener.getClassName())這段程式碼的作用是取出 web.xml 中配置的 Listener 的 class 配置資訊,從而構造實際配置的 Listener 物件。

看下 filterStart 方法:

Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
這段程式碼看起來很簡單,取出 web.xml 解析時讀到的 filter 配置資訊,在第 17 行呼叫 ApplicationFilterConfig 的構造方法:
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
預設情況下 filterDef 中是沒有 Filter 物件的,所以會呼叫第 12 行 getFilter 方法:
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
與 Listener 的物件構造類似,都是通過呼叫getInstanceManager().newInstance方法。當然,按照 Servlet 規範,第 13 行還會呼叫 Filter 的 init 方法。

看下 loadOnStartup 方法:

Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
在 web 應用啟動時將會載入配置了 load-on-startup 屬性的 Servlet。第 24 行,呼叫了 StandardWrapper 類的 load 方法:
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
在第 2 行 loadServlet 方法中與上面的 Listener 和 Filter 物件構造一樣呼叫instanceManager.newInstance來構造 Servlet 物件,與 Filter 類似在第 5 行呼叫 Servlet 的 init 方法。

當然這種載入只是針對配置了 load-on-startup 屬性的 Servlet 而言,其它一般 Servlet 的載入和初始化會推遲到真正請求訪問 web 應用而第一次呼叫該 Servlet 時,下面會看到這種情況下程式碼分析。

以上分析的 web 應用啟動後這些物件的載入情況,接下來分析一下一次請求訪問時,相關的 Filter、Servlet 物件的呼叫。

在之前的《Tomcat 7 的一次請求分析》系列文章中曾經分析了一次請求如何與容器中的 Engine、Host、Context、Wrapper 各級元件匹配,並在這些容器元件內部的管道中流轉的。在該系列第四篇文章最後提到,一次請求最終會執行與它最適配的一個 StandardWrapper 的基礎閥org.apache.catalina.core.StandardWrapperValve的 invoke 方法。當時限於篇幅沒繼續往下分析,這裡接著這段來看看請求的流轉。看下 invoke 方法的程式碼:

Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
因為要支援 Servlet 3 的新特性及各種異常處理,這段程式碼顯得比較長。關注重點第 42 行,這裡會呼叫 StandardWrapper 的 allocate 方法,不再貼出這個方法的程式碼,需要提醒的是在 allocate 方法中可能會呼叫 loadServlet() 方法,這就是上一段提到的請求訪問 web 應用而第一次呼叫該 Servlet 時再載入並初始化 Servlet 。

第 87 到 91 行會構造一個過濾器鏈( filterChain )用於執行這一次請求所經過的相應 Filter ,第 111 和第 128 行會呼叫該 filterChain 的 doFilter 方法:

Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
在該方法最後呼叫了 internalDoFilter 方法:
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
Tomcat 7 中 web 應用載入原理(三)Listener、Filter、Servlet 的載入和呼叫
概述一下這段程式碼,第 6 到 60 行是執行過濾器鏈中的各個過濾器的 doFilter 方法,例項變數n表示過濾器鏈中所有的過濾器,pos表示當前要執行的過濾器。其中第 7 行取出當前要執行的 Filter,之後將pos加 1,接著第 30 行執行 Filter 的 doFilter 方法。一般的過濾器實現中在最後都會有這一句:

FilterChain.doFilter(request, response);
複製程式碼

這樣就又回到了 filterChain 的 doFilter 方法,形成了一個遞迴呼叫。要注意的是,filterChain 物件內部的 pos 是不斷加的,所以假如過濾器鏈中的各個 Filter 的 doFilter 方法都執行完之後將會執行到第 63 行,在接下來的第 92 行、第 95 行即呼叫 Servlet 的 service 方法。

相關文章