本文主所关注的是
web.xml
文件各类元素的解析加载顺序。
2018/11/15 因为CSDN改良了markdown语法,进行重新排版。
1. 前言
-
等不及的可以直接拖到最后看总结。
-
在Tomcat中,
web.xml
对应于Tomcat中的WebXml
类,web.xml
文件里解析出来的内容由Tomcat中的的WebXml
类来承载; -
具体的解析过程可以参见
ContextConfig
类的parseWebXml
方法。 -
WebXml
类中的configureContext(Context)
方法以Context
为参数,使用WebXml
类中存储的解析数据对Context
进行填充。这里举个例子:其中就有这样一端代码:// servletMappings中 <servlet-mapping> 的键值对 for (Entry<String, String> entry : servletMappings.entrySet()) { // 将键值对添加到Context, 让Context可以掌控映射处理关系 context.addServletMapping(entry.getKey(), entry.getValue()); }
-
时机: 在监听到
Lifecycle.CONFIGURE_START_EVENT
事件后,WebXml
将自身存储的信息注入到Context中 —— (关于这个事件,可以在下面的startInternal
样例源码中看到播发的位置) -
以下就是执行顺序了:
ContextConfig
类监听到Lifecycle.CONFIGURE_START_EVENT
事件ContextConfig
类调用自身的configureStart()
方法,进而调用自身的webConfig()
方法 ,最后在将${CATALINA_BASE}/conf/web.xml
配置信息融入自身后,调用WebXml.configureContext(context)
来将解析到配置信息注入到Context实例中。
2. 细节
web.xml
文件的解析是在Context启动时参与的。所以我们将注意力集中到Context启动时的顺序上来。
Tomcat中的Context启动时的顺序是这样的(源码位于:StandardContext
实现的startInternal
)
- 解析
web.xml
(通过触发Lifecycle.CONFIGURE_START_EVENT
事件, 让ContextConfig
类去完成解析web.xml工作, 解析出来的信息由WebXml
承载, 并且由WebXml
实例将它们注入到Context
中); 注意这里是同步回调 ; 具体细节可以参见ContextConfig
类中的源代码。- 其中一个重要环节就是解析
web.xml
(源码位置:ContextConfig.webConfig()
)。 - 另外一个重要环节就是读取lib下的JAR中所有的
META/resources
里的静态资源(源码位置:ContextConfig.processResourceJARs()
)。
- 其中一个重要环节就是解析
- 往**
ServletContext
实例中注入<context-param>
参数** - 回调Servlet3.0的
ServletContainerInitializers
接口实现类;ServletContainerInitializers
接口实现类是在ContextConfig
类中的processServletContainerInitializers();
方法解析到的。 - 触发
Listener
事件(beforeContextInitialized, afterContextInitialized); 这里只会触发ServletContextListener
接口类型的 ; 例如Spring-web中的ContextLoaderListener
就实现了ServletContextListener
接口类型 - 初始化
Filter
, 调用其init
方法 - 加载 **启动时即加载的servlet **
-
(Load-on-startup元素在web应用启动的时候指定了servlet被加载的顺序,它的值必须是一个整数。如果它的值是一个负整数或是这个元素不存在,那么容器会在该servlet被调用的时候,加载这个servlet。如果值是正整数或零,容器在配置的时候就加载并初始化这个servlet,容器必须保证值小的先被加载。如果值相等,容器可以自动选择先加载谁。 )
-
另外在
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. 总结
让我们回顾下加载顺序,以便处理一些奇怪需求能做到心中有数。
-
读取lib下的JAR中所有的
META/resources
里的静态资源。 -
往
ServletContext
实例中注入<context-param>
参数。 -
回调 Servlet3.0的
ServletContainerInitializer
接口实现类 ; 注意ServletContainerInitializer
接口实现类是在ContextConfig.processServletContainerInitializers();
方法解析到的。相关的例子有SpringMVC中的SpringServletContainerInitializer
,以及Log4j2中的Log4jServletContainerInitializer
。以SPI的方式检索。 -
触发
Listener
事件(beforeContextInitialized
,afterContextInitialized
); 这里只会触发ServletContextListener
接口类型的 ; 例如Spring-web中的ContextLoaderListener
就实现了ServletContextListener
接口类型。 -
初始化
Filter
, 调用其init
方法。 -
加载 - 启动时即加载的
Servlet
(Load-on-startup 属性为正或零时,且 值小的先被加载)。 -
以上总结为一句话就是 : Web容器加载顺序为
ServletContext ~ context-param ~ ServletContainerInitializer ~ listener ~ filter ~ servlet
。 -
对于
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>