我們談到tomcat時,第一印象是它是一種servlet容器,這個概念是相當抽象和本質的,我們仍然對tomcat的内幕很陌生。我們知道,tomcat由connector和container兩大元件構成,connector在前面的文章已經介紹過了,今天我們就來看看container是怎麼回事。
一、container基本結構
前文中有講到,connector和container的初始化工作是由digester解析conf/server.xml來完成的,而在server.xml中已經告訴了我們container的基本結構。我們先來看看server.xml檔案:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
<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 &quot;%r&quot; %s %b" />
</host>
</engine>
</service>
</server>
通過xml檔案,我們可以很清晰的看到,server下包含了service(可有多個),service下包含了connector和engine,其實還可以包含executor(線程池)。engine正是與connector處在同一水準的容器,engine下面有host,下面給出tomcat子產品組成圖。
前面在模拟tomcat連接配接器時,正是将servlet容器的執行個體作為參數傳入到連接配接器的setcontainer()方法中,這樣連接配接器才能調用servlet容器的invoke()方法。
接下來給出一張大衆化的container元件結構圖。
通過代碼可以知道,tomcat提供了一個container接口來抽象容器,并且細分了4種類型的容器,分别是engine、host、context和wrapper,對應不同的概念層次。
· engine:表示整個catalina的servlet引擎
· host:表示一個擁有數個上下文的虛拟主機
· context:表示一個web應用,一個context包含一個或多個wrapper
· wrapper:表示一個獨立的servlet
engine是頂層容器,java程式能夠運作是因為有引擎即jvm,很自然的,engine正是可以配置jvm(jvmroute="jvm1")。host是engine的子容器,context是host的子容器,wrapper是context的子容器,這4個接口的标準實作分别是standardengine、standardhost、standardcontext、standardwrapper,她們都在org.apache.catalina.core包類,分析tomcat容器正式主要分析這4個類。下圖展示了container接口及其子接口和實作類的uml類圖。
通過上面的兩個圖,我們知道了container的結構,那麼container的初始化工作是怎麼完成的呢?
二、container初始化
回到catalina類中,在load方法中調用了createstartdigester方法。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
/**
* create and configure the digester we will be using for startup.
*/
protected digester createstartdigester() {
digester digester = new digester();
// initialize the digester
// configure the actions we will be using
// 如果遇到”server“元素起始符;則建立"org.apache.catalina.core.standardserv // er"的一個執行個體對象,并壓入堆棧;如果"server"元素的"classname"屬性存在,那麼用 // 這個屬性的值所指定的class來建立執行個體對象,并壓入堆棧。
digester.addobjectcreate("server",
"org.apache.catalina.core.standardserver",
"classname");
// 從server.xml讀取"server"元素的所有{屬性:值}配對,用對應的setter方法将屬性值 // 設定到堆棧頂層元素(server)。
digester.addsetproperties("server");
digester.addsetnext("server",
"setserver",
"org.apache.catalina.server");
digester.addobjectcreate("server/service",
"org.apache.catalina.core.standardservice",
digester.addsetproperties("server/service");
digester.addsetnext("server/service",
"addservice",
"org.apache.catalina.service");
digester.addobjectcreate("server/service/listener",
null, // must be specified in the element
digester.addsetproperties("server/service/listener");
digester.addsetnext("server/service/listener",
"addlifecyclelistener",
"org.apache.catalina.lifecyclelistener");
//executor
//connector
// add rulesets for nested elements
digester.addruleset(new namingruleset("server/globalnamingresources/"));
digester.addruleset(new engineruleset("server/service/"));
digester.addruleset(new hostruleset("server/service/engine/"));
digester.addruleset(new contextruleset("server/service/engine/host/"));
addclusterruleset(digester, "server/service/engine/host/cluster/");
digester.addruleset(new namingruleset("server/service/engine/host/context/"));
// when the 'engine' is found, set the parentclassloader.
digester.addrule("server/service/engine",
new setparentclassloaderrule(parentclassloader));
addclusterruleset(digester, "server/service/engine/cluster/");
return (digester);
}
從簡化的源碼中可以看到,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。
三、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類中,可以看到這麼一行代碼:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
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方法可以看到,
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
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方法。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
@override
public void backgroundprocess() {
if (loader != null) {
loader.backgroundprocess();
} catch (exception e) {
log.warn(sm.getstring("containerbase.backgroundprocess.loader", loader), e);
}
//
firelifecycleevent(lifecycle.periodic_event, null);
這個方法中比較重要的兩個
loader.backgroundprocess():調用了載入器的webapploader的backgroundprocess方法,進入這個方法可以看到:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
if (reloadable && modified()) {
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。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
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
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
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方法。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
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的部署**。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
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包等直接部署!
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
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()方法中,在其三種部署方式中都有一節代碼
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
class<?> clazz = class.forname(host.getconfigclass());//預設值:contextconfig
lifecyclelistener listener = (lifecyclelistener) clazz.newinstance();
context.addlifecyclelistener(listener);
這段代碼的作用是給context容器添加了contextconfig監聽器。而在context的startinternal方法中,發送了監聽事件:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLyFGdz9lbvNWavw1cldWYtl2Lc12bj5SZ5VGdp5SesV2dvw1LcpDc0RHaiojIsJye.png)
firelifecycleevent(lifecycle.configure_start_event, null);
contextconfig監聽到該事件,調用configurestart方法,在該方法中調用webconfig(),webconfig完成web.xml解析,生成servlet、filter等資訊,并配置加載wrapper。通過對contextconfig的分析可以知道,wrapper 代表一個 servlet,它負責管理一個 servlet,包括的 servlet 的裝載、初始化、執行以及資源回收。
好了,從engine---host---contex----wrapper這個鍊路上的容器初始化和app加載已經完成。接下來的文章我們來看請求在容器裡所走過的代碼邏輯。
原文連結:[http://wely.iteye.com/blog/2295222]