深入淺出Tomcat/4 - Tomcat容器

張太國發表於2019-02-02

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 &quot;%r&quot; %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;
            }
        }

 

相關文章