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容器級別最低。