在本章节中,以下几个问题会被回答:
- web容器和servlet容器的区别是什么;
- 在springMVC中的web.xml是什么时候加载到tomcat中的;
- tomcat是怎么加载我们的web服务的;
- tomcat是怎么实现的热部署;
1 Container基本结构
从上文中有讲到,Connector和Container的初始化工作是由Digester来解析conf/server.xml来完成的。而在server.xml中已经告诉我们了Container的基本结构。那么我们先来看看server.xml文件:
<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
……
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
如果有接触过xml文件,那么我们可以很清晰的看到,在这个xml文件中,Server下包含了Service,Service下包含了Connector和Engine,Engin下包含了Host。是时候放出一张图了。
2 Container初始化
2.1 Container组件构成
继续看看Container容器的结构图,这个图很大众化,是个Container容器介绍的文章都会有:
通过代码,我们会知道Container是一个接口,Container分成4个级别的容器,而且这四个级别容器的关系为父子关系。真正的顶层容器是Engine。Container作为容器,存在几个概念上的级别:
- Engine 表示一个Servlet引擎,它可以包含一个或多个子容器,比如Host或者Context容器;
- Host 表示一台虚拟的主机,它可以包含一系列Context容器;
- Context 表示一个唯一的ServletContext,一个 Context 对应一个 Web 工程,它可以包含一个 或多个Wrapper容器;
- Wrapper 表示一个独立的Servlet定义,即Wrapper本质就是对Servlet进行了一层包装。
通过这个图我们知道了Container的结构,那么Container的初始化工作是怎么完成的呢?
继续回到Catalina类中,在load方法中调用了createStartDigester方法。
protected Digester createStartDigester() {
……
////如果遇到”Server“元素起始符;则创建"org.apache.catalina.core.StandardServer"的一个实例对象,并压入堆栈;如果"Server"元素的"className"属性存在,那么用这个属性的值所指定的class来创建实例对象,并压入堆栈。
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
//从server.xml读取"Server"元素的所有{属性:值}配对,用对应的Setter方法将属性值设置到堆栈顶层元素(Server)。
digester.addSetProperties("Server");
//遇到"Server"结束符时,调用“次顶层元素(Catalina)”的"setServer"方法,
digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
……
//遇到标签使用规则
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
……
}
从简化的源码中可以看到,digester对server.xml设置的标签动作有5种调用:
- addObjectCreate:遇到起始标签的元素,初始化一个实例对象入栈
- addSetProperties:遇到某个属性名,使用setter来赋值
- addSetNext:遇到结束标签的元素,调用相应的方法
- addRule:调用rule的begin 、body、end、finish方法来解析xml,入栈和出栈给对象赋值
- addRuleSet:调用addRuleInstances来解析xml标签
从这些规则和xml中可以看到,Calatina的Server对象是StandardServer。
StandardService包含了多个Connector(xml中有2个connector)和一个StandardEngine Container。
StandardEngine包含了一个Host Container
2.2 Context容器加载web服务与热部署
从confg/server.xml中我们可以看到Server的容器的初始化只有Engine和Host,那么Context是什么时候初始化的呢,是怎么加载我们的web application,怎么实现的热部署呢?
先说结论,tomcat的Engine会启动一个线程,该线程每10s会发送一个发送一个事件,监听到该事件的部署配置类会自动去扫描webapp文件夹下的war包,将其加载成一个Context,即启动一个web服务。
OK,回过头看conf/server.xml和createStartDigester,添加了HostRuleSet,进入在HostRuleSet类中,可以看到这么一行代码:
digester.addRule(prefix + "Host",
new LifecycleListenerRule
("org.apache.catalina.startup.HostConfig",
"hostConfigClass"));
继续进入LifecycleListenerRule类可以发现,在监听事件中增加了HostConfig类的对象,也就是说StandardHost中新增了一个HostConfig监听器。
再回过头来进入StandardEngine的starInternal方法supper.startInternal(父类ContainerBase)中有这行代码:
threadStart();
进入后发现开启了一个线程,调用ContainerBackgroundProcessor这个的run方法,而这个run方法可以看到
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
……
try {
while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);//在StandardEngine中构造方法设置默认backgroundProcessorDelay=10,即10s调用一次
} catch (InterruptedException e) {
// Ignore
}
if (!threadDone) {
……
processChildren(parent, cl);
}
}
}
……
}
}
也就是说每该线程每10s会调用一次processChildren,继续跟踪该方法,会看到调用其子容器Engine、Host、Context、Wrapper各容器组件及与它们相关的其它组件的backgroundProcess方法。
@Override
public void backgroundProcess() {
if (loader != null) {
try {
loader.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);
}
}
……
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
这个方法中比较重要的两个
loader.backgroundProcess():调用了载入器的WebappLoader的backgroundProcess方法,进入这个方法可以看到:
public void backgroundProcess() {
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (container instanceof StandardContext) {
((StandardContext) container).reload();
}
} finally {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
}
} else {
closeJARs(false);
}
}
看判断条件reloadable和modified(),reloadable即为是否开启热部署,而modified()则是当前文件是否有修改的判断,当开启了热部署且有修改就会调用Context的reload方法进行重加载,实现web服务的热部署。
fireLifecycleEvent:对容器的监听对象发送Lifecycle.PERIODIC_EVENT事件,调用LifecycleListener的lifecycleEvent。
public void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);
LifecycleListener interested[] = listeners;
for (int i = 0; i < interested.length; i++)
interested[i].lifecycleEvent(event);
}
好的,前面说到StandardHost通server.xml配置了HostConfig监听器,那么进入HostConfig查看对该事件的响应方法lifecycleEvent
public void lifecycleEvent(LifecycleEvent event) {
// Identify the host we are associated with
try {
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
setContextClass(((StandardHost) host).getContextClass());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
// 看事件与其对应的方法调用
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
可以看到Lifecycle.PERIODIC_EVENT事件会调用其check方法。
protected void check() {
if (host.getAutoDeploy()) {//这个条件对应这server.xml的Host配置的autoDeploy="true"
DeployedApplication[] apps =
deployed.values().toArray(new DeployedApplication[0]);
for (int i = 0; i < apps.length; i++) {
if (!isServiced(apps[i].name))
//资源查找
checkResources(apps[i], false);
}
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
//部署
deployApps();
}
}
很显然,如果server.xml的Host配置了能够自动部署,那么会调用deployApps方法。也就是说tomcat每10s会调用一次deployApps,完web application的部署。
protected void deployApps() {
File appBase = appBase();
File configBase = configBase();
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);
}
可以看到可以通过xml,war包等直接部署!
protected void deployDescriptor(ContextName cn, File contextXml) {
……
Context context = null;
……
Class<?> clazz = Class.forName(host.getConfigClass());//默认值:ContextConfig
LifecycleListener listener =
(LifecycleListener) clazz.newInstance();
context.addLifecycleListener(listener);
……
host.addChild(context);
……
}
而部署的过程,其实就是创建了Context对象,并添加到Host中。
此外从HostConfig部署Contex的方法中可以看到,有3中方式部署war包:
- 1 在server.xml的Host标签中声明Context标签
- 2 将war包放入webapps中
- 3 context.xml配置方式
至此,我们已经知道了Engine、Host、Context的加载了,同时也知道了tomcat是怎么加载我们的web服务,是怎么实现的热部署。那么接下来就剩下最后一个Wrapper的加载了。
很捉急,在server.xml中没有关于Wrapper的初始化加载,那么在哪里呢?
同样回到,上面的deployApps()方法中,在其三种部署方式中都有一节代码
Class<?> clazz = Class.forName(host.getConfigClass());//默认值:ContextConfig
LifecycleListener listener =
(LifecycleListener) clazz.newInstance();
context.addLifecycleListener(listener);
这段代码的作用是给Context容器添加了ContextConfig监听器。而在Context的startInternal方法中,发送了监听事件:
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
ContextConfig监听到该事件,调用configureStart方法,在该方法中调用webConfig(),webConfig完成web.xml解析,生成servlet、filter等信息,并配置加载Wrapper。
通过对ContextConfig的分析可以知道,Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。
好了,到了现在,从Engine---Host---Contex----Wrapper这个链路上的容器初始化已经完成。接下来看看Connector的初始化与启动过程。
系列文章直达:
初始化与启动:https://yq.aliyun.com/articles/20169?spm=0.0.0.0.4yGfpo
容器:https://yq.aliyun.com/articles/20172?spm=0.0.0.0.2uPEZi
连接器:https://yq.aliyun.com/articles/20175?spm=0.0.0.0.2uPEZi
一个http请求的经历:https://yq.aliyun.com/articles/20177?spm=0.0.0.0.2uPEZi
重要的设计模式:https://yq.aliyun.com/articles/20179?spm=0.0.0.0.2uPEZi