1. 程式人生 > >Tomcat原始碼研究之web.xml中元素的載入順序

Tomcat原始碼研究之web.xml中元素的載入順序

本文主所關注的是web.xml檔案各類元素的解析載入順序。
2018/11/15 因為CSDN改良了markdown語法,進行重新排版。

1. 前言

  1. 等不及的可以直接拖到最後看總結。

  2. 在Tomcat中,web.xml對應於Tomcat中的WebXml類,web.xml檔案裡解析出來的內容由Tomcat中的的WebXml類來承載;

  3. 具體的解析過程可以參見 ContextConfig類的parseWebXml方法。

  4. WebXml類中的configureContext(Context)方法以Context為引數,使用WebXml類中儲存的解析資料對Context進行填充。這裡舉個例子:其中就有這樣一端程式碼:

    // servletMappings中 <servlet-mapping> 的鍵值對
    for (Entry<String, String> entry : servletMappings.entrySet()) {
    	// 將鍵值對新增到Context, 讓Context可以掌控對映處理關係
        context.addServletMapping(entry.getKey(), entry.getValue());
    }
    
  5. 時機: 在監聽到Lifecycle.CONFIGURE_START_EVENT事件後, WebXml將自身儲存的資訊注入到Context中 —— (關於這個事件,可以在下面的startInternal

    樣例原始碼中看到播發的位置)

  6. 以下就是執行順序了:

    1. ContextConfig類監聽到Lifecycle.CONFIGURE_START_EVENT事件
    2. ContextConfig類呼叫自身的configureStart()方法,進而呼叫自身的webConfig()方法 ,最後在將${CATALINA_BASE}/conf/web.xml配置資訊融入自身後,呼叫WebXml.configureContext(context)來將解析到配置資訊注入到Context例項中。

2. 細節

web.xml檔案的解析是在Context啟動時參與的。所以我們將注意力集中到Context啟動時的順序上來。

