深入淺出Tomcat/4 - Tomcat容器

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;
            }
        }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章