StandardContext 类介绍
StandardContext 和其他 Container 一样,也是重写了 startInternal 方法。由于涉及到 webapp 的启动流程,需要很多准备工作,比如使用 WebResourceRoot 加载资源文件、利用 Loader 加载 class、使用 JarScanner 扫描 jar 包,等等。因此StandardContext 的启动逻辑比较复杂,这里描述下几个重要的步骤:
1. 创建工作目录,比如$CATALINA_HOME\work\Catalina\localhost\examples;实例化 ContextServlet,应用程序拿到的是 ApplicationContext的外观模式
2. 实例化 WebResourceRoot,默认实现类是 StandardRoot,用于读取 webapp 的文件资源
3. 实例化 Loader 对象,Loader 是 tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class
4. 发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是从 webapp 中读取 servlet 相关的 Listener、Servlet、Filter 等
5. 实例化 Sesssion 管理器,默认使用 StandardManager
6. 调用 listenerStart,实例化 servlet 相关的各种 Listener,并且调用
ServletContextListener
7. 处理 Filter
8. 加载 Servlet
核心方法 为 startInternal():
Tomcat的生命周期机制告诉我们,一个组件的启动过程应该关注它的start方法,这个start方法是典型的模板方法设计模式。LifecycleBase是所有组件都继承的抽象类,该类提供了生命周期相关的通用方法,start()方法也可以在LifecycleBase中找到。
观察start方法,在该方法中定义了组件启动的应进行的操作,又留出一个抽象方法startInternal()方法供子类实现组件自身的操作。
所以来看 StandContext 的 startInternal() 方法。
@Override
protected synchronized void startInternal() throws LifecycleException {
LogPropertiesTest.debug("14、StandardContext : 执行 startInternal() 方法, 执行类 :"+this.getClass());
if(log.isDebugEnabled())
log.debug("Starting " + getBaseName());
// 1.发布正在启动的JMX通知,这样可以通过NotificationListener来监听Web应用的启动。
// Send j2ee.state.starting notification
if (this.getObjectName() != null) {
Notification notification = new Notification("j2ee.state.starting",
this.getObjectName(), sequenceNumber.getAndIncrement());
broadcaster.sendNotification(notification);
}
setConfigured(false);
boolean ok = true;
// Currently this is effectively a NO-OP but needs to be called to
// ensure the NamingResources follows the correct lifecycle
// 2.启动当前维护的JNDI资源。
if (namingResources != null) {
namingResources.start();
}
// 3.初始化临时工作目录,即设置的workDir,默认为$CATALINA-BASE/work/<Engine名称>/<Host名称>/<Context名称>。
// Post work directory
postWorkDirectory();
// 4.初始化当前Context使用的WebResouceRoot并启动。WebResouceRoot维护了Web应用所以的资源集合
// (Class文件、Jar包以及其他资源文件),主要用于类加载器和按照路径查找资源文件。
// Add missing components as necessary
if (getResources() == null) { // (1) Required by Loader
if (log.isDebugEnabled())
log.debug("Configuring default Resources");
try {
setResources(new StandardRoot(this));
} catch (IllegalArgumentException e) {
log.error(sm.getString("standardContext.resourcesInit"), e);
ok = false;
}
}
if (ok) {
// WebResourceRoot #createWebResourceSet ==》 "/WEB-INF/classes/META-INF/resources"
resourcesStart();
}
// 5.创建Web应用类加载器webappLoader,webappLoader继承自LifecycleMBeanBase,在其启动后会去创建Web应用类加载器(ParallelWebappClassLoader)。
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
// 6.如果没有设置Cookie处理器,默认为Rfc6265CookieProcessor。
if (cookieProcessor == null) {
cookieProcessor = new Rfc6265CookieProcessor();
}
// 7.设置字符集映射,用于根据Locale获取字符集编码。
getCharsetMapper();
// Validate required extensions
// 8.web应用的依赖检测。
boolean dependencyCheck = true;
try {
dependencyCheck = ExtensionValidator.validateApplication
(getResources(), this);
} catch (IOException ioe) {
log.error(sm.getString("standardContext.extensionValidationError"), ioe);
dependencyCheck = false;
}
if (!dependencyCheck) {
// do not make application available if dependency check fails
ok = false;
}
// Reading the "catalina.useNaming" environment variable
String useNamingProperty = System.getProperty("catalina.useNaming");
if ((useNamingProperty != null)
&& (useNamingProperty.equals("false"))) {
useNaming = false;
}
// 9.NamingContextListener注册
if (ok && isUseNaming()) {
if (getNamingContextListener() == null) {
NamingContextListener ncl = new NamingContextListener();
ncl.setName(getNamingContextName());
ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite());
addLifecycleListener(ncl);
setNamingContextListener(ncl);
}
}
// Standard container startup
if (log.isDebugEnabled())
log.debug("Processing standard container startup");
// Binding thread
ClassLoader oldCCL = bindThread();
try {
if (ok) {
// Start our subordinate components, if any
// 10.启动Web应用类加载器,此时真正创建出ParallelWebappClassLoader实例。
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}
// since the loader just started, the webapp classloader is now
// created.
setClassLoaderProperty("clearReferencesRmiTargets",
getClearReferencesRmiTargets());
setClassLoaderProperty("clearReferencesStopThreads",
getClearReferencesStopThreads());
setClassLoaderProperty("clearReferencesStopTimerThreads",
getClearReferencesStopTimerThreads());
setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread",
getClearReferencesHttpClientKeepAliveThread());
setClassLoaderProperty("clearReferencesObjectStreamClassCaches",
getClearReferencesObjectStreamClassCaches());
// By calling unbindThread and bindThread in a row, we setup the
// current Thread CCL to be the webapp classloader
unbindThread(oldCCL);
oldCCL = bindThread();
// Initialize logger again. Other components might have used it
// too early, so it should be reset.
logger = null;
getLogger();
// 11.启动安全组件。
Realm realm = getRealmInternal();
if(null != realm) {
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}
// Place the CredentialHandler into the ServletContext so
// applications can have access to it. Wrap it in a "safe"
// handler so application's can't modify it.
CredentialHandler safeHandler = new CredentialHandler() {
@Override
public boolean matches(String inputCredentials, String storedCredentials) {
return getRealmInternal().getCredentialHandler().matches(inputCredentials, storedCredentials);
}
@Override
public String mutate(String inputCredentials) {
return getRealmInternal().getCredentialHandler().mutate(inputCredentials);
}
};
context.setAttribute(Globals.CREDENTIAL_HANDLER, safeHandler);
}
// Notify our interested LifecycleListeners ContextConfig#webConfig()
/**
ContextConfig 它是一个 LifycycleListener,它在 Context 启动过程中是承担了一个非常重要的角色。StandardContext 会发出 CONFIGURE_START_EVENT 事件,而 ContextConfig 会处理该事件,主要目的是通过 web.xml 或者 Servlet3.0 的注解配置,读取 Servlet 相关的配置信息,比如 Filter、Servlet、Listener 等,其核心逻辑在 ContextConfig#webConfig() 方法中实现。下面,我们对 ContextConfig 进行详细分析
CONFIGURE_START_EVENT = "configure_start";
*/
// 12.发布CONFIGURE_START_EVENT事件,ContextConfig 监听该事件以完成 Servlet 的创建。
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
// Start our child containers, if not already started
// 13.启动Context子节点Wrapper。
for (Container child : findChildren()) {
if (!child.getState().isAvailable()) {
child.start();
}
}
// Start the Valves in our pipeline (including the basic),
// if any
// 14.启动Context的pipeline。
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
// Acquire clustered manager
// 15.创建会话管理器。
Manager contextManager = null;
Manager manager = getManager();
if (manager == null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.cluster.noManager",
Boolean.valueOf((getCluster() != null)),
Boolean.valueOf(distributable)));
}
if ( (getCluster() != null) && distributable) {
try {
contextManager = getCluster().createManager(getName());
} catch (Exception ex) {
log.error("standardContext.clusterFail", ex);
ok = false;
}
} else {
contextManager = new StandardManager();
}
}
// Configure default manager if none was specified
if (contextManager != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.manager",
contextManager.getClass().getName()));
}
setManager(contextManager);
}
if (manager!=null && (getCluster() != null) && distributable) {
//let the cluster know that there is a context that is distributable
//and that it has its own manager
getCluster().registerManager(manager);
}
}
if (!getConfigured()) {
log.error(sm.getString("standardContext.configurationFail"));
ok = false;
}
// We put the resources into the servlet context
// 16.将Context的Web资源集合添加到ServletContext。
if (ok)
getServletContext().setAttribute
(Globals.RESOURCES_ATTR, getResources());
// 17.创建实例管理器instanceManager,用于创建对象实例,如Servlet、Filter等。
if (ok ) {
if (getInstanceManager() == null) {
javax.naming.Context context = null;
if (isUseNaming() && getNamingContextListener() != null) {
context = getNamingContextListener().getEnvContext();
}
Map<String, Map<String, String>> injectionMap = buildInjectionMap(
getIgnoreAnnotations() ? new NamingResourcesImpl(): getNamingResources());
setInstanceManager(new DefaultInstanceManager(context,
injectionMap, this, this.getClass().getClassLoader()));
}
getServletContext().setAttribute(
InstanceManager.class.getName(), getInstanceManager());
InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager());
}
// Create context attributes that will be required
// 18.将Jar包扫描器添加到ServletContext。
if (ok) {
getServletContext().setAttribute(
JarScanner.class.getName(), getJarScanner());
}
// Set up the context init params
// 19.合并参数。 指定 ServletContext 的相关参数
mergeParameters();
// 在初始化 Servlet、Listener 之前,便会先调用 ServletContainerInitializer,进行额外的初始化处理。
// 注意:ServletContainerInitializer 需要的是 Class 对象,而不是具体的实例对象,这个时候 servlet 相关的 Listener
// 并没有被实例化,因此不会产生矛盾
// Call ServletContainerInitializers
// 调用 ServletContainerInitializer#onStartup()
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
Set<Class<?>> value = entry.getValue();
/*
* for (Class<?> class1 : value) { LogPropertiesTest.
* debug("--ServletContainerInitializer--------------------14、StandardContext : 执行 startInternal() 方法, class1 :"
* +class1.getClass()); }
*/
// 20.启动添加到Context的ServletContainerInitializer。
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
// 21.实例化应用类监听器ApplicationListener。
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
checkConstraintsForUncoveredMethods(findConstraints());
}
try {
// Start manager
// 22.启动会话管理器。
Manager manager = getManager();
if (manager instanceof Lifecycle) {
((Lifecycle) manager).start();
}
} catch(Exception e) {
log.error(sm.getString("standardContext.managerFail"), e);
ok = false;
}
// Configure and call application filters
// 23.实例化FilterConfig、Filter并调用Filter.init()。
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
// 24.对于loadOnStartup大于等于0的Wrapper,调用Wrapper.load(),该方法负责实例化Servlet,并调用Servlet.init()进行初始化。
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
// Start ContainerBackgroundProcessor thread
// 25.启动后台定时处理程序,只有backgroundProcessorDelay>0才启动,用于监控守护文件的变更。
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
// Set available status depending upon startup success
if (ok) {
if (log.isDebugEnabled())
log.debug("Starting completed");
} else {
log.error(sm.getString("standardContext.startFailed", getName()));
}
startTime=System.currentTimeMillis();
// Send j2ee.state.running notification
// 26.发布正在运行的JMX通知
if (ok && (this.getObjectName() != null)) {
Notification notification =
new Notification("j2ee.state.running", this.getObjectName(),
sequenceNumber.getAndIncrement());
broadcaster.sendNotification(notification);
}
// The WebResources implementation caches references to JAR files. On
// some platforms these references may lock the JAR files. Since web
// application start is likely to have read from lots of JARs, trigger
// a clean-up now.
// 27.释放资源,如关闭jar文件。
getResources().gc();
// 28.设置Context状态。
// Reinitializing if something went wrong
if (!ok) {
setState(LifecycleState.FAILED);
} else {
setState(LifecycleState.STARTING);
}
// StandContext启动很复杂,涉及很多知识面
}
ContextConfig
首先我们看一下 StandardContext 类中的ContextConfig是何时创建的:
1、Bootstrap.load(String[])
2、Catalina #load()
Digester digester = createStartDigester();
3、Catalina # createStartDigester()
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
4、Digester #addRuleSet(RuleSet ruleSet)
public void addRuleSet(RuleSet ruleSet) { // ruleSet == org.apache.catalina.startup.HostRuleSet@687080dc
ruleSet.addRuleInstances(this); // this == org.apache.tomcat.util.digester.Digester@38bc8ab5
}
5、HostRuleSet #addRuleInstances(Digester digester)
// prefix == Server/Service/Engine/
digester.addRule(prefix + "Host",
new LifecycleListenerRule
("org.apache.catalina.startup.HostConfig",
"hostConfigClass"));
digester.addSetNext(prefix + "Host",
"addChild",
"org.apache.catalina.Container");// 通过 Digester 创建 HostConfig, 然后调用 StandardHost 对象的addChild方法,将HostConfig对象添加到StandardHost,
// 也就是添加到StandardHost父类LifecycleBase中的 属性集合中 private final List<LifecycleListener> lifecycleListeners =
// new CopyOnWriteArrayList<>();
6、StandardHost #start()
7、StandardHost #startInternal()
super.startInternal();
8、StandardHost #fireLifecycleEvent(String type, Object data)
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event); // listener = org.apache.catalina.startup.HostConfig@729d991e
}
}
9、 HostConfig #lifecycleEvent(LifecycleEvent event)
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
10、 HostConfig #start()
if (host.getDeployOnStartup())
deployApps();
11、 HostConfig #deployApps()
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);}
12、HostConfig #deployDirectories(File appBase, String[] files)
if (dir.isDirectory()) {
ContextName cn = new ContextName(files[i], false);if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
continue;// ExecutorService es = host.getStartStopExecutor();
results.add(es.submit(new DeployDirectory(this, cn, dir)));
}
13、HostConfig #deployDirectory(ContextName cn, File dir)
Class<?> clazz = Class.forName(host.getConfigClass()); // host.getConfigClass() == private String configClass =
// "org.apache.catalina.startup.ContextConfig";
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener); // listener == org.apache.catalina.startup.ContextConfig@3e11f9e9
至此 HostConfig被添加到 StandardContext对象中。
ContextConfig是创建Context时默认添的一个生命周期监听器。它监听6个事件,其中三个和Context启动关系密切:AFTER_INIT_EVENT、BEFORE_START_EVENT、CONFIGURE_START_EVENT。
ContextConfig的lifecycleEvent()方法:
StandardContext #fireLifecycleEvent(String type, Object data)
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
ContextConfig#lifecycleEvent()方法:
@Override
public void lifecycleEvent(LifecycleEvent event) {
// Identify the context we are associated with
try {
context = (Context) event.getLifecycle();
} catch (ClassCastException e) {
log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}
}
AFTER_INIT_EVENT 事件
严格说,该事件属于Context事件初始化阶段,主要用于Context属性的配置工作。
根据前面讲的,再来回顾一下Context的创建,有以下来源:
解析server.xml中的Context元素。
通过HostConfig部署Web应用时,解析Web应用(或者WAR包)根目录下的META-INF/context.xml文件。如果不存在,则自动创建一个默认的Context对象,只设置name,path,docBase等几个属性。
通诺HostConfig部署Web应用时,解析$CATALINA-BASE/conf/Catalina/localhost目录下的Context部署文件描述符创建。
除了Context创建时的属性配置,Tomcat提供的默认配置也要一并添加到Context实例中,AFTER_INIT_EVENT事件就是要完成这部分工作的。
来看该事件触发时执行的init()方法:
/**
* Process a "init" event for this Context.
1、处理Context的两个默认配置文件:conf/context.xml和/conf/[enginename]/[hostname]/context.xml.default,解析到context中;
2、对war包进行校验:主要是校验目录结构(是否有WEB-INF目录,是否有classes目录和META-INF目录等)
3、对于没有解压的文件还会将其解压:是在ExpandWar类的expand方法中完成的
*/
protected synchronized void init() {
// Called from StandardContext.init()
Digester contextDigester = createContextDigester();
contextDigester.getParser();
if (log.isDebugEnabled()) {
log.debug(sm.getString("contextConfig.init"));
}
context.setConfigured(false);
ok = true;
contextConfig(contextDigester);
}
init首先会创建createContextDigester创建解析规则,点进去看可以发现会回到之前讲Server解析时提到的ContextRuleSet,只不过这时传进去的create参数值为false。
不多说,重点来看contextConfig()方法
protected void contextConfig(Digester digester) {
String defaultContextXml = null;
// Open the default context.xml file, if it exists
if (context instanceof StandardContext) {
defaultContextXml = ((StandardContext)context).getDefaultContextXml();
}
// set the default if we don't have any overrides
if (defaultContextXml == null) {
defaultContextXml = Constants.DefaultContextXml;
}
if (!context.getOverride()) {
File defaultContextFile = new File(defaultContextXml);
if (!defaultContextFile.isAbsolute()) {
defaultContextFile =
new File(context.getCatalinaBase(), defaultContextXml);
}
if (defaultContextFile.exists()) {
try {
URL defaultContextUrl = defaultContextFile.toURI().toURL();
processContextConfig(digester, defaultContextUrl);
} catch (MalformedURLException e) {
log.error(sm.getString(
"contextConfig.badUrl", defaultContextFile), e);
}
}
File hostContextFile = new File(getHostConfigBase(), Constants.HostContextXml);
if (hostContextFile.exists()) {
try {
URL hostContextUrl = hostContextFile.toURI().toURL();
processContextConfig(digester, hostContextUrl);
} catch (MalformedURLException e) {
log.error(sm.getString(
"contextConfig.badUrl", hostContextFile), e);
}
}
}
if (context.getConfigFile() != null) {
processContextConfig(digester, context.getConfigFile());
}
}
看到解析的过程如下:
1.如果Context的override属性为false(默认配置):
1.1 如果存在defaultContextXml即conf/context.xml(Catalina容器级默认配置文件),那么解析该文件,更新Context实例属性。
1.2 如果存在hostContextXml即$CATALINA-BASE/conf/Catalina/localhost/context.xml.default文件(Host级的默认配置),则解析该文件,更新Context实例属性。
2.如果context的configFile不为空(即$CATALINA-BASE/conf/Catalina/localhost下的Context部署描述文件或者Web应用根目录下的META-INF/context.xml文件),那么解析该文件,更新Context实例属性。
看到这会发现configFile其实被解析了两遍,在创建Context时会先解析一遍,这里再被解析一遍,这是什么原因呢?
因为这里会解析conf/context.xml和context.xml.default文件,配置默认属性,如果之前创建Context时已经配置了某个属性,而这个属性又在conf/context.xml和context.xml.default中存在,显然这时会被覆盖,想要配置Context级别的属性不被覆盖,所以这时再解析一遍。
根据上述,可以得出结论:
Tomcat中Context属性的优先级为:configFile > $CATALINA-BASE/conf/Catalina/localhost/context.xml.default > conf/context.xml,即Web应用配置优先级最高,Host级别配置次之,Catalina容器级别最低。