tomcat源碼分析02:啓動流程

注:本文源碼分析基於 tomcat 9.0.43,源碼的gitee倉庫倉庫地址:https://gitee.com/funcy/tomcat.

1. 示例demo

本文是tomcat源碼分析的第二篇,在idea下搭建 tomcat9 源碼調試環境一文中,我們搭建好了tomcat的源碼調試環境,接下來我們就在裏面添加示例代碼,然後進行源碼分析了。

我們將代碼都放在test目錄下,包名爲org.apache.tomcat.demo

1.1 準備servlet

我們先準備一個servet,內容如下:

public class MyHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.getWriter().println("<h1>hello world!!!</h1>");
        System.out.println("hello world");
    }
}

這個servlet比較簡單,就只是向頁面與控制檯打印了一句:hello world

1.2 實現ServletContainerInitializer

這個是servlet 3.0規範,本文的demo中主要用來代替 web.xml進行servlet註冊的,都到2021年了,就不整web.xml的方式了,代碼如下:

public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> clsSet, ServletContext servletContext)
            throws ServletException {
        MyHttpServlet servlet = new MyHttpServlet();
        ServletRegistration.Dynamic registration
                = servletContext.addServlet("servlet", servlet);
        // loadOnStartup 設置成 -1 時,只有在第一次請求時,纔會調用 init 方法
        registration.setLoadOnStartup(-1);
        registration.addMapping("/*");
    }
}

關於servlet 3.0規範及ServletContainerInitializer的作用,本文就不展開了,想了解的 小夥伴可自行百度。

1.3 創建spi文件

爲了MyServletContainerInitializer能被tomcat加載到,我們還需要創建一個spi文件:

如上圖,我們需要在test的同級目錄下創建test資源包,我這裏將其命名爲testRes,需要標記爲Test Resources Root,然後在其下創建兩個目錄META-INF/service,再創建一個文本文件,將其命名爲javax.servlet.ServletContainerInitializer(就是ServletContainerInitializer的全限定名了),文件內容如下:

org.apache.tomcat.demo.MyServletContainerInitializer

裏面的內容就是我們自己實現的ServletContainerInitializer了。tomcat在啓動時,會找到META-INF/service/javax.servlet.ServletContainerInitializer文件,讀取內容後,就能加載MyServletContainerInitializer了,我們後面會從源碼的角度來分析這個過程。

1.4 主類

接下來是主類:

public class Demo01 {

    @Test
    public void test() throws Exception {
        Tomcat tomcat = new Tomcat();

        // 創建連接器
        Connector connector = new Connector();
        connector.setPort(8080);
        connector.setURIEncoding("UTF-8");
        tomcat.getService().addConnector(connector);

        // 創建 context
        String docBase = System.getProperty("java.io.tmpdir");
        Context context = tomcat.addContext("", docBase);

        // 得到 lifecycleListener 實際類型爲 ContextConfig
        LifecycleListener lifecycleListener = (LifecycleListener)
                Class.forName(tomcat.getHost().getConfigClass())
                        .getDeclaredConstructor().newInstance();
        context.addLifecycleListener(lifecycleListener);

        tomcat.start();
        tomcat.getServer().await();
    }
}

這個類還是比較簡單的,先創建了一個Connector,然後向tomcat中添加了一個Context,最後就是啓動tomcat了。

運行,然後在瀏覽器訪問http://localhost:8080,結果如下:

可以看到,MyHttpServlet可以正常對外訪問了。

2. tomcat架構體系

在正式分析tomcat源碼前,我們首先要對tomcat有個整體的認識,tomcat的整個架構體系如下:

  • Server:整個Tomcat服務器,一個Tomcat只有一個Server標準實現爲StandardServer
  • ServiceServer中的一個邏輯功能層, 一個Server可以包含多個Service(他們是彼此完全獨立,只共享基本的JVM和系統路徑上的類),一個Service負責維護一個或多個Connector和一個Container標準實現爲StandardService
  • Connector:稱作連接器,是Service的核心組件之一,一個Service可以有多個Connector,用於接受請求並將請求封裝成RequestResponse,然後交給Container進行處理,Container處理完之後再交給Connector返回給客戶端;
  • ContainerService的另一個核心組件,它由四個子容器組件構成,分別是:EngineHostContextWrapper,這四個組件不是平行的,而是父子關係,Engine包含HostHost包含ContextContext 包含 Wrapper
  • JasperJSP引擎;
  • Session:會話管理.

關於Container的進一步說明:

Container由四個子容器組件構成:EngineHostContextWrapper,關係如下:

  • Engine:一個Service中有 多個Connector一個EngineEngine表示整個Servlet引擎,一個Engine下面可以包含一個或者多個Host標準實現爲StandardEngine
  • Host:代表一個站點,也可以叫虛擬主機,一個Host可以配置多個Context標準實現爲StandardHost
  • Context:代表ServletContext,它具備了Servlet運行的基本環境,理論上只要有Context就能運行Servlet了,簡單的 Tomcat可以沒有EngineHost標準實現爲StandardContext
  • WrapperWrapper代表一個Servlet,它負責管理一個Servlet,包括的Servlet的裝載、初始化、執行以及資源回收,Wrapper是最底層的容器,它沒有子容器了,標準實現爲StandardWrapper.

瞭解了tomcat這一層層的結構後,接下來我們就正式從源碼上來分析tomat的啓動流程了。

3. Tomcat#start()方法

tomcat 的啓動方法爲Tomcat#start(),我們跟進去:

public void start() throws LifecycleException {
    getServer();
    server.start();
}

