Tomcat 第三篇:總體架構設計

極客挖掘機發表於2020-09-21

Tomcat 總體架構設計

在開始這篇文章的時候,忽然發現上一篇內容的題目不是很合適,不應該叫啟動流程,更確切的應該是叫啟動指令碼。

在最開始,先介紹下 Tomcat 的總體設計,先有一個大概的印象,對 Tomcat 不至於那麼陌生。

先介紹下 Tomcat 的一些基礎元件(以下內容來自劉光瑞老師的「tomcat 架構解析」):

元件名稱 介紹
Server 這個其實就是 Servlet 容器,一個 Tomcat 中只能有一個 Server
Service Service 表示一個或多個 Connector 的集合,這些 Connector 共享同一個 Container 來處理其請求。在同一個 Tomcat 例項內可以包含任意多個 Service 例項,它們彼此獨立
Connector 這個是 Tomcat 的聯結器,用於監聽並轉化 Servlet 的請求,同時將讀取的 Socket 請求轉交由 Container 處理,並且支援不同的協議以及不同的 I/O 方式。
Container 表示能夠執行客戶端請求並返回響應的一類物件。在 Tomcat 中存在不同級別的容器 Engine, Host, Context, Wrapper
Engine 表示整個 Servlet 引擎。在 Tomcat 中, Engine 為最高層級的容器物件。儘管 Engine 不是直接處理請求的容器,卻是獲取目標容器的入口。
Host Host 作為一類容器,表示 Servlet 引擎(即 Engine 中的虛擬機器,與一個伺服器的網路名有關,如域名等。客戶端可以使用這個網路名連線伺服器,這個名稱必須要在 DNS 伺服器上註冊
Context Context 作為一類容器,用於表示 Servletcontext ,在 Servlet 規範中,一個 Servletcontext 即表示一個獨立的 Web 應用。
Wapper Wapper 作為一類容器,用於表示 Web 應用中定義的 Servlet。
Executor 表示 Tomcat 元件間可以共享的執行緒池。

這個表格大致看一下,瞭解下 Tomcat 的一些元件,無需記憶,先有個印象,後面的內容會慢慢的聊到每一個元件。

作為一款 Web 容器, Tomcat 大體上實現了兩個核心功能:

  • 處理 Socket 連線,負責網路位元組流與 RequestResponse 物件的轉化。
  • 載入並管理 Servlet ,以及處理具體的 Request 請求。

所以 Tomcat 設計了兩個核心元件聯結器(Connector)和容器(Container)。聯結器負責對外交流,容器負責內部處理。

Tomcat 為了實現多種支援多種 I/O 模型和應用層協議,將 聯結器(Connector)容器(Container) 進行了分離,

在 Tomcat 中,總會有一個 Service ,一個 Service 包含著多個 Connector 和一個 Container(或者說是它的頂層容器 Engine ) 。

而一個 Container 可以包含多個 Host ,一個 Host 內部又可以有多個 Context ,一個 Context 內部又會有多個 Servlet 。

Tomcat 的設計感覺上和 「俄羅斯套娃」 很像,一層包著一層。

Tomcat 啟動初始化流程

先放一張啟動流程圖,然後我們再從原始碼的角度來驗證這個啟動流程。

從上面這張圖可以清晰的瞭解到 Tomcat 的初始化的核心過程如下:

BootStrap -> Catalina -> Server -> Service -> Excutor -> Container (Engine -> Host -> Context -> Container)

1 BootStrap.main()

首先,整個程式是從 BootStrap 開始啟動的,執行的是 BootStrap.main() :

public static void main(String args[]) {

    synchronized (daemonLock) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to
            // prevent a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
    }

    try {
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }

        if (command.equals("startd")) {
            args[args.length - 1] = "start";
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stopd")) {
            args[args.length - 1] = "stop";
            daemon.stop();
        } else if (command.equals("start")) {
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (null == daemon.getServer()) {
                System.exit(1);
            }
            System.exit(0);
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }
}

在最開始,執行的是一段 synchronized 的同步程式碼,這裡執行程式碼 bootstrap.init() 初始化了 bootstrap 物件,具體做了什麼跟進去看下:

public void init() throws Exception {
    // 初始化類載入器
    initClassLoaders();
    // 設定執行緒類載入器,將容器的載入器傳入
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    // 載入安全類
    SecurityClassLoad.securityClassLoad(catalinaLoader);
    // 初始化日誌
    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    // 注意這裡,通過反射載入了 Catalina
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // Set the shared extensions class loader
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    // 呼叫了上面載入的 Catalina 中的 setParentClassLoader 方法
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    // 將類載入器賦值成一個引數
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    // 看名字意思是一個共享載入器
    paramValues[0] = sharedLoader;
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    // 呼叫了 Catalina 內的 setParentClassLoader 方法對 Catalina 類內的類載入器賦值
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;
}

註釋都加好了,就不多解釋了,我們接著回到之前的 main ,接下來還有一句是 daemon = bootstrap ,就是把我們剛才初始化過的 bootstrap 賦值給了 daemon 這個變數。

接下來是一大段的判斷,主要是用來判斷輸入引數,這裡面我們關注的就中間那一小段,當輸入引數為 start 的時候,總共做了兩件大事:

  • daemon.load(args)
  • daemon.start()

先跟進去看下 daemon.load(args) 都幹了點啥:

