目錄
1.服務器的啓動
在Tomcat學習(一):啓動過程(1)中簡單介紹了Tomcat的啓動類、加載server.xml和初始化部分組件的過程。
在使用load方法初始化組件後,Catalina又調用了Server的start方法,開始了各組件的啓動。和init一樣,LifecycleBase也在startInternal前後增加了事件發佈:
public final synchronized void start() throws LifecycleException {
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {
return;
}
if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}
try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);
startInternal();
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}
對於正在準備啓動、正在啓動、已啓動三個狀態,不做重複啓動,日誌記錄後返回;如果還未初始化,則進行初始化;如果初始化失敗,則停止啓動;正常狀態下,應該是已經初始化或者已停止才能啓動,否則會拋異常。
然後發佈準備啓動事件,並執行startInternal方法,最後根據啓動狀態決定是停機、拋出異常還是發佈已啓動事件。
進入Server#startInternal,會立即發出CONFIGURE_START_EVENT事件,然後啓動JNDI和所有的Service。JNDI的啓動只是發佈一個事件,沒有額外操作,所以主要看Service的。
Service#startInternal和initInternal基本一致,只是調用方法變成了start而已。
1)Engine
StandardEngine的startInternal方法是委託父類ContainerBase完成的,主要工作就是:
- 啓動集羣
- 啓動安全組件Realm
- 啓動子容器(即Host,對於Host來說就是Context,以此類推)
- 啓動Pipeline
- 發佈正在啓動相關事件
在StandardHost中,首先向Pipeline添加一個ErrorReportValve,然後委託Container繼續向下一級啓動。這裏有一個隱藏的Context加載方式。在Host啓動完根據server.xml解析到的Context後,會發出生命週期時間,此時有一個LifecycleListener,名爲HostConfig,就會處理該事件,從而到webapps(根據appBase屬性配置)目錄下掃描Web應用。該邏輯下面單獨列出。
在StandardContext中,首先發出一個j2ee.state.starting廣播,然後啓動所屬的JNDI服務。然後嘗試加載Web應用,這一段也單獨列出。由於加載Context時,會把Servlet一併解析、加載好,所以這次就不需要委託ContainerBase向下一級啓動了。與Host類似,也會觸發ContextConfig加載Web應用。該邏輯下面單獨列出。
Pipeline的啓動就簡單很多,就是啓動所有實現了Lifecycle接口的Valve,默認的幾個Valve啓動過程都是發佈事件。
2)Executor
其實Tomcat的Executor線程池,也是基於JDK的線程池實現的,所以這裏創建了任務隊列TaskQueue(基於LinkedBlockingQueue)和ThreadPoolExecutor(基於JDK的ThreadPoolExecutor)
protected void startInternal() throws LifecycleException {
taskqueue = new TaskQueue(maxQueueSize);
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
executor.setThreadRenewalDelay(threadRenewalDelay);
if (prestartminSpareThreads) {
executor.prestartAllCoreThreads();
}
taskqueue.setParent(executor);
setState(LifecycleState.STARTING);
}
3)MapperListener
MapperListener會嘗試註冊默認Host名,且將自身註冊爲Engine和所有Host的監聽器。然後對於每個Host,將其註冊到Mapper。
public void startInternal() throws LifecycleException {
setState(LifecycleState.STARTING);
Engine engine = service.getContainer();
if (engine == null) {
return;
}
findDefaultHost();
addListeners(engine);
Container[] conHosts = engine.findChildren();
for (Container conHost : conHosts) {
Host host = (Host) conHost;
if (!LifecycleState.NEW.equals(host.getState())) {
// Registering the host will register the context and wrappers
registerHost(host);
}
}
}
Mapper和MapperListener是完成從請求URL到容器映射的關鍵類,具體作用會在請求處理中體現。
4)Connector啓動了內部的ProtocolHandler,最後又啓動了內部的Endpoint。以NioEndpoint爲例,它對startInternal進行了實現:
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
if (socketProperties.getProcessorCache() != 0) {
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
}
if (socketProperties.getEventCache() != 0) {
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache());
}
if (socketProperties.getBufferPool() != 0) {
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool());
}
// Create worker collection
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start poller thread
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
startAcceptorThread();
}
}
首先是初始化了Processor、Event和Socket的緩存,也就是說Tomcat爲了提高處理效率,是設計了緩存機制的。
接下來對線程池、LimitLatch(限制最大連接數)進行初始化。
下面一段的Poller和初始化時提前啓動的Poller實現不同,但也還是根據SelectionKey做處理。
最後啓動一個Acceptor線程,負責接收請求,然後包裝請求。
Poller和Acceptor線程具體工作在請求處理環節介紹。
2.Web應用的加載
一個典型的Context(Web應用)配置如下:
<Context docBase="testapp" path="/test"/>
docBase代表其在webapps(Host的appBase屬性配置的)下的文件夾名,path代表其URL的context部分,例如上面配置的應用,根請求地址是:http://localhost:8080/test。
1)首先是創建工作目錄,位於 webapps/work/Engine名稱/Host名稱/Context名稱 。
2)然後嘗試加載WebResourceRoot,實現類爲StandardRoot,包括的資源有五種:
private final List<List<WebResourceSet>> allResources =
new ArrayList<>();
{
allResources.add(preResources);
allResources.add(mainResources);
allResources.add(classResources);
allResources.add(jarResources);
allResources.add(postResources);
}
其中,mainResources指的是 /WEB-INF/classes/META-INF/resources 目錄下的資源文件。pre、jar、post,都是context.xml(是Tomcat的配置文件)中指定的。class指的就是所有class文件了。這一步如果執行成功,就會調用StandardRoot的start方法,對加載到的資源進行初始化。
3)然後創建WebappLoader(就是每個應用私有的ClassLoader)。
4)然後創建Cookie處理器(默認實現爲Rfc6265CookieProcessor)、初始化字符集映射、依賴檢測、初始化JNDI服務。
5)接下來啓動WebappLoader(是的,它也是一個Lifecycle實現類)。啓動過程的主要工作有:
- 設置classpath
- 設置權限
- 加載/WEB-INF/classes和/WEB-INF/lib下的資源
- 發佈事件
啓動完成後,爲它設置一些屬性。
6)然後啓動安全組件Realm。
7)發佈CONFIGURE_START_EVENT事件,從而激活ContextConfig。
8)啓動子節點(即StandardWrapper,靠ContextConfig創建)、Pipeline、會話管理器等組件。
9)將加載到的資源,全部傳給ServletContext。
10)創建實例管理器,用於實例化Servlet、Filter。
11)創建Jar包掃描器並設置到ServletContext。
12)合併ServletContext和Context組件的參數。
13)調起ServletContainerInitializer對象(Servlet規範3.0版本引入,用於編程式配置Servlet、Filter)。
14)實例化應用監聽器,包括SevletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionIdListener、HttpSessionAttributeListener,HttpSessionListener、ServletContextListener。Spring MVC就是靠ServletContextListener啓動DispatcherServlet的。
15)檢測未覆蓋的HTTP方法的安全約束
16)啓動會話管理器
17)實例化FilterConfig、Filter,並調用Filter#init()初始化
18)如果web.xml中,load-on-startup值大於0,此處調用Wrapper的load方法開始實例化
19)啓動後臺定時處理線程,用來監控文件變更,如果有變化,就需要重新加載or部署
20)發佈j2ee.state.running通知
21)釋放WebResourceRoot資源
22)發佈啓動事件
3.自動掃描機制
在Engine啓動部分,介紹了基於Tomcat生命週期事件機制設計的HostConfig、ContextConfig存在自動掃描的能力,在這裏進行介紹。
3.1 HostConfig
首先看它的事件處理邏輯:
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;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
如果是StandardHost發出的事件,就更新一下信息,然後處理四種事件:
1)PERIODIC_EVENT
該事件代表Web應用資源發生變化,需要重新加載or部署。check方法如下:
protected void check() {
if (host.getAutoDeploy()) {
// Check for resources modification to trigger redeployment
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);
}
// Check for old versions of applications that can now be undeployed
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
// Hotdeploy applications
deployApps();
}
}
checkResource方法會檢查每個已部署的應用的資源,從代碼中可以看到,每個應用都維護了兩個列表:redeployResources和reloadResources,記錄了資源及其最後修改時間,前一個列表的資源如果發生變更,會導致重新部署,後一個則是重新加載。重新加載和重新部署的區別是,重新加載是Context的重啓,重新部署則是重新創建Context。
對於一個資源,有三種情況:
- 是目錄,更新時間即可
- 是WAR包,且Context的docBase屬性結尾不是“.war”,即需要解壓,那麼就先刪除舊目錄,再重新加載,否則直接重新加載即可
- 其他情況,直接重新加載即可
2)BEFORE_START_EVENT
此時Host剛初始化完畢,尚未啓動,這一步主要是用來創建需要的目錄
3)START_EVENT
該事件會在Host啓動時觸發,主要工作是完成Web應用的部署。
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);
}
可以看到,這裏對三種不同形式的應用進行了部署。
- 根據context描述文件部署
描述文件其實就是把server.xml中,Context元素獨立出來,通過Host元素的xmlBase屬性指定描述文件所在目錄,未配置則默認是 $CATALINA_BASE/conf/Engine名稱/Host名稱。部署時,掃描配置文件目錄,通過線程池提交一個解析任務。
解析任務中又使用了Digester對xml文件進行解析,然後構造一個Context對象,並利用Host的addChild方法,將Context添加到Host。在ContainerBase中,addChildInternal方法會判斷當前容器是否已經啓動,是的話就將新加入的子容器也一起啓動了。最後還會添加對描述文件、Web應用目錄、web.xml等的監視,一旦文件發生變更,就會觸發重新加載或部署。
- 部署WAR包
對於appBase目錄下所有的WAR包(文件名不能是META-INF或WEB-INF),類似根據context描述文件部署,也是利用線程池提交任務來加載的。在提交前,如果配置了需要解壓,會把WAR包展開成目錄再操作。
如果deployXML屬性爲true且其內部包含META-INF/context.xml,則使用該文件創建Context對象,如果deployXML爲false,但包含META-INF/context.xml,則創建FailedContext,表示部署失敗。其他情況下,都是根據contextClass屬性來反射創建Context對象,默認是StandardContext。
如果使用WAR包包含的描述文件,且copyXML屬性爲true,還會將描述文件複製到 $CATALINA_BASE/conf/Engine名稱/Host名稱 下,文件名和WAR包名稱相同。
最後對Context對象設置一些屬性,比如生命週期監聽器、名稱、路徑、版本、docBase等,然後通過addChild方法添加到Host。
- Web應用目錄部署
基本上和WAR包的一樣,可以理解成WAR包多了一步解壓操作
4)STOP_EVENT
解除Host作爲MBean的註冊
3.2 ContextConfig
ContextConfig也是一個生命週期事件監聽器,會處理6類事件,不過只有3個和Web應用加載關係比較大,所以只看這三個。
1)CONFIGURE_START_EVENT
負責解析web.xml,將Servlet包裝爲Wrapper,並創建Filter、ServletContextListener等Web容器相關的對象。
- 初始化Web容器
- 如果配置了ignoreAnnotations屬性爲false,則解析註解配置,並添加JNDI資源引用
- 驗證安全角色名稱
- 設置安全認證
第一步中,會加載 默認配置(即 Tomcat目錄下conf/web.xml)、Web應用的 WEB-INF/web.xml、JAR包中的 META-INF/web-fragment.xml 和 META-INF/services/javax.servlet.ServletContainerInitializer。然後將從web-fragment.xml解析得到的WebXml對象進行排序,並將排序後各WebXml對應的來源JAR包名存入ServletContext的javax.servlet.context.orderedLibs屬性。
對於這個orderedLibs,如果不爲空,那麼就會檢查這些JAR包包含的ServletContainerInitializer實現,封裝爲initializerClassMap。再解析其HandlesTypes註解指定的Class列表,封裝成typeInitializerMap(key爲Class,值爲ServletContainerInitializer集合)。
如果typeInitializerMap不爲空,就會據此來處理/WEB-INF/classes下的類,查找@WebServlet、@WebFilter、@WebListener的等註解的內容,合併進WebXml對象。
然後,將所有的WebXml對象合併。
接下來就是配置JspServlet,以及用合併後的WebXml配置StandardContext,包括Servlet、Filter等Servlet規範規定的組件,還有歡迎文件(welcome-file-list配置)、postConstructMethod和preDestroyMethod等配置。在for (ServletDef servlet : webxml.getServlets().values()) {...}循環中,完成了Servlet封裝爲Wrapper的過程,並通過addChild添加到Context中。
接下來還會查找 WEB-INF/classes/META-INF/resources 下的靜態資源,靜態資源和上面解析出的ServletContainerInitializer信息,最後都會被設置到StandardContext中。
2)BEFORE_START_EVENT
該事件用於處理docBase配置,並且解決目錄鎖問題。
先看docBase的處理。首先獲取了Host的appBase屬性和Context的docBase屬性,從而計算出docBase的絕對路徑。如果docBase配置的是個WAR包,且需要解壓,則解壓之,並且將docBase屬性更新爲解包後的目錄。否則就不用更新。
如果docBase是目錄,但是有一個同名WAR包,也需要解壓部署,則重新解壓。
如果docBase配置的目錄不存在,但存在同名WAR包,且需要解壓部署,則解壓並更新docBase屬性。
然後看目錄鎖的處理。首先也是獲取了Host的appBase屬性和Context的docBase屬性,計算出docBase的絕對路徑。然後計算出工作目錄下Web應用的目錄名或WAR包名,並將源文件複製到工作目錄,最後更新docBase屬性爲工作目錄下的目錄名 or WAR包名。
這一步的好處是不會對源文件加鎖,這樣就可以邊運行邊做修改。
3)AFTER_INIT_EVENT
這一步首先初始化了Digester,用來後續解析conf/context.xml、web.xml用。
然後解析了conf/context.xml,用來將Tomcat提供的默認配置更新到StandardContext對象中。並解析了conf/Engine名稱/Host名稱/context.xml.default文件(Host級的默認配置),同樣進行更新。最後解析Context的configFile配置的context.xml文件,再次更新屬性。
實際上在邏輯中,上面三個發生的順序,應該是 3 - 2 - 1 纔對。
至此,Tomcat就完成了所有組件的啓動,並且開啓了Socket監聽,後面就是等待請求到達並處理了。