我們先看看getServer()

    public Server getServer() {
        if (server != null) {
            return server;
        }
        System.setProperty("catalina.useNaming", "false");
        // 創建標準的 server
        server = new StandardServer();
        ...
        // 添加 Service
        Service service = new StandardService();
        service.setName("Tomcat");
        server.addService(service);
        return server;
    }

這個方法幹了兩件事:

  1. 創建StandardServer
  2. 創建StandardService

讓我們再回到Tomcat#start()方法,server對象有了,接下來就是server.start()方法了,StandardServerLifecycleBase的子類,讓我們進入LifecycleBase#start方法:

    public final synchronized void start() throws LifecycleException {
        // 省略了部分代碼
        ...
        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();
            // 省略了部分代碼
            ...
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.startFail", toString());
        }
    }

這裏我們省略了部分代碼,LifecycleBase#start 就只幹了兩件事:init()(初始化)與startInternal()(啓動),我們先看初始化方法LifecycleBase#init

    @Override
    public final synchronized void init() throws LifecycleException {
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }

        try {
            // 設置狀態,觸發"正在初始化"事件
            setStateInternal(LifecycleState.INITIALIZING, null, false);
            // 初始化
            initInternal();
            // 設置狀態,觸發"初始化完成"事件
            setStateInternal(LifecycleState.INITIALIZED, null, false);
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.initFail", toString());
        }
    }

這個方法乾的事不多,我們重點關注initInternal()方法,這是個抽象方法,StandardService重寫了它,我們進入StandardService#initInternal方法:

    protected void initInternal() throws LifecycleException {

        super.initInternal();

        // 1. 初始化 engine
        if (engine != null) {
            // 調用初始化方法
            engine.init();
        }

        // 2. 初始化 Executor
        for (Executor executor : findExecutors()) {
            if (executor instanceof JmxEnabled) {
                ((JmxEnabled) executor).setDomain(getDomain());
            }
            executor.init();
        }

        // 3. 初始化 mapperListener
        mapperListener.init();

        // 4. 初始化 Connectors
        synchronized (connectorsLock) {
            for (Connector connector : connectors) {
                connector.init();
            }
        }
    }

這個方法裏會初始化engineExecutormapperListenerConnectors,我們先來看看engine.init(),點進去,發現它到了LifecycleBase#init方法!我們來看看LifecycleBase的實現類:

可以看到,前面介紹的tomcat各組件都是LifecycleBase的子類,上圖未列出的:

Connector

/** 繼承了 LifecycleMBeanBase */
public class Connector extends LifecycleMBeanBase  {

}

/** 而 LifecycleMBeanBase 又繼承了 LifecycleBase  */
public abstract class LifecycleMBeanBase extends LifecycleBase
        implements JmxEnabled {

}

從源碼上可以看到,Tomcat#start()方法裏執行了Server#start()方法,Server#start()又會執行Engine.start()Host#start()方法的,一直到Wrapper#start()tomcat正是以這種套娃般的方式進行啓動的,同樣的方法還有LifecycleBase#init,也是這樣層層執行下去的,整個流程如圖:

StandardServerStandardServiceConnector等組件都有重寫initInternal()startInternal()方法,想要了解某個組件初始化或啓動時所做的工作,直接進入該類查看這兩個方法就可以了。

需要注意的是幾個Container類,StandardEngineStandardHostStandardContextStandardWrapper都是ContainerBase的子類:

我們重點來看看ContainerBasestartInternal(...)方法:

    protected synchronized void startInternal() throws LifecycleException {
        ...

        // 啓動子容器
        Container children[] = findChildren();
        List<Future<Void>> results = new ArrayList<>();
        for (Container child : children) {
            // 在線程池中處理 啓動操作
            results.add(startStopExecutor.submit(new StartChild(child)));
        }

        MultiThrowable multiThrowable = null;

        for (Future<Void> result : results) {
            try {
                result.get();
            } catch (Throwable e) {
                ...
            }

        }
        ...
    }

    /**
     * 啓動子容器的類,實現了 Callable 接口
     */
    private static class StartChild implements Callable<Void> {

        private Container child;

        public StartChild(Container child) {
            this.child = child;
        }

        @Override
        public Void call() throws LifecycleException {
            // 啓動操作,調用的是 LifecycleBase#start 方法
            child.start();
            return null;
        }
    }

ContainerBasestartInternal(...)方法會先獲取到當前Container子Container,然後調用LifecycleBase#start(),這個啓動操作會被包裝成StartChild對象,放在線程池中執行。注意,LifecycleBase#start(...)操作時,會先調用init()方法再調用start(),因此Container類的啓動與初始化是在這裏同一個方法裏進行的。

舉例來說,

  1. StandardEngine調用startInternal(...)方法,找到的子ContainerStandardHost,再就調用StandardHost#init(...)StandardHost#start(...)方法;
  2. 調用StandardHost#start(...)方法時,又會調用ContainerBase#startInternal(...)方法,再又找到StandardHost子ContainerStandardContext,然後又執行它的init(...)start(...)方法;
  3. 直到運行StandardWrapper#start(...),由於StandardWrapper沒有子Container了,這種套娃式的運行就結束了。

實際上,前面的組件都沒做什麼實質性工作,只是爲了初始化或啓動下一個組件,真正幹活的是最後的組件,如

  • Context:會在這裏處理servlet的加載
  • Connector:處理http連接,開啓端口監聽

這些內容在分析具體功能時再展開分析吧。

4. 總結

本文作爲tomcat源碼正式分析的第一篇,主要是準備了一個源碼分析demo,然後介紹了tomcat的各大組件,最後闡述了tomcat的整個啓動流程,旨在讓大家對tomcat有一個全局上的認識。


限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

本文首發於微信公衆號 Java技術探祕,如果您喜歡本文,歡迎關注該公衆號,讓我們一起在技術的世界裏探祕吧!

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