Tomcat學習(二):啓動過程(2)

目錄

1.服務器的啓動

2.Web應用的加載

3.自動掃描機制

3.1 HostConfig

3.2 ContextConfig


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監聽,後面就是等待請求到達並處理了。

發佈了84 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章