4000 Tomcat源码分析 -- StandardContext的启动
2018-03-09 16:56
525 查看
本篇结构:
前言
StandContext的启动过程
ContextConfig
总结
直接解析server.xml中的Context标签创建、解析Context文件描述符(位于$CATALINA-BASE/conf/Catalina/localhost目录下或者META-INF目录下)、创建默认StandardContext(即没有Context文件描述符,又没在server.xml中配置)。
了解了Web应用的创建和部署后,下面再来看看Web应用的初始化和启动工作。StandardHost和HostConfig只是根据不同情况创建Context对象,具体初始化和启动工作由组件Context自身完成。
观察start方法,在该方法中定义了组件启动的应进行的操作,又留出一个抽象方法startInternal()方法供子类实现组件自身的操作。
所以来看StandContext的startInternal()方法。
该方法所做的操作很多很复杂,下面简单列下,就不深究了。主要是:
1.发布正在启动的JMX通知,这样可以通过NotificationListener来监听Web应用的启动。
2.启动当前维护的JNDI资源。(哼,不懂JNDI)
3.初始化临时工作目录,即设置的workDir,默认为
4.初始化当前Context使用的WebResouceRoot并启动。WebResouceRoot维护了Web应用所以的资源集合(Class文件、Jar包以及其他资源文件),主要用于类加载器和按照路径查找资源文件。
5.创建Web应用类加载器webappLoader,webappLoader继承自LifecycleMBeanBase,在其启动后会去创建Web应用类加载器(ParallelWebappClassLoader)。
同时webappLoader提供了backgroundProcess方法,用于Context后台处理,当检测到Web应用的类文件、Jar包发生变化时,重新加载Context。
6.如果没有设置Cookie处理器,默认为Rfc6265CookieProcessor。
7.设置字符集映射,用于根据Locale获取字符集编码。
8.web应用的依赖检测。
9.NamingContextListener注册。
10.启动Web应用类加载器,此时真正创建出ParallelWebappClassLoader实例。
11.启动安全组件。
12.发布CONFIGURE_START_EVENT事件,ContextConfig监听该事件以完成Servlet的创建。
13.启动Context子节点Wrapper。
14.启动Context的pipeline。
15.创建会话管理器。
16.将Context的Web资源集合添加到ServletContext。
17.创建实例管理器instanceManager,用于创建对象实例,如Servlet、Filter等。
18.将Jar包扫描器添加到ServletContext。
19.合并参数。
20.启动添加到Context的ServletContainerInitializer。
21.实例化应用类监听器ApplicationListener。
22.启动会话管理器。
23.实例化FilterConfig、Filter并调用Filter.init()。
24.对于loadOnStartup大于等于0的Wrapper,调用Wrapper.load(),该方法负责实例化Servlet,并调用Servlet.init()进行初始化。
25.启动后台定时处理程序,只有backgroundProcessorDelay>0才启动,用于监控守护文件的变更。
26.发布正在运行的JMX通知。
27.释放资源,如关闭jar文件。
28.设置Context状态。
StandContext启动很复杂,涉及很多知识面,我是很模糊的,也不打算深究了。
ContextConfig的lifecycleEvent()方法:
根据前面讲的,再来回顾一下Context的创建,有以下来源:
1. 解析server.xml中的Context元素。
2. 通过HostConfig部署Web应用时,解析Web应用(或者WAR包)根目录下的META-INF/context.xml文件。如果不存在,则自动创建一个默认的Context对象,只设置name,path,docBase等几个属性。
3. 通诺HostConfig部署Web应用时,解析$CATALINA-BASE/conf/Catalina/localhost目录下的Context部署文件描述符创建。
除了Context创建时的属性配置,Tomcat提供的默认配置也要一并添加到Context实例中,AFTER_INIT_EVENT事件就是要完成这部分工作的。
来看该事件触发时执行的init()方法:
init首先会创建createContextDigester创建解析规则,点进去看可以发现会回到之前讲Server解析时提到的ContextRuleSet,只不过这时传进去的create参数值为false。
不多说,重点来看contextConfig()方法:
(1)如果Context的override属性为false(默认配置):
①如果存在defaultContextXml即conf/context.xml(Catalina容器级默认配置文件),那么解析该文件,更新Context实例属性。
②如果存在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容器级别最低。
更新Context的docBase属性是为了满足WAR部署的情况。当Web应用为一个WAR压缩且需要解压部署(Hot的unpackWAR=true,且Context的unpackWAR=true)时,docBase属性指向的是解压后的文件夹目录,而非WAR包的路径。
具体过程是在fixDocBase()方法中:
(1)根据Host的appBase以及Context的docBase计算docBase的绝对路径。
(2)如果docBase指向WAR包:
需要解压部署:
①解压WAR文件;
②将Context的docBase设置为解压后的路径。
不需要解压部署:
只检测WAR包,不更新docBase。
(3)如果docBase指向目录:
①如果docBase指向的是有效目录,且存在与该目录同名的WAR包,同时需要解压部署,则重新解压WAR包;
②如果docBase指向的是无效目录,即不存在,但是存在与该目录同名的WAR包,如果需要解压,则解压WAR包,更新Context的docBase为解压路径。
③如果不需要解压部署,则只检测WAR包。
当Context的antiResourceLocking属性为true时,Tomcat会将当前Web应用目录复制到临时文件夹下,以避免对原目录的资源加锁。
CONFIGURE_START_EVENT触发该方法:
该事件主要工作内容:
根据配置创建Wrapper(Servlet)、Filter、ServletContextListener等,完成Web容器的初始化。除了解析Web应用目录下的web.xml外,还包括Tomcat的默认配置、web-fragment.xml、ServletContainerInitializer,以及相关XML文件的排序和合并。
根据Servlet规范,Web应用部署描述可来源于WEB-IN/web.xml、Web应用JAR包中的META-INF/web-fragment.xml和META-INF/services/javax.servlet.ServletContainerInitializer。
除了Servlet规范中提到的部署描述方式,Tomcat还支持默认配置,以简化Web应用的配置工作。这些默认配置有容器级别的(conf/web.xml)和Host级别(conf/Engine名称/Host名称/web.xml.default)。解析中Web应用中的配置优先级最高,Host其次,最后是容器级。
来看webConfig方法:
这个方法是Web容器的初始化,过程涉及内容较多,就简单描述下:
主要是解析默认配置,先解析容器级的配置(conf/web.xml),然后解析Host级别的配置(web.xml.default);
解析Web应用的web.xml,其他的解析结果均会合并到该解析结果中。
扫描素有JAR包,如果有META-INF/web-fragment.xml,解析该分件。
…
最关注的应该是使用web.xml配置Context实例,包括Servlet、Filter、Listener等Servelt规范中支持的组件,这些可以在configureContext()中找到。
当然看到这,至少希望明白web.xml是在这里解析,Servlet是在这里被包装成StandardWrapper,Filter是在这里创建的。
前言
StandContext的启动过程
ContextConfig
总结
一、前言
根据上篇的介绍,我们知道创建StandContext的三种方式:直接解析server.xml中的Context标签创建、解析Context文件描述符(位于$CATALINA-BASE/conf/Catalina/localhost目录下或者META-INF目录下)、创建默认StandardContext(即没有Context文件描述符,又没在server.xml中配置)。
了解了Web应用的创建和部署后,下面再来看看Web应用的初始化和启动工作。StandardHost和HostConfig只是根据不同情况创建Context对象,具体初始化和启动工作由组件Context自身完成。
二、StandContext的启动过程
Tomcat的生命周期机制告诉我们,一个组件的启动过程应该关注它的start方法,这个start方法是典型的模板方法设计模式。LifecycleBase是所有组件都继承的抽象类,该类提供了生命周期相关的通用方法,start()方法也可以在LifecycleBase中找到。观察start方法,在该方法中定义了组件启动的应进行的操作,又留出一个抽象方法startInternal()方法供子类实现组件自身的操作。
所以来看StandContext的startInternal()方法。
该方法所做的操作很多很复杂,下面简单列下,就不深究了。主要是:
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); }
2.启动当前维护的JNDI资源。(哼,不懂JNDI)
if (namingResources != null) { namingResources.start(); }
3.初始化临时工作目录,即设置的workDir,默认为
$CATALINA-BASE/work/<Engine名称>/<Host名称>/<Context名称>。
postWorkDirectory(); private void postWorkDirectory() { // Acquire (or calculate) the work directory path String workDir = getWorkDir(); if (workDir == null || workDir.length() == 0) { // Retrieve our parent (normally a host) name String hostName = null; String engineName = null; String hostWorkDir = null; Container parentHost = getParent(); if (parentHost != null) { hostName = parentHost.getName(); if (parentHost instanceof StandardHost) { hostWorkDir = ((StandardHost)parentHost).getWorkDir(); } Container parentEngine = parentHost.getParent(); if (parentEngine != null) { engineName = parentEngine.getName(); } } if ((hostName == null) || (hostName.length() < 1)) hostName = "_"; if ((engineName == null) || (engineName.length() < 1)) engineName = "_"; String temp = getBaseName(); if (temp.startsWith("/")) temp = temp.substring(1); temp = temp.replace('/', '_'); temp = temp.replace('\\', '_'); if (temp.length() < 1) temp = ContextName.ROOT_NAME; if (hostWorkDir != null ) { workDir = hostWorkDir + File.separator + temp; } else { workDir = "work" + File.separator + engineName + File.separator + hostName + File.separator + temp; } setWorkDir(workDir); }
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) { resourcesStart(); }
5.创建Web应用类加载器webappLoader,webappLoader继承自LifecycleMBeanBase,在其启动后会去创建Web应用类加载器(ParallelWebappClassLoader)。
if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
同时webappLoader提供了backgroundProcess方法,用于Context后台处理,当检测到Web应用的类文件、Jar包发生变化时,重新加载Context。
public void backgroundProcess() { if (reloadable && modified()) { try { Thread.currentThread().setContextClassLoader (WebappLoader.class.getClassLoader()); if (context != null) { context.reload(); } } finally { if (context != null && context.getLoader() != null) { Thread.currentThread().setContextClassLoader (context.getLoader().getClassLoader()); } } } }
6.如果没有设置Cookie处理器,默认为Rfc6265CookieProcessor。
if (cookieProcessor == null) { cookieProcessor = new Rfc6265CookieProcessor(); }
7.设置字符集映射,用于根据Locale获取字符集编码。
getCharsetMapper() public CharsetMapper getCharsetMapper() { // Create a mapper the first time it is requested if (this.charsetMapper == null) { try { Class<?> clazz = Class.forName(charsetMapperClass); this.charsetMapper = (CharsetMapper) clazz.getConstructor().newInstance(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); this.charsetMapper = new CharsetMapper(); } } return this.charsetMapper; }
8.web应用的依赖检测。
9.NamingContextListener注册。
10.启动Web应用类加载器,此时真正创建出ParallelWebappClassLoader实例。
Loader loader = getLoader(); if (loader instanceof Lifecycle) { ((Lifecycle) loader).start(); }
11.启动安全组件。
Realm realm = getRealmInternal(); if(null != realm) { if (realm instanceof Lifecycle) { ((Lifecycle) realm).start(); }
12.发布CONFIGURE_START_EVENT事件,ContextConfig监听该事件以完成Servlet的创建。
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
13.启动Context子节点Wrapper。
for (Container child : findChildren()) { if (!child.getState().isAvailable()) { child.start(); } }
14.启动Context的pipeline。
if (pipeline instanceof Lifecycle) { ((Lifecycle) pipeline).start(); }
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) &a 134d2 mp;& distributable) { try { contextManager = getCluster().createManager(getName()); } catch (Exception ex) { log.error("standardContext.clusterFail", ex); ok = false; } } else { contextManager = new StandardManager(); } }
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()); }
18.将Jar包扫描器添加到ServletContext。
if (ok) { getServletContext().setAttribute( JarScanner.class.getName(), getJarScanner()); }
19.合并参数。
private void mergeParameters() { Map<String,String> mergedParams = new HashMap<>(); String names[] = findParameters(); for (int i = 0; i < names.length; i++) { mergedParams.put(names[i], findParameter(names[i])); } ApplicationParameter params[] = findApplicationParameters(); for (int i = 0; i < params.length; i++) { if (params[i].getOverride()) { if (mergedParams.get(params[i].getName()) == null) { mergedParams.put(params[i].getName(), params[i].getValue()); } } else { mergedParams.put(params[i].getName(), params[i].getValue()); } } ServletContext sc = getServletContext(); for (Map.Entry<String,String> entry : mergedParams.entrySet()) { sc.setInitParameter(entry.getKey(), entry.getValue()); } }
20.启动添加到Context的ServletContainerInitializer。
21.实例化应用类监听器ApplicationListener。
if (ok) { if (!listenerStart()) { log.error(sm.getString("standardContext.listenerFail")); ok = false; } }
22.启动会话管理器。
Manager manager = getManager(); if (manager instanceof Lifecycle) { ((Lifecycle) manager).start(); }
23.实例化FilterConfig、Filter并调用Filter.init()。
if (ok) { if (!filterStart()) { log.error(sm.getString("standardContext.filterFail")); ok = false; } }
24.对于loadOnStartup大于等于0的Wrapper,调用Wrapper.load(),该方法负责实例化Servlet,并调用Servlet.init()进行初始化。
if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail")); ok = false; } }
25.启动后台定时处理程序,只有backgroundProcessorDelay>0才启动,用于监控守护文件的变更。
// Start ContainerBackgroundProcessor thread super.threadStart();
26.发布正在运行的JMX通知。
// Send j2ee.state.running notification if (ok && (this.getObjectName() != null)) { Notification notification = new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber.getAndIncrement()); broadcaster.sendNotification(notification); }
27.释放资源,如关闭jar文件。
// 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. getResources().gc();
28.设置Context状态。
if (!ok) { setState(LifecycleState.FAILED); } else { setState(LifecycleState.STARTING); }
StandContext启动很复杂,涉及很多知识面,我是很模糊的,也不打算深究了。
三、ContextConfig
ContextConfig是创建Context时默认添的一个生命周期监听器。它监听6个事件,其中三个和Context启动关系密切:AFTER_INIT_EVENT、BEFORE_START_EVENT、CONFIGURE_START_EVENT。ContextConfig的lifecycleEvent()方法:
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(); } }
3.1、AFTER_INIT_EVENT事件
严格说,该事件属于Context事件初始化阶段,主要用于Context属性的配置工作。根据前面讲的,再来回顾一下Context的创建,有以下来源:
1. 解析server.xml中的Context元素。
2. 通过HostConfig部署Web应用时,解析Web应用(或者WAR包)根目录下的META-INF/context.xml文件。如果不存在,则自动创建一个默认的Context对象,只设置name,path,docBase等几个属性。
3. 通诺HostConfig部署Web应用时,解析$CATALINA-BASE/conf/Catalina/localhost目录下的Context部署文件描述符创建。
除了Context创建时的属性配置,Tomcat提供的默认配置也要一并添加到Context实例中,AFTER_INIT_EVENT事件就是要完成这部分工作的。
来看该事件触发时执行的init()方法:
protected 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(默认配置):
①如果存在defaultContextXml即conf/context.xml(Catalina容器级默认配置文件),那么解析该文件,更新Context实例属性。
②如果存在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容器级别最低。
3.2、BEFORE_START_EVENT
该事件在Context启动之前触发,主要用于更新docBase属性,解决Web目录锁的问题。protected synchronized void beforeStart() { try { fixDocBase(); } catch (IOException e) { log.error(sm.getString( "contextConfig.fixDocBase", context.getName()), e); } antiLocking(); }
更新Context的docBase属性是为了满足WAR部署的情况。当Web应用为一个WAR压缩且需要解压部署(Hot的unpackWAR=true,且Context的unpackWAR=true)时,docBase属性指向的是解压后的文件夹目录,而非WAR包的路径。
具体过程是在fixDocBase()方法中:
protected void fixDocBase() throws IOException { Host host = (Host) context.getParent(); File appBase = host.getAppBaseFile(); String docBase = context.getDocBase(); if (docBase == null) { // Trying to guess the docBase according to the path String path = context.getPath(); if (path == null) { return; } ContextName cn = new ContextName(path, context.getWebappVersion()); docBase = cn.getBaseName(); } File file = new File(docBase); if (!file.isAbsolute()) { docBase = (new File(appBase, docBase)).getPath(); } else { docBase = file.getCanonicalPath(); } file = new File(docBase); String origDocBase = docBase; ContextName cn = new ContextName(context.getPath(), context.getWebappVersion()); String pathName = cn.getBaseName(); boolean unpackWARs = true; if (host instanceof StandardHost) { unpackWARs = ((StandardHost) host).isUnpackWARs(); if (unpackWARs && context instanceof StandardContext) { unpackWARs = ((StandardContext) context).getUnpackWAR(); } } boolean docBaseInAppBase = docBase.startsWith(appBase.getPath() + File.separatorChar); if (docBase.toLowerCase(Locale.ENGLISH).endsWith(".war") && !file.isDirectory()) { URL war = UriUtil.buildJarUrl(new File(docBase)); if (unpackWARs) { docBase = ExpandWar.expand(host, war, pathName); file = new File(docBase); docBase = file.getCanonicalPath(); if (context instanceof StandardContext) { ((StandardContext) context).setOriginalDocBase(origDocBase); } } else { ExpandWar.validate(host, war, pathName); } } else { File docDir = new File(docBase); File warFile = new File(docBase + ".war"); URL war = null; if (warFile.exists() && docBaseInAppBase) { war = UriUtil.buildJarUrl(warFile); } if (docDir.exists()) { if (war != null && unpackWARs) { // Check if WAR needs to be re-expanded (e.g. if it has // changed). Note: HostConfig.deployWar() takes care of // ensuring that the correct XML file is used. // This will be a NO-OP if the WAR is unchanged. ExpandWar.expand(host, war, pathName); } } else { if (war != null) { if (unpackWARs) { docBase = ExpandWar.expand(host, war, pathName); file = new File(docBase); docBase = file.getCanonicalPath(); } else { docBase = warFile.getCanonicalPath(); ExpandWar.validate(host, war, pathName); } } if (context instanceof StandardContext) { ((StandardContext) context).setOriginalDocBase(origDocBase); } } } // Re-calculate now docBase is a canonical path docBaseInAppBase = docBase.startsWith(appBase.getPath() + File.separatorChar); if (docBaseInAppBase) { docBase = docBase.substring(appBase.getPath().length()); docBase = docBase.replace(File.separatorChar, '/'); if (docBase.startsWith("/")) { docBase = docBase.substring(1); } } else { docBase = docBase.replace(File.separatorChar, '/'); } context.setDocBase(docBase); }
(1)根据Host的appBase以及Context的docBase计算docBase的绝对路径。
(2)如果docBase指向WAR包:
需要解压部署:
①解压WAR文件;
②将Context的docBase设置为解压后的路径。
不需要解压部署:
只检测WAR包,不更新docBase。
(3)如果docBase指向目录:
①如果docBase指向的是有效目录,且存在与该目录同名的WAR包,同时需要解压部署,则重新解压WAR包;
②如果docBase指向的是无效目录,即不存在,但是存在与该目录同名的WAR包,如果需要解压,则解压WAR包,更新Context的docBase为解压路径。
③如果不需要解压部署,则只检测WAR包。
当Context的antiResourceLocking属性为true时,Tomcat会将当前Web应用目录复制到临时文件夹下,以避免对原目录的资源加锁。
3.3、CONFIGURE_START_EVENT
Context在启动之前,会触发CONFIGURE_START_EVENT事件,ContextConfig通过该事件解析web.xml,创建Wrapper(Servlet)、Filter、ServletContextListener等,完成web容器的初始化。CONFIGURE_START_EVENT触发该方法:
protected synchronized void configureStart() { // Called from StandardContext.start() if (log.isDebugEnabled()) { log.debug(sm.getString("contextConfig.start")); } if (log.isDebugEnabled()) { log.debug(sm.getString("contextConfig.xmlSettings", context.getName(), Boolean.valueOf(context.getXmlValidation()), Boolean.valueOf(context.getXmlNamespaceAware()))); } webConfig(); if (!context.getIgnoreAnnotations()) { applicationAnnotationsConfig(); } if (ok) { validateSecurityRoles(); } // Configure an authenticator if we need one if (ok) { authenticatorConfig(); } // Dump the contents of this pipeline if requested if (log.isDebugEnabled()) { log.debug("Pipeline Configuration:"); Pipeline pipeline = context.getPipeline(); Valve valves[] = null; if (pipeline != null) { valves = pipeline.getValves(); } if (valves != null) { for (int i = 0; i < valves.length; i++) { log.debug(" " + valves[i].getClass().getName()); } } log.debug("======================"); } // Make our application available if no problems were encountered if (ok) { context.setConfigured(true); } else { log.error(sm.getString("contextConfig.unavailable")); context.setConfigured(false); } }
该事件主要工作内容:
根据配置创建Wrapper(Servlet)、Filter、ServletContextListener等,完成Web容器的初始化。除了解析Web应用目录下的web.xml外,还包括Tomcat的默认配置、web-fragment.xml、ServletContainerInitializer,以及相关XML文件的排序和合并。
根据Servlet规范,Web应用部署描述可来源于WEB-IN/web.xml、Web应用JAR包中的META-INF/web-fragment.xml和META-INF/services/javax.servlet.ServletContainerInitializer。
除了Servlet规范中提到的部署描述方式,Tomcat还支持默认配置,以简化Web应用的配置工作。这些默认配置有容器级别的(conf/web.xml)和Host级别(conf/Engine名称/Host名称/web.xml.default)。解析中Web应用中的配置优先级最高,Host其次,最后是容器级。
来看webConfig方法:
protected void webConfig() { /* * Anything and everything can override the global and host defaults. * This is implemented in two parts * - Handle as a web fragment that gets added after everything else so * everything else takes priority * - Mark Servlets as overridable so SCI configuration can replace * configuration from the defaults */ /* * The rules for annotation scanning are not as clear-cut as one might * think. Tomcat implements the following process: * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of * which Servlet spec version is declared in web.xml. The EG has * confirmed this is the expected behaviour. * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main * web.xml is marked as metadata-complete, JARs are still processed * for SCIs. * - If metadata-complete=true and an absolute ordering is specified, * JARs excluded from the ordering are also excluded from the SCI * processing. * - If an SCI has a @HandlesType annotation then all classes (except * those in JARs excluded from an absolute ordering) need to be * scanned to check if they match. */ WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(), context.getXmlValidation(), context.getXmlBlockExternal()); Set<WebXml> defaults = new HashSet<>(); defaults.add(getDefaultWebXmlFragment(webXmlParser)); Set<WebXml> tomcatWebXml = new HashSet<>(); tomcatWebXml.add(getTomcatWebXmlFragment(webXmlParser)); WebXml webXml = createWebXml(); // Parse context level web.xml InputSource contextWebXml = getContextWebXmlSource(); if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) { ok = false; } ServletContext sContext = context.getServletContext(); // Ordering is important here // Step 1. Identify all the JARs packaged with the application and those // provided by the container. If any of the application JARs have a // web-fragment.xml it will be parsed at this point. web-fragment.xml // files are ignored for container provided JARs. Map<String,WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser); // Step 2. Order the fragments. Set<WebXml> orderedFragments = null; orderedFragments = WebXml.orderWebFragments(webXml, fragments, sContext); // Step 3. Look for ServletContainerInitializer implementations if (ok) { processServletContainerInitializers(); } if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) { // Step 4. Process /WEB-INF/classes for annotations and // @HandlesTypes matches Map<String,JavaClassCacheEntry> javaClassCache = new HashMap<>(); if (ok) { WebResource[] webResources = context.getResources().listResources("/WEB-INF/classes"); for (WebResource webResource : webResources) { // Skip the META-INF directory from any JARs that have been // expanded in to WEB-INF/classes (sometimes IDEs do this). if ("META-INF".equals(webResource.getName())) { continue; } processAnnotationsWebResource(webResource, webXml, webXml.isMetadataComplete(), javaClassCache); } } // Step 5. Process JARs for annotations and // @HandlesTypes matches - only need to process those fragments we // are going to use (remember orderedFragments includes any // container fragments) if (ok) { processAnnotations( orderedFragments, webXml.isMetadataComplete(), javaClassCache); } // Cache, if used, is no longer required so clear it javaClassCache.clear(); } if (!webXml.isMetadataComplete()) { // Step 6. Merge web-fragment.xml files into the main web.xml // file. if (ok) { ok = webXml.merge(orderedFragments); } // Step 7a // merge tomcat-web.xml webXml.merge(tomcatWebXml); // Step 7b. Apply global defaults // Have to merge defaults before JSP conversion since defaults // provide JSP servlet definition. webXml.merge(defaults); // Step 8. Convert explicitly mentioned jsps to servlets if (ok) { convertJsps(webXml); } // Step 9. Apply merged web.xml to Context if (ok) { configureContext(webXml); } } else { webXml.merge(tomcatWebXml); webXml.merge(defaults); convertJsps(webXml); configureContext(webXml); } if (context.getLogEffectiveWebXml()) { log.info("web.xml:\n" + webXml.toXml()); } // Always need to look for static resources // Step 10. Look for static resources packaged in JARs if (ok) { // Spec does not define an order. // Use ordered JARs followed by remaining JARs Set<WebXml> resourceJars = new LinkedHashSet<>(); for (WebXml fragment : orderedFragments) { resourceJars.add(fragment); } for (WebXml fragment : fragments.values()) { if (!resourceJars.contains(fragment)) { resourceJars.add(fragment); } } processResourceJARs(resourceJars); // See also StandardContext.resourcesStart() for // WEB-INF/classes/META-INF/resources configuration } // Step 11. Apply the ServletContainerInitializer config to the // context if (ok) { for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializerClassMap.entrySet()) { if (entry.getValue().isEmpty()) { context.addServletContainerInitializer( entry.getKey(), null); } else { context.addServletContainerInitializer( entry.getKey(), entry.getValue()); } } } }
这个方法是Web容器的初始化,过程涉及内容较多,就简单描述下:
主要是解析默认配置,先解析容器级的配置(conf/web.xml),然后解析Host级别的配置(web.xml.default);
解析Web应用的web.xml,其他的解析结果均会合并到该解析结果中。
扫描素有JAR包,如果有META-INF/web-fragment.xml,解析该分件。
…
最关注的应该是使用web.xml配置Context实例,包括Servlet、Filter、Listener等Servelt规范中支持的组件,这些可以在configureContext()中找到。
四、总结
这篇很模糊不清,很潦草,没有太大参考价值,更多的还是自己去看源码吧。当然看到这,至少希望明白web.xml是在这里解析,Servlet是在这里被包装成StandardWrapper,Filter是在这里创建的。
相关文章推荐
- Tomcat源码分析之Context的创建与启动分析
- Tomcat源码分析(3)--StandardServer类中涉及到的初始化和启动
- Spring boot源码分析-AnnotationConfigApplicationContext非web环境下的启动容器(2)
- Tomcat源码分析(一)--服务启动
- TOMCAT源码分析(启动框架)
- tomcat源码分析-Connector初始化与启动
- Tomcat源码分析之―具体启动流程分析
- tomcat源码分析 MBeanFactory#createStandardContext过程
- 【Tomcat9源码分析】生命周期、启动、停止概述
- TOMCAT源码分析(启动框架)
- Tomcat源码分析 -- Tomcat的启动过程(二)
- tomcat 4.1.30启动过程的源码分析
- Tomcat源码分析(七)--单一启动/关闭机制(生命周期)
- tomcat源码研读笔记—tomcat的接收请求之四 StandardContext接收请求
- Tomcat源码阅读之StandardEngine分析与Valve的设计
- Tomcat源码阅读之StandardHost与HostConfig的分析
- tomcat7启动流程源码分析
- TOMCAT源码分析(启动框架)
- 关于Spring启动时Context加载源码分析
- tomcat源码分析(二)启动---Debug方式