Container是一個Tomcat容器的介面,Tomcat有四種容器
· Engine
· Host
· Context
· Wrapper
Engine代表整個Catalina的Servlet引擎,Host則代表若干個上下文的虛擬主機。Context則代表一個Web應用,而一個Context則會用有多個Wrapper。Wrapper是一個單獨的Servlet。
下圖是幾種容器實現的類繼承圖,我們可以看到最下層以Standard開頭的幾個類
· StandardEngine
· StandardHost
· StandardContext
· StandardWrapper
以上幾個類是Tomcat對幾種容器的預設實現。
以上幾個類是Tomcat對幾種容器的預設實現。
Engine
Engine的屬性name,是Engine的名字,如果有多個Engine,Engine需要唯一。defaultHost也非常重要,如果一個Engine有多個Host時,如果匹配不到合適的Host時,則需要預設選取一個,也就是defaultHost定義的,它的值為Host的name。
<Engine name="Catalina" defaultHost="localhost"> <RealmclassName="org.apache.catalina.realm.LockOutRealm"> <!--This Realm uses the UserDatabase configured in the global JNDI resources under the key"UserDatabase". Any edits that are performed against thisUserDatabase are immediately available for use by theRealm. --> <RealmclassName="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> </Realm> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <!--SingleSignOn valve, share authentication between web applications Documentation at: /docs/config/valve.html--> <!-- <ValveclassName="org.apache.catalina.authenticator.SingleSignOn" /> --> <!-- Access log processes allexample. Documentation at:/docs/config/valve.html Note: The pattern used isequivalent to using pattern="common" --> <ValveclassName="org.apache.catalina.valves.AccessLogValve"directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b" /> </Host> </Engine>
Engine還有另外一個非常重要的屬性叫jvmRoute,它一般用在Cluster裡。
假設Cluster是這麼配置的,Tomcat1的 conf/server.xml
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat1">
Tomcat2的conf/server.xml
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat2">
在生成SessionID時,jvmRoute會用到的,程式碼如下:
public class StandardSessionIdGeneratorextends SessionIdGeneratorBase{ @Override publicString generateSessionId(String route) { byterandom[] = newbyte[16]; int sessionIdLength = getSessionIdLength(); //Render the result as a String of hexadecimal digits // Start with enough space forsessionIdLength and medium route size StringBuilderbuffer = new StringBuilder(2 * sessionIdLength + 20); int resultLenBytes = 0; while (resultLenBytes < sessionIdLength) { getRandomBytes(random); for (int j = 0; j < random.length && resultLenBytes < sessionIdLength; j++) { byte b1 = (byte) ((random[j] & 0xf0) >> 4); byte b2 = (byte) (random[j] & 0x0f); if (b1 < 10) buffer.append((char) ('0' + b1)); else buffer.append((char) ('A' + (b1 - 10))); if (b2< 10) buffer.append((char) ('0' + b2)); else buffer.append((char) ('A' + (b2 - 10))); resultLenBytes++; } } if(route != null&& route.length() > 0) { buffer.append('.').append(route); }else { String jvmRoute =getJvmRoute(); if (jvmRoute != null && jvmRoute.length() > 0) { buffer.append('.').append(jvmRoute); } } returnbuffer.toString(); } }
最後幾行程式碼顯示如果在Cluster情況下會將jvmRoute加在sessionID後面。
Host
Host是代表虛擬主機,主要設定appbase目錄,例如webapps等。Host中的name代表域名,所以下面的例子中代表的localhost,可以通過localhost來訪問。appBase是指該站點所在的目錄,預設一般是webapps。unpackWARs這個屬性也很重要,一般來說,一個webapp的釋出包有格式各樣,例如zip,war等,對於war包放到appBase
下是否自動解壓縮,顯而易見,當為true時,自動解包。autoDeploy是指是指Tomcat在執行時應用程式是否自動部署。
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
Context
Context可以在以下幾個地方宣告:
1. Tomcat的server.xml配置檔案中的<Context>節點用於配置Context,它直接在Tomcat解析server.xml的時候,就完成Context物件的建立。
2. Web應用的/META-INF/context.xml檔案可用於配置Context,此配置檔案用於配置Web應用對應的Context屬性。
3. 可用%CATALINA_HOME%/conf[EngineName]/[HostName]/[Web專案名].xml檔案宣告建立一個Context。
4. Tomcat全域性配置為conf/context.xml,此檔案配置的屬性會設定到所有的Context中
5. Tomcat的Host級別配置檔案為/conf[EngineName]/[HostName]/context.xml.default檔案,它配置的屬性會設定到某Host下面所有的Context中。
以上5種方法有些是共享的,有些是獨享的。其中後面2種是被Tomcat共享的。在實際的應用中,個人非常推薦第三種方法。如果在採用第一種方法,這種方法是有侵入性的,不建議,而且該檔案是在Tomcat啟動時才載入。對於共享的方法我個人也是不推薦使用的,畢竟在實際的應用中還是希望自己的app配置單獨出來更合理一些。
Wrapper
Wrapper 代表一個Servlet,它負責管理一個Servlet,包括Servlet 的裝載、初始化、執行以及資源回收。Wrapper的父容器一般是Context,Wrapper是最底層的容器,它沒有子容器了,所以呼叫它的addChild 將會拋illegalargumentexception。Wrapper的實現類是StandardWrapper,StandardWrapper還實現了擁有一個Servlet 初始化資訊的ServletConfig,由此看出StandardWrapper 將直接和Servlet 的各種資訊打交道。
Container的啟動
前面的類圖講過,前面提到的容容器都實現或繼承了LifeCycle,所以LifeCycle裡的幾個生命週期同樣適用於這裡。不過除了繼承自LifeCycle之外,幾個容器也繼承ContainerBase這個類。幾個Container的初始化和啟動都是通過initInternal和startInternal來實現的。需要的話,各個容器可以實現自己的邏輯。
因為4大容器都繼承ContainerBase,我們看看該類的initInternal和startInternal的實現。
@Override protected void initInternal() throws LifecycleException { reconfigureStartStopExecutor(getStartStopThreads()); super.initInternal(); } /* * Implementation note: If there is ademand for more control than this then * it is likely that the best solutionwill be to reference an external * executor. */ private void reconfigureStartStopExecutor(int threads) { if (threads == 1) { //Use a fake executor if(!(startStopExecutorinstanceof InlineExecutorService)) { startStopExecutor = new InlineExecutorService(); } } else{ //Delegate utility execution to the Service Serverserver = Container.getService(this).getServer(); server.setUtilityThreads(threads); startStopExecutor= server.getUtilityExecutor(); } }
我們可以看到這裡並沒有設定一些狀態。在初始化的過程中,初始化statStopExecutor,它的型別是java.util.concurrent.ExecutorService。
下面是startInternal的程式碼,我們可以看出這裡做的事情:
1. 如果cluster和realm都配置後,需要呼叫它們自己的啟動方法。
2. 呼叫子容器的啟動方法。
3. 啟動管道。
4. 設定生命週期的狀態。
5. 同時啟動一些background的監控執行緒。
@Override protected synchronized void startInternal() throws LifecycleException { // Start our subordinate components, if any logger = null; getLogger(); Cluster cluster = getClusterInternal(); if (cluster instanceof Lifecycle) { ((Lifecycle) cluster).start(); } Realm realm = getRealmInternal(); if (realm instanceof Lifecycle) { ((Lifecycle) realm).start(); } // Start our child containers, if any Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<>(); for (int i = 0; i < children.length; i++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); } MultiThrowable multiThrowable = null; for (Future<Void> result : results) { try { result.get(); } catch (Throwable e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); if (multiThrowable == null) { multiThrowable = new MultiThrowable(); } multiThrowable.add(e); } } if (multiThrowable != null) { throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"), multiThrowable.getThrowable()); } // Start the Valves in our pipeline (including the basic), if any if (pipeline instanceof Lifecycle) { ((Lifecycle) pipeline).start(); } setState(LifecycleState.STARTING); // Start our thread if (backgroundProcessorDelay > 0) { monitorFuture = Container.getService(ContainerBase.this).getServer() .getUtilityExecutor().scheduleWithFixedDelay( new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS); } }
這裡首先根據配置啟動了Cluster和Realm,啟動的方法也很直觀,直接呼叫它們的start方法。Cluster一般用於叢集,Realm是Tomcat的安全域,管理資源的訪問許可權,例如身份認證,許可權等。一個Tomcat可以擁有多個Realm的。
根據程式碼,子容器是使用startStopExecutor來實現的,startStopExecutor會使用新的執行緒來啟動,這樣可以使用多個執行緒來同時啟動多個子容器,這樣在效能上更勝一籌。因為可能有多個子容器,把他們存入到Future的List裡,然後遍歷每個Future並呼叫其get方法。
遍歷Future的作用是什麼?1,get方法是阻塞的,只有執行緒處理完後才能繼續往下走,這樣保證了Pipeline啟動之前容器確保呼叫完成。2,可以處理啟動過程中的異常,如果有容器啟動失敗,也不至於繼續執行下去。
啟動子容器呼叫了StartChild這麼一個類似,它的實現如下:
private static class StartChild implements Callable<Void> { private Container child; public StartChild(Container child) { this.child = child; } @Override public Void call() throws LifecycleException { child.start(); return null; } }
這個類也是定義在ContainerBase裡的,所以所有容器的啟動過程都對呼叫容器的start方法。
我們可以看到StartChild實現了Callable介面。我們知道啟動執行緒,有Runnable和Callable等方式,那麼Runnable和Callable的區別在哪裡呢?我認為的區別是:
1. 對於實現Runnable,run方法並不會返回任何東西,但是對於Callable,真是可以實現當執行完成後返回結果的。但需要注意,一個執行緒並不能和Callable建立,儘可以和Runnable一起建立。
2. 另外一個區別就是Callable的Call方式可以丟擲Exception,但是Runnable的run方法這不可以。
根據以上,我們可以看出為什麼要用Callable,前面說捕獲到異常也正是這個原理。
在這裡我們也看到了Future這個東西。有必要在這裡詳細解釋一下Future的概念。Future用來表示非同步計算的結果,它提供了一些方法用來檢查計算是否已經完成,或等待計算的完成以及獲取計算的結果。計算結束後的結果只能通過get方法來獲取。當然,也可以使用Cancel方法來取消計算。在回到我們這裡的程式碼,如下,我們可以看到結果已經存在result裡,通過get方法來獲取,前面我們分析Callable可以丟擲異常,這裡我們可以看到有捕獲到這些異常的程式碼。
for (Future<Void> result : results) { try { result.get(); } catch (Throwable e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); if (multiThrowable == null) { multiThrowable = new MultiThrowable(); } multiThrowable.add(e); } }
Engine
Engine的預設實現類是StandardEngine,它的初始化和啟動會呼叫initInternal和startInternal。下面是StandardEngine的結構圖
初始化和啟動的程式碼分別如下:
@Override protected void initInternal() throws LifecycleException { // Ensure that a Realm is present before any attempt is made to start // one. This will create the default NullRealm if necessary. getRealm(); super.initInternal(); } /** * Start this component and implement the requirements * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. * * @exception LifecycleException if this component detects a fatal error * that prevents this component from being used */ @Override protected synchronized void startInternal() throws LifecycleException { // Log our server identification information if (log.isInfoEnabled()) { log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo())); } // Standard container startup super.startInternal(); }
初始化和啟動還是分別呼叫了ContainerBase的initInternal·和startInternal。特別要注意的是initInternal額外呼叫了getRealm獲取Realm的資訊。那麼getRealm的實現如下:
@Override
public Realm getRealm() {
Realm configured = super.getRealm();
// If no set realm has been called - default to NullRealm
// This can be overridden at engine, context and host level
if (configured == null) {
configured = new NullRealm();
this.setRealm(configured);
}
return configured;
}
我們可以看出,如果沒有realm配置,直接返回預設的NullRealm。
Host
Host的預設實現類是StandardHost,繼承圖如下。
下面程式碼只有startInternal,並沒有initInternal,那是因為StandardHost並沒有重寫initInternal。
程式碼比較簡單,除了呼叫ContainerBase的startInternal,前面還需要查詢Pipeline裡的Valve有沒有和ErrorReport相關的。如果沒有建立Valve一下,然後加到Pipeline裡。
protected synchronized void startInternal() throws LifecycleException { // Set error report valve String errorValve = getErrorReportValveClass(); if ((errorValve != null) && (!errorValve.equals(""))) { try { boolean found = false; Valve[] valves = getPipeline().getValves(); for (Valve valve : valves) { if (errorValve.equals(valve.getClass().getName())) { found = true; break; } } if(!found) { Valve valve = (Valve) Class.forName(errorValve).getConstructor().newInstance(); getPipeline().addValve(valve); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString( "standardHost.invalidErrorReportValveClass", errorValve), t); } } super.startInternal(); }
其中預設的ErrorReport Valve是
/** * The Java class name of the default error reporter implementation class * for deployed web applications. */ private String errorReportValveClass = "org.apache.catalina.valves.ErrorReportValve"
Context
下面是Context的初始化程式碼,後面呼叫了NamingResource相關資訊。
@Override protected void initInternal() throws LifecycleException { super.initInternal(); // Register the naming resources if (namingResources != null) { namingResources.init(); } // Send j2ee.object.created notification if (this.getObjectName() != null) { Notification notification = new Notification("j2ee.object.created", this.getObjectName(), sequenceNumber.getAndIncrement()); broadcaster.sendNotification(notification); } }
接下來看看startInternal,這個方法非常長,節選重要程式碼.
如果resouce沒有啟動,需要呼叫resource的啟動,接下來是呼叫web.xml中定義的Listener,另外還需要初始化該配置檔案定義的Filter以及load-on-startup的Servlet。
protected synchronized void startInternal() throws LifecycleException { //… … if (ok) { resourcesStart(); } //… … // Configure and call application event listeners if (ok) { if (!listenerStart()) { log.error(sm.getString("standardContext.listenerFail")); ok = false; } } //…… // Configure and call application filters if (ok) { if (!filterStart()) { log.error(sm.getString("standardContext.filterFail")); ok = false; } } // Load and initialize all "load on startup" servlets if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail")); ok = false; } }