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>
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值