Tomcat中的Context啟動時的順序是這樣的(原始碼位於:StandardContext實現的startInternal

  1. 解析web.xml (通過觸發Lifecycle.CONFIGURE_START_EVENT事件, 讓ContextConfig類去完成解析web.xml工作, 解析出來的資訊由WebXml承載, 並且由WebXml例項將它們注入到Context中); 注意這裡是同步回撥 ; 具體細節可以參見ContextConfig類中的原始碼。
    1. 其中一個重要環節就是解析web.xml(原始碼位置:ContextConfig.webConfig())。
    2. 另外一個重要環節就是讀取lib下的JAR中所有的META/resources裡的靜態資源(原始碼位置:ContextConfig.processResourceJARs())。
  2. 往**ServletContext例項中注入<context-param> 引數**
  3. 回撥Servlet3.0的ServletContainerInitializers介面實現類ServletContainerInitializers介面實現類是在ContextConfig 類中的processServletContainerInitializers();方法解析到的。
  4. 觸發 Listener 事件(beforeContextInitialized, afterContextInitialized); 這裡只會觸發 ServletContextListener介面型別的 ; 例如Spring-web中的ContextLoaderListener就實現了ServletContextListener介面型別
  5. 初始化 Filter, 呼叫其init方法
  6. 載入 **啟動時即載入的servlet **
    1. (Load-on-startup元素在web應用啟動的時候指定了servlet被載入的順序,它的值必須是一個整數。如果它的值是一個負整數或是這個元素不存在,那麼容器會在該servlet被呼叫的時候,載入這個servlet。如果值是正整數或零,容器在配置的時候就載入並初始化這個servlet,容器必須保證值小的先被載入。如果值相等,容器可以自動選擇先載入誰。 )

    2. 另外在WebXml類中configureContext方法中可以看到這樣一行

      // servletMappings中
      for (Entry<String, String> entry : servletMappings.entrySet()) {
      	// <servlet-mapping>的匹配關係
          context.addServletMapping(entry.getKey(), entry.getValue());
      }
      

3. 原始碼解析之StandardContext.startInternal

// StandardContext實現的startInternal
protected synchronized void startInternal() throws LifecycleException {

	// 略
	
	// Sets the configured property to false. 
	//  代表配置工作還未完成
	setConfigured(false);
	boolean ok = true;

	// 略
	
	// Sets a loader
	// 設定本Context的ClassLoader層級結構
	if (getLoader() == null) {
		WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
		webappLoader.setDelegate(getDelegate());
		setLoader(webappLoader);
	}

	// Initialize character set mapper
	getCharsetMapper();

	// Post work directory
	postWorkDirectory();

	// 略

	// 1. -----------------------------------------------
	// Notify our interested LifecycleListeners
	// 	此事件將被ContextConfig監聽到; 參見《Tomcat原始碼研究之ContextConfig.md》中的 configureStart() 方法講解, 
	//    1. 其中一個重要環節就是解析web.xml
	//    2. 另外一個重要環節就是讀取lib下的JAR中所有的META/resources裡的靜態資源
	fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
	
	// Start our child containers, if not already started
	for (Container child : findChildren()) {
		if (!child.getState().isAvailable()) {
			child.start();
		}
	}

	// 略	

	// Start the Valves in our pipeline (including the basic),
	// if any
	if (pipeline instanceof Lifecycle) {
		((Lifecycle) pipeline).start();
	}
	
	// 略

	try {

		// 2. ----------------------------------------------- 
        // Set up the context init params
		// 這裡會將從web.xml解析出來的<context-param> 引數通過呼叫setInitParameter方法注入到ServletContext中
		// 外界可以通過 getInitParameter 方法取到它們.
        mergeParameters();

		// 3. -----------------------------------------------
		// Call ServletContainerInitializers
		// 啟動Servlet3.0的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;
			}
		}

		// 4. -----------------------------------------------
		// Configure and call application event listeners
		// 觸發監聽的listener
		if (ok) {
			// 啟動listener,觸發事件beforeContextInitialized, afterContextInitialized
			if (!listenerStart()) {
				log.error(sm.getString("standardContext.listenerFail"));
				ok = false;
			}
		}
		
		// 略

		// 5. -----------------------------------------------
		// 初始化Filter
		// Configure and call application filters
		if (ok) {
			// 這裡初始化每個定義的Filter, 呼叫它們的init方法
			// 每個Filter在Tomcat內部對應一個FilterDef, 而ApplicationFilterConfig則實現了FilterConfig
			// ApplicationFilterConfig中有FilterDef欄位, 而FilterDef內部沒有ApplicationFilterConfig欄位		
			if (!filterStart()) {
				log.error(sm.getString("standardContext.filterFail"));
				ok = false;
			}
		}

		// 6. -----------------------------------------------
		// Load and initialize all "load on startup" servlets
		// 載入 啟動時即載入的servlet
		// 在loadOnStartup 方法裡你可以看到 loadOnStartup 屬性小於0的就不會被載入
		// 這loadOnStartup方法裡面呼叫的是Wrapper類的load方法; 這裡就會載入wrapper所包裝的servlet.
		// 
		//
		// 參見規則org.apache.catalina.startup.WebRuleSet類的addRuleInstances方法 ; 注意這個方法裡的digester目標是WebXml例項; 所以addRuleInstances方法裡的宣告的"addServlet"方法就是定義在WebXml中.
		if (ok) {
			if (!loadOnStartup(findChildren())){
				log.error(sm.getString("standardContext.servletFail"));
				ok = false;
			}
		}
		
		// Start ContainerBackgroundProcessor thread
		super.threadStart();
	} finally {
		// Unbinding thread
		unbindThread(oldCCL);
	}

	// 略

	// Reinitializing if something went wrong
	if (!ok) {
		setState(LifecycleState.FAILED);
	} else {
		// 觸發事件
		setState(LifecycleState.STARTING);
	}
}

4. 總結

讓我們回顧下載入順序,以便處理一些奇怪需求能做到心中有數。

  1. 讀取lib下的JAR中所有的META/resources裡的靜態資源。

  2. ServletContext例項中注入<context-param> 引數。

  3. 回撥 Servlet3.0的ServletContainerInitializer介面實現類 ; 注意ServletContainerInitializer介面實現類是在ContextConfig.processServletContainerInitializers();方法解析到的。相關的例子有SpringMVC中的SpringServletContainerInitializer,以及Log4j2中的Log4jServletContainerInitializer。以SPI的方式檢索。

  4. 觸發 Listener 事件(beforeContextInitialized, afterContextInitialized); 這裡只會觸發 ServletContextListener介面型別的 ; 例如Spring-web中的ContextLoaderListener就實現了ServletContextListener介面型別。

  5. 初始化 Filter, 呼叫其init方法。

  6. 載入 - 啟動時即載入的Servlet(Load-on-startup 屬性為正或零時,且 值小的先被載入)。

  7. 以上總結為一句話就是 : Web容器載入順序為ServletContext ~ context-param ~ ServletContainerInitializer ~ listener ~ filter ~ servlet

  8. 對於Filter,其執行順序是由在web.xml宣告的<filter-mapping> 順序,而不是<filter>的宣告順序。

    <!-- 例如這裡, 下面的testFilter就不會執行-->
    <filter-mapping>
    	<filter-name>struts2</filter-name>
    	<url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <filter-mapping>
    	<filter-name>testFilter</filter-name>
    	<url-pattern>/*</url-pattern>
    </filter-mapping>