private void load(String[] arguments) throws Exception {
    // Call the load() method
    String methodName = "load";
    Object param[];
    Class<?> paramTypes[];
    if (arguments==null || arguments.length==0) {
        paramTypes = null;
        param = null;
    } else {
        paramTypes = new Class[1];
        paramTypes[0] = arguments.getClass();
        param = new Object[1];
        param[0] = arguments;
    }
    // 這裡的 catalinaDaemon 剛才在 init() 方法裡面進行了初始化,所以這裡呼叫的實際上是 Catalina 的 load 方法
    Method method =
        catalinaDaemon.getClass().getMethod(methodName, paramTypes);
    if (log.isDebugEnabled()) {
        log.debug("Calling startup class " + method);
    }
    method.invoke(catalinaDaemon, param);
}

這裡實際上是開啟了整個鏈式呼叫,接著往下追,看下 Catalina 裡面的 load 方法。

在這裡,會有一個方法的過載 load(String args[])load() ,不過關係不大,最終都是會呼叫到那個無參的方法上的。

public void load() {

    if (loaded) {
        return;
    }
    loaded = true;

    long t1 = System.nanoTime();

    initDirs();

    // Before digester - it may be needed
    initNaming();

    // Create and execute our Digester
    Digester digester = createStartDigester();

    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {
        try {
            file = configFile();
            inputStream = new FileInputStream(file);
            inputSource = new InputSource(file.toURI().toURL().toString());
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("catalina.configFail", file), e);
            }
        }
        if (inputStream == null) {
            try {
                inputStream = getClass().getClassLoader()
                    .getResourceAsStream(getConfigFile());
                inputSource = new InputSource
                    (getClass().getClassLoader()
                        .getResource(getConfigFile()).toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail",
                            getConfigFile()), e);
                }
            }
        }

        // This should be included in catalina.jar
        // Alternative: don't bother with xml, just create it manually.
        if (inputStream == null) {
            try {
                inputStream = getClass().getClassLoader()
                        .getResourceAsStream("server-embed.xml");
                inputSource = new InputSource
                (getClass().getClassLoader()
                        .getResource("server-embed.xml").toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail",
                            "server-embed.xml"), e);
                }
            }
        }


        if (inputStream == null || inputSource == null) {
            if  (file == null) {
                log.warn(sm.getString("catalina.configFail",
                        getConfigFile() + "] or [server-embed.xml]"));
            } else {
                log.warn(sm.getString("catalina.configFail",
                        file.getAbsolutePath()));
                if (file.exists() && !file.canRead()) {
                    log.warn("Permissions incorrect, read permission is not allowed on the file.");
                }
            }
            return;
        }

        try {
            inputSource.setByteStream(inputStream);
            digester.push(this);
            digester.parse(inputSource);
        } catch (SAXParseException spe) {
            log.warn("Catalina.start using " + getConfigFile() + ": " +
                    spe.getMessage());
            return;
        } catch (Exception e) {
            log.warn("Catalina.start using " + getConfigFile() + ": " , e);
            return;
        }
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                // Ignore
            }
        }
    }

    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

    // 前面都是在初始化載入各種配置檔案,使用了 Digester
    // Stream redirection
    initStreams();

    // Start the new server
    try {
        // 開始呼叫的 Server 的初始化方法
        // Server 是一個介面,並且繼承了 Lifecycle ,進行生命週期的管理
        getServer().init();
    } catch (LifecycleException e) {
        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
            throw new java.lang.Error(e);
        } else {
            log.error("Catalina.start", e);
        }
    }

    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
    }
}

這一大坨大多數都是在載入各種配置檔案,在這裡,會完成對 server.xml 檔案的解析。

我們關心的其實就只有最後那段 try...catch 裡面的那一句 getServer().init() ,開始呼叫 Server 的初始化方法,這個方法就是開啟一系列容器元件的載入方法。

不過看到最後一句的 log 列印,忽然有印象了,之前在啟動 Tomcat 的時候,在日誌的最後部分,都會看到這句列印,而且可以看到的是,這裡並不是我之前以為的是使用毫秒數算出來的,而是取的納秒數通過除 1000000 計算得出的,難道是這樣算的更準?

getServer().init() 實際上是呼叫了 org.apache.catalina.Server 中的 init() 方法,而 org.apache.catalina.Server 則是一個介面,還繼承了 org.apache.catalina.Lifecycle 進行容器生命週期的管理。

而抽象類 org.apache.catalina.util.LifecycleBase 則是實現了 org.apache.catalina.Lifecycle 介面,我們在 org.apache.catalina.Lifecycle 中開啟 init() 方法:

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() 這個方法,從名字上來看就是一個初始化方法,點過去一看,結果是一個抽象方法,實現類如下:

我們到 org.apache.catalina.core.StandardServer 中去看下其中的 initInternal() 方法,在這個方法的最後,看到了迴圈 Service 並且呼叫了 service.init()

for (Service service : services) {
    service.init();
}

因為一個 Server 是可以有多個 Service 的,所以這裡用了一個迴圈,接下來就是一個順序的容器初始化的呼叫過程:

StandardServer -> StandardService -> StandardEngine -> Connector

每個容器都在初始化自身相關設定的同時,將子容器初始化。

Tomcat 在啟動初始化的時候,是通過鏈條式的呼叫,每初始化一個完成一個元件,就會在元件內呼叫下一個元件的初始化方法。

同樣的操作在啟動的時候也是一樣的,不知道各位是否還記得我們在 Bootstrap.main() 中還有另一句程式碼 daemon.start()

public void start() throws Exception {
    if (catalinaDaemon == null) {
        init();
    }

    Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
    method.invoke(catalinaDaemon, (Object [])null);
}

操作嘛都是一樣的操作,從這段程式碼開始,呼叫了 Catalina.start() 的方法,然後開啟了另一個鏈式呼叫。

我就不多說了,留給各位讀者自己去翻翻看原始碼吧。

參考

https://segmentfault.com/a/1190000023475177

https://zhuanlan.zhihu.com/p/75723328

相關文章