前言
本文是对Tomcat生命周期内容进行扩展和强化的第一篇文章,在上一篇文章中以StandarServer
为例,从宏观上分析了容器的生命周期流转过程,分析了LifeEvent
的设计思想,在此基础上,本文着重于分析StandardService
下所有Container
相关子容器的初始化和启动过程。
文中涉及的很多知识已在前面的文章中做了铺垫,重复的内容就不在累述。建议读者按着Tomcat系列文章的顺序阅读下来,否则可能会造成一定的理解困难,在遇到之前已经提及过的知识点时,本文都会以“在某某文章中说过某某”类似的语句进行提醒
在Tomcat架构中各个组件和组件间关系(二)中曾经提过,Tomcat从整体架构上可以分为两大部分:监听请求并生成对应Request
和Response
的Connector
连接器,以及处理请求和控制Tomcat容器运转的Container
。再联系上篇生命周期文章中图9中对应的三大部分,我们以container.init()
和connector.init()
两者作为切入点,开始对组件的初始化进行分析(第二部分executor.init()
,因为默认是不配置连接池的,所以可以认为该部分无效),分析入口如下所示
前文中提到过
Container
的顶层容器为StandardEngine
,结合模板方法的设计可知,container.init()
最终会调用StandardEngine.initInternal()
其中
getRealm()
主要用于获取在server.xml
上配置的<Realm>
域对象,而域对象的作用之前也说过,主要用于安全性认证。除此之外就剩下简单调用父类的initInternal()
。看到这里有些读者可能会产生疑惑,之前说过容器间的初始化是“父传子”,“子传孙”的责任链模式,怎么刚到StandardEngine
就断了呢?其实可以这么理解,责任链开始必定是由外到内的过程,当最内层执行完一定返回上一层,也就是再经历由内到外的逆向过程,我们来看看在逆向的过程中发生了什么(暂且忽略StandardService
的initInternal()
的剩下部分)初始化方法的最初入口在
Catalina
类中的load()
,load()
会调用getServer().init()
,最终对应LifecycleBase
的init()
,如上图所示,这里的initInternal()
就是责任链的入口,当返回时会设置初始化结束生命周期状态LifecycleState.INITIALIZED
,对应的生命周期事件为Lifecycle.AFTER_INIT_EVENT
在设置生命周期状态的同时会发布对应的生命周期状态给对该事件“感兴趣”的监听器,我们看看哪些监听器会对这里的
Lifecycle.AFTER_INIT_EVENT
做出响应。在Tomcat架构中各个组件及组件间关系(二)中的图20,有关ContextConfig
有关介绍时提过,该监听器会对初始化结束事件作出相应,主要工作为初始化解析两种web.xml
文件的解析器webDigester
和webFragmentDigester
,为接下来的启动事件解析web.xml
做准备。至此Container
容器的初始化工作结束,回到图1中开始分析Connector
的init()
方法Connector
的初始化过程同样遵循之前所说的“模板方法”设计模式,最终会走到上图中Connector
自身实现的initInternal()
,方法中CoyoteAdapter
可以理解为Connector
和Container
之间的桥梁,也就是说将Connector
中接收的请求交给Container
一系列容器处理的流程就是该类负责的,这里将当前的Connector
实例通过构造器传递给了CoyoteAdapter
,又因为Connector
和StandardService
存在双向关联关系,那么我们就可以在CoyoteAdapter
中得到Connector
对应的StandardService
,进而得到StandardService
下的StandardEngine
,并将request
交于一系列容器进行处理,具体的代码下文讲对应流程时会看到,由于在整个初始化和启动流程中类与类之间的关系比较复杂,因此,我按照分析的关键功能画了一张大致的类图,有助于下面的理解和分析图5中代码将创建好的adapter
与protocolHandler
进行了关联,在前文中分析过,默认情况protocolHandler
就是Http11Protocol
的实例,在Tomcat架构中各个组件及组件间关系(二)中分析过,该实例是在Digester
解析ConnectorCreateRule
时创建Connector
对象的同时创建的
Http11Protocol
代表了对HTTP1.1协议进行处理的类,初始化时又创建了JIoEndpoint
和Http11ConnectionHandler
的实例,前者用于处理端到端的socket io请求,根据I/O方式的不同又可分为AprEndpoint
、JIoEndpoint
、NioEndpoint
;后者主要用于创建对应协议请求的处理器。((JIoEndpoint) endpoint).setHandler(cHandler)
建立了两者之间的关系,最后三行代码分别设置了关闭Socket
延迟开关、Socket
连接超时时间和开启tcpNoDelay
选项回到图5对协议处理类
protocolHandler
进行初始化,底层调用了所有协议处理类的父类AbstractHandler
的init()
对于Http的bio请求方式来说,这里
endpointName
为http-bio-8080
,协议处理类的初始化主要对相应的endpoint
进行初始化又来一个模板方法,在父类
AbstractEndpoint
中抽象了bind()
,交由不同类型的端到端类进行实现,本文中必然就是对应JIoEndpoint
的bind()
,对应代码清单1
@Override
public void bind() throws Exception {
// Initialize thread count defaults for acceptor
// (1)
if (acceptorThreadCount == 0) {
acceptorThreadCount = 1;
}
// Initialize maxConnections
// (2)
if (getMaxConnections() == 0) {
// User hasn't set a value - use the default
setMaxConnections(getMaxThreadsInternal());
}
// (3)
if (serverSocketFactory == null) {
if (isSSLEnabled()) {
serverSocketFactory =
handler.getSslImplementation().getServerSocketFactory(this);
} else {
serverSocketFactory = new DefaultServerSocketFactory(this);
}
}
// (4)
if (serverSocket == null) {
try {
if (getAddress() == null) {
serverSocket = serverSocketFactory.createSocket(getPort(),
getBacklog());
} else {
serverSocket = serverSocketFactory.createSocket(getPort(),
getBacklog(), getAddress());
}
} catch (BindException orig) {
String msg;
if (getAddress() == null)
msg = orig.getMessage() + " <null>:" + getPort();
else
msg = orig.getMessage() + " " +
getAddress().toString() + ":" + getPort();
BindException be = new BindException(msg);
be.initCause(orig);
throw be;
}
}
}
标注1处涉及一个成员变量acceptorThreadCount
,该变量在AbstractEndpoint
中,表示等待Socket
连接的Acceptor
线程个数。Acceptor
是一个在AbstractEndpoint
中定义的抽象内部类,该类实现了Runnable
接口
该类中仅仅定义了
Acceptor
的几种状态,并没有实现run()
,那必然就是在其子类中实现了,为了流程分析的整体性,我们暂且跳过JIoEndpoint
中Acceptor
的具体实现,因为只有在启动时,该线程才会执行,待讲解到启动流程再做分析,我们接着代码清单1往下看标注2处设置了每个
Endpoint
允许的最大连接数,需要注意的是,这里不要和Acceptor
线程的连接数acceptorThreadCount
混淆,当getMaxConnections()
返回0时,socket允许最大连接数就由getMaxThreadsInternal()
指定在初始化
JIoEndpoint
时,已经通过setMaxConnections(int maxCon)
将父类maxConnections
置为0,那么最大连接数即为AbstractEndpoint
类中的成员变量maxThreads = 200
标注3创建了
ServerSocket
工厂,根据是否在server.xml
中开启SSL安全协议serverSocketFactory
共有两种创建方式,默认不开启,对应DefaultServerSocket
。标注4根据这里的serverSocketFactory
创建对应的serverSocket
,到这里大家肯定都非常熟悉了,服务端套接字嘛。至此,所有容器和连接器的初始化工作结束,下面我们来看容器的启动过程在Tomcat的生命周期一文中,我们分析到
StandardServer
的启动,StandardServer
启动会调动子容器StandardService
的startInternal()
同的初始化流程一样,
StandardService
的启动流程也分为三部分,我们重点依然是Container
容器和Connector
。Container.start()
对应StandardEngine.startInternal()
比
StandardServer
和StandardService
启动不同的是Container
的子容器除了StandardContext
外都会调用公共父类ContainerBase
的startInternal
findChildren()
拿到成员变量HashMap<String, Container> children
的所有value,该集合是在Digester
解析容器对应规则的时候通过addChild(Container)
放入值的,比如对于这里的StandardEngine
来说,StandardHost
是他的children
,StartChild
实现了Callable
接口,其call()
调用了child.start()
,对应StandardHost
的startInternal()
方法中主要做了两件事:1. 给
StandardHost
的StandardPipeline
又添加了一个阀门ErrorReportValve
;2.继续调用ContainerBase
的startInternal
。但需要注意的是对于StandardHost
来说children
肯定是StandardContext
,此时通过findChildren()
得到的StandardContext
实际上是通过解析server.xml
中的<Context>
转变而来,该StandardContext
和通常意义上的webapps/xxx.war
并没有对应关系,况且很多时候我们并不会在server.xml
中添加<Context>
标签,而对于真正的StandardContext
的解析并不是通过StandardHost.findChildren()
得到。这时候我们需要看图14的第二个红框处,setState(LifecycleState.STARTING)
给StandardHost
的生命周期状态设为STARTING
(注意此时流程已经进入StandardHost
,虽然图14对应的是StandardEngine
,但是两者都会调用ContainerBase.startInternal()
,代码是一样的,下面的说明也需要注意这一点),并发送对应的事件START_EVENT
给对应的监听器HostConfig
重点在于最后一个判断,默认情况下
host.getDeployOnStartup()
返回成员变量deployOnStartup
为true,表示一启动就加载web应用共有web应用部署方式:1.XML描述符;2.WAR;3.扩展文件夹,我们选择WAR方式进行分析。
appBase()
得到${catalina.base}/webapps
下对应的文件,filterAppPaths(appBase.list())
得到/webapps
下所有的war文件,并滤除一些排除项,代码清单2 展示了deployWARs
的具体逻辑
protected void deployWARs(File appBase, String[] files) {
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor();
List<Future<?>> results = new ArrayList<Future<?>>();
for (int i = 0; i < files.length; i++) {
if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File war = new File(appBase, files[i]);
if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
war.isFile() && !invalidWars.contains(files[i]) ) {
ContextName cn = new ContextName(files[i], true);
if (isServiced(cn.getName())) {
continue;
}
if (deploymentExists(cn.getName())) {
DeployedApplication app = deployed.get(cn.getName());
boolean unpackWAR = unpackWARs;
if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
}
if (!unpackWAR && app != null) {
// Need to check for a directory that should not be
// there
File dir = new File(appBase, cn.getBaseName());
if (dir.exists()) {
if (!app.loggedDirWarning) {
log.warn(sm.getString(
"hostConfig.deployWar.hiddenDir",
dir.getAbsoluteFile(),
war.getAbsoluteFile()));
app.loggedDirWarning = true;
}
} else {
app.loggedDirWarning = false;
}
}
continue;
}
// Check for WARs with /../ /./ or similar sequences in the name
if (!validateContextPath(appBase, cn.getBaseName())) {
log.error(sm.getString(
"hostConfig.illegalWarName", files[i]));
invalidWars.add(files[i]);
continue;
}
results.add(es.submit(new DeployWar(this, cn, war)));
}
}
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployWar.threaded.error"), e);
}
}
}
遍历每一个war包,对文件名称进行校验和特殊字符的处理,判断文件名对应的war包是否已经运行是否已经部署成功,最后将文件及其对应信息包装成DeployWar
放入线程池中进行部署,由于DeployWar
实现了Runable
,所以这里的es.submit
使用的是ExectuorService.submit(Runnable)
这个重载方法,得到的Future
中并没有返回值,下面result.get()
的目的只是为了阻塞让所有的DeployWar
任务执行完毕,我们来看DeployWar
做了什么
DeployWar
中只是将当前需要加载的ContextName和对应的war文件传递给deployWAR(String, File)
,如代码清单3
protected void deployWAR(ContextName cn, File war) {
// Checking for a nested /META-INF/context.xml
JarFile jar = null;
InputStream istream = null;
FileOutputStream fos = null;
BufferedOutputStream ostream = null;
File xml = new File(appBase(),
cn.getBaseName() + "/META-INF/context.xml");
boolean xmlInWar = false;
try {
jar = new JarFile(war);
JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
if (entry != null) {
xmlInWar = true;
}
} catch (IOException e) {
/* Ignore */
} finally {
if (jar != null) {
try {
jar.close();
} catch (IOException ioe) {
// Ignore;
}
jar = null;
}
}
//
Context context = null;
try {
if (deployXML && xml.exists() && unpackWARs && !copyXML) {
synchronized (digesterLock) {
try {
context = (Context) digester.parse(xml);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
war.getAbsolutePath()), e);
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
}
}
context.setConfigFile(xml.toURI().toURL());
} else if (deployXML && xmlInWar) {
synchronized (digesterLock) {
try {
jar = new JarFile(war);
JarEntry entry =
jar.getJarEntry(Constants.ApplicationContextXml);
istream = jar.getInputStream(entry);
context = (Context) digester.parse(istream);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
war.getAbsolutePath()), e);
} finally {
digester.reset();
if (istream != null) {
try {
istream.close();
} catch (IOException e) {
/* Ignore */
}
istream = null;
}
if (jar != null) {
try {
jar.close();
} catch (IOException e) {
/* Ignore */
}
jar = null;
}
if (context == null) {
context = new FailedContext();
}
context.setConfigFile(
UriUtil.buildJarUrl(war, Constants.ApplicationContextXml));
}
}
} else if (!deployXML && xmlInWar) {
// Block deployment as META-INF/context.xml may contain security
// configuration necessary for a secure deployment.
log.error(sm.getString("hostConfig.deployDescriptor.blocked",
cn.getPath(), Constants.ApplicationContextXml,
new File(configBase(), cn.getBaseName() + ".xml")));
} else {
context = (Context) Class.forName(contextClass).newInstance();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployWar.error",
war.getAbsolutePath()), t);
} finally {
if (context == null) {
context = new FailedContext();
}
}
boolean copyThisXml = false;
if (deployXML) {
if (host instanceof StandardHost) {
copyThisXml = ((StandardHost) host).isCopyXML();
}
// If Host is using default value Context can override it.
if (!copyThisXml && context instanceof StandardContext) {
copyThisXml = ((StandardContext) context).getCopyXML();
}
if (xmlInWar && copyThisXml) {
// Change location of XML file to config base
xml = new File(configBase(), cn.getBaseName() + ".xml");
try {
jar = new JarFile(war);
JarEntry entry =
jar.getJarEntry(Constants.ApplicationContextXml);
istream = jar.getInputStream(entry);
fos = new FileOutputStream(xml);
ostream = new BufferedOutputStream(fos, 1024);
byte buffer[] = new byte[1024];
while (true) {
int n = istream.read(buffer);
if (n < 0) {
break;
}
ostream.write(buffer, 0, n);
}
ostream.flush();
} catch (IOException e) {
/* Ignore */
} finally {
if (ostream != null) {
try {
ostream.close();
} catch (IOException ioe) {
// Ignore
}
ostream = null;
}
if (fos != null) {
try {
fos.close();
} catch (IOException ioe) {
// Ignore
}
fos = null;
}
if (istream != null) {
try {
istream.close();
} catch (IOException ioe) {
// Ignore
}
istream = null;
}
if (jar != null) {
try {
jar.close();
} catch (IOException ioe) {
// Ignore;
}
jar = null;
}
}
}
}
DeployedApplication deployedApp = new DeployedApplication(cn.getName(),
xml.exists() && deployXML && copyThisXml);
long startTime = 0;
// Deploy the application in this WAR file
if(log.isInfoEnabled()) {
startTime = System.currentTimeMillis();
log.info(sm.getString("hostConfig.deployWar",
war.getAbsolutePath()));
}
try {
// Populate redeploy resources with the WAR file
deployedApp.redeployResources.put
(war.getAbsolutePath(), Long.valueOf(war.lastModified()));
if (deployXML && xml.exists() && copyThisXml) {
deployedApp.redeployResources.put(xml.getAbsolutePath(),
Long.valueOf(xml.lastModified()));
} else {
// In case an XML file is added to the config base later
deployedApp.redeployResources.put(
(new File(configBase(),
cn.getBaseName() + ".xml")).getAbsolutePath(),
Long.valueOf(0));
}
// (1)
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener =
(LifecycleListener) clazz.newInstance();
context.addLifecycleListener(listener);
context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName() + ".war");
host.addChild(context);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployWar.error",
war.getAbsolutePath()), t);
} finally {
// If we're unpacking WARs, the docBase will be mutated after
// starting the context
boolean unpackWAR = unpackWARs;
if (unpackWAR && context instanceof StandardContext) {
unpackWAR = ((StandardContext) context).getUnpackWAR();
}
if (unpackWAR && context.getDocBase() != null) {
File docBase = new File(appBase(), cn.getBaseName());
deployedApp.redeployResources.put(docBase.getAbsolutePath(),
Long.valueOf(docBase.lastModified()));
addWatchedResources(deployedApp, docBase.getAbsolutePath(),
context);
if (deployXML && !copyThisXml && (xmlInWar || xml.exists())) {
deployedApp.redeployResources.put(xml.getAbsolutePath(),
Long.valueOf(xml.lastModified()));
}
} else {
// Passing null for docBase means that no resources will be
// watched. This will be logged at debug level.
addWatchedResources(deployedApp, null, context);
}
// Add the global redeploy resources (which are never deleted) at
// the end so they don't interfere with the deletion process
addGlobalRedeployResources(deployedApp);
}
deployed.put(cn.getName(), deployedApp);
if (log.isInfoEnabled()) {
log.info(sm.getString("hostConfig.deployWar.finished",
war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
}
}
代码逻辑比较长,我们挑重点的讲。标注1为每一个StandardContext
创建一个监听器ContextConfig
,该监听器的值是写死的,并通过host.getConfigClass()
获得,之后为StandardContext
设置名称、路劲、版本等信息,最后调用host.addChild(Container)
建立StandardHost
与所有StandardContext
的关联。流程又走入生命周期的一个大循环内,调用LifecycleBase.start()
,由于此时StandardContext
刚刚创建出来,其生命周期状态为NEW
,并不会进入启动流程而是先进行init()
由于在
StandardHost.initInternal()
没做什么关键操作,这里就不做分析了,之后在LifecycleBase
中会向ContextConfig
发送AFTER_INIT_EVENT
事件,此时ContextConfig
会对该事件做出响应,调用init()
进行web.xml
文件的解析规则设置,具体的分析过程已经在Tomcat架构中各个组件及组件间关系(二)讲过图14中的最后一句
threadStart()
用于启动ContainerBase
中的ContainerBackgroundProcessor
线程,同样在Tomcat架构中各个组件及组件间关系(二)提过,该线程的启动有一个先决条件,就是backgroundProcessorDelay > 0
,而Container
子容器中只有StandardEngine
对该值进行了覆盖,满足大于0的条件,因此可以说该线程启动入口只在StandardEngine
启动时调用父类的startInternal()
中。线程中会调用processChildren()
从图中可以看出虽然线程的启动入口只在
StandardEngine
启动时,但代码采用了递归开启的方式,使得StandardEngine
下所有的子容器都能执行backgroundProcess()
的具体实现,如代码清单4
@Override
public void backgroundProcess() {
if (!getState().isAvailable())
return;
if (cluster != null) {
try {
cluster.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);
}
}
if (loader != null) {
try {
loader.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);
}
}
if (manager != null) {
try {
manager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e);
}
}
Realm realm = getRealmInternal();
if (realm != null) {
try {
realm.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e);
}
}
Valve current = pipeline.getFirst();
while (current != null) {
try {
current.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e);
}
current = current.getNext();
}
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
cluster
、realm
调用backgroundProcess()
非本文重点,这里不做分析。我们来看看loader.backgroundProcess()
,在违反ClassLoader双亲委派机制三部曲第二部——Tomcat类加载机制中曾今阐述过war包加载需要依赖WebappLoader
,而StandardContext
又是个war一一对应的,那很明显这里就是调用WebappLoader
的backgroundProcess()
reloadable
是<Context>
的一个属性,用于标明是否运行war在文件改动后自动重新加载,modified()
用于检测war中是否有class或者resource文件存在改动,如果两者都为true则会调用StandardContext.reload()
,该方法的逻辑其实也非常简单,就是先pause容器,在stop容器,最后start。我们回到代码清单4中最后一行,代码向Container
的所有子容器发送了PERIODIC_EVENT
,其中HostConfig
会对该事件做出响应,调用该类的check()
图中的代码其实很大一部分就是上面说如果发布war包流程的重复,if中判断
<Host>
中autoDeploy
自动部署属性是否打开,再从已部署应用的Map<String, DeployedApplication> deployed
集合中得到所有war,进行部署前的校验,最后再次调用deployApps()
对三种形式的文件进行发布至此,
Container
下所有容器的初始化和启动流程基本分析完毕,Connector
分析了初始化流程,还顺带解释了Tomcat自动加载的原理,由于篇幅不宜过长,因此将Connector
的启动过程放在下一篇文章中再做分析