SpringBoot成長記9:onRefresh如何啟動(dòng)內(nèi)嵌的Tomcat容器的驮肉?

file

上一節(jié)我們主要分析了refreshContext中熏矿,主要有3個(gè)邏輯,如下圖:

file

上一節(jié)重點(diǎn)解析了invokeBeanFactoryPostProcessors執(zhí)行容器擴(kuò)展點(diǎn)缆八,實(shí)現(xiàn)了自動(dòng)裝備配置曲掰、第三方執(zhí)行擴(kuò)展的執(zhí)行。

今天我們繼續(xù)分析refreshContext另一個(gè)重要的邏輯onRefresh()邏輯奈辰,讓我們開始吧栏妖!

快速概覽: onRefresh啟動(dòng)內(nèi)嵌tomcat前的操作

refreshContext中onRefresh之前還有一些邏輯,我們先來快速看下它們主要做了什么奖恰。首先來看下代碼:

    @Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
              //省略

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

            //省略
    }

上面主要涉及了3個(gè)方法吊趾,從名字行就能猜出來它們做了什么:

1)registerBeanPostProcessors 通過掃描到BeanDefination中找出BeanPostProcessor,增加幾個(gè)Bean的擴(kuò)展點(diǎn)BeanPostProcessor 按4類順序逐個(gè)增加瑟啃。

回顧術(shù)語BeanPostProcessor是什么论泛?

之前BeanFactoryPostProcessor是對(duì)容器的擴(kuò)展,主要有一個(gè)方法蛹屿,可以給容器設(shè)置屬性屁奏,補(bǔ)充一些單例對(duì)象,補(bǔ)充一些BeanDefinition错负。

那BeanPostProcessor是對(duì)bean的擴(kuò)展坟瓢,有before和after兩類方法,對(duì)Bean如何做擴(kuò)展犹撒,在bean的創(chuàng)建前后折联,給bean補(bǔ)充一些屬性等。

**2)initMessageSource 注冊(cè)消息M essageSource對(duì)象到容器 **DelegatingMessageSource 國際化相關(guān)支持识颊,默認(rèn)的沒有诚镰。

**3)initApplicationEventMulticaster 注冊(cè)廣播對(duì)象到容器 ** 這個(gè)對(duì)象就是之前觸發(fā)listener擴(kuò)展點(diǎn)的廣播對(duì)象。

熟悉了onRefresh方法之前的大體邏輯后,目前為止清笨,整個(gè)rereshConext()執(zhí)行的邏輯主要如下:

file

onRefresh的核心脈絡(luò)

熟悉了onRefresh方法之前的大體邏輯后月杉,接下來我們就先研究下onRefresh的核心脈絡(luò)在做什么了。

    //ServletWebServerApplicationContext.java
    @Override
    protected void onRefresh() {
        super.onRefresh();
        try {
            createWebServer();
        }
        catch (Throwable ex) {
            throw new ApplicationContextException("Unable to start web server", ex);
        }
    }
    //父類GenericWebApplicationContext.java
    @Override
    protected void onRefresh() {
        this.themeSource = UiApplicationContextUtils.initThemeSource(this);
    }

這個(gè)onRefresh方法的脈絡(luò)其實(shí)很簡單抠艾,父類沒有什么邏輯沙合,核心應(yīng)該就是createWebServer了,我們繼續(xù)來看下:

    private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = getServletContext();
        if (webServer == null && servletContext == null) {
            ServletWebServerFactory factory = getWebServerFactory();
            this.webServer = factory.getWebServer(getSelfInitializer());
        }
        else if (servletContext != null) {
            try {
                getSelfInitializer().onStartup(servletContext);
            }
            catch (ServletException ex) {
                throw new ApplicationContextException("Cannot initialize servlet context", ex);
            }
        }
        initPropertySources();
    }

這個(gè)邏輯其實(shí)很有意思跌帐,主要的核心脈絡(luò)是if-else-if

1)如果webServer和servletContext為空首懈,就創(chuàng)建一個(gè)WebServer,之后執(zhí)行initPropertySources谨敛。

2)否則就使用getSelfInitializer究履,執(zhí)行onStartup方法,之后執(zhí)行initPropertySources脸狸。

可以默認(rèn)情況webServer和servletContext為空的最仑,這個(gè)我們?cè)谥胺治龅恼麄€(gè)流程中沒看到過任何關(guān)于這兩個(gè)組件的邏輯,或者你自己斷點(diǎn)也能明顯的找到代碼執(zhí)行的路徑炊甲。

這里其實(shí)你會(huì)發(fā)現(xiàn)泥彤,判斷走哪個(gè)分支的方法可能不止一種,你可以連蒙帶猜卿啡,也可以斷點(diǎn)走一下吟吝,也可以根據(jù)經(jīng)驗(yàn)分析等等。方法有很多颈娜,大家千萬分析原理或者源碼的時(shí)候不要陷入追尋有哪些方法上剑逃。方法不是最重要的,適合你自己的就好官辽。這個(gè)思想很關(guān)鍵蛹磺,你可以模仿,但不能完全照搬同仆,一定要結(jié)合自己情況考慮萤捆,最終才能成為你自己的。自己悟到的才是自己的俗批,這些也是思想俗或,也是最關(guān)鍵的。所以不要總問我有哪些方法扶镀,有時(shí)候是你自己悟出來的蕴侣,我只能提醒或者建議焰轻。

好了臭觉,言歸正傳,這里實(shí)際走的路徑就是第一條了。如下圖所示:

file

最終蝠筑,你會(huì)發(fā)現(xiàn)onRefresh涉及的核心組件ServletWebServerFactory狞膘、WebServerServletContext什乙。

SpringBoot對(duì)web容器的抽象封裝和設(shè)計(jì)

既然之前涉及到了幾個(gè)組件ServletWebServerFactory挽封、WebServerServletContext臣镣。 那它們是分別是什么東西呢辅愿?

其實(shí)從名字就能猜出很多東西,不難想到:

ServletContext忆某,這個(gè)是指處理整個(gè)web請(qǐng)求是的上下文對(duì)象点待,在Tocmat中通常是整個(gè)請(qǐng)求的上下文參數(shù)都封裝在這個(gè)對(duì)象中了,非常關(guān)鍵的對(duì)象癞埠。

ServletWebServerFactory和WebServer是什么?很明顯ServletWebServerFactory是個(gè)工廠聋呢,用來創(chuàng)建WebServer。

而WebServer從接口中定義的方法就可以看出來碗啄,封裝了web容器的啟動(dòng)和停止,獲取端口的核心操作窄驹,也就是說WebServer是web容器的一個(gè)抽象封裝烦衣。

@FunctionalInterface
public interface ServletWebServerFactory {

   /**
    * Gets a new fully configured but paused {@link WebServer} instance. Clients should
    * not be able to connect to the returned server until {@link WebServer#start()} is
    * called (which happens when the {@code ApplicationContext} has been fully
    * refreshed).
    * @param initializers {@link ServletContextInitializer}s that should be applied as
    * the server starts
    * @return a fully configured and started {@link WebServer}
    * @see WebServer#stop()
    */
   WebServer getWebServer(ServletContextInitializer... initializers);

}
public interface WebServer {

    /**
     * Starts the web server. Calling this method on an already started server has no
     * effect.
     * @throws WebServerException if the server cannot be started
     */
    void start() throws WebServerException;

    /**
     * Stops the web server. Calling this method on an already stopped server has no
     * effect.
     * @throws WebServerException if the server cannot be stopped
     */
    void stop() throws WebServerException;

    /**
     * Return the port this server is listening on.
     * @return the port (or -1 if none)
     */
    int getPort();

}

從上面兩個(gè)接口的設(shè)計(jì)和注釋看

首先ServletWebServerFactory的getWebServer注釋翻譯: 獲取一個(gè)新的完全配置但暫停的 {@link WebServer} 實(shí)例秸歧。 客戶應(yīng)無法連接到返回的服務(wù)器,直到 {@link WebServer#start()} 是調(diào)用(當(dāng) {@code ApplicationContext} 已完全刷新)衅澈。

也就是說键菱,這個(gè)方法意思就是獲取到一個(gè)配置好的webServer容器,在調(diào)用start方法時(shí)啟動(dòng)容器今布,啟動(dòng)時(shí)候ApplicationContext经备,也就是Spring容器已經(jīng)完成了刷新。

WebServer接口封裝了web容器的常見操作部默,如啟動(dòng)侵蒙、停止,獲取端口號(hào)之類的傅蹂。

也就是說ServletWebServerFactory可以獲得一個(gè)web容器纷闺,WebServer可以操作一個(gè)容器。

并且從下面的圖中可以看出份蝴,它們有很多不同web容器的實(shí)現(xiàn)犁功。整體如下圖所示:

file

綜上,最終你可以理解為WebServer和ServletWebServerFactory婚夫,這一套浸卦,其實(shí)就是SpringBoot對(duì)web容器的抽象封裝,WebServer可以代表了整個(gè)容器案糙。

了解了onRefesh的整體脈絡(luò)和關(guān)鍵的組件之后限嫌,我們來看下如何創(chuàng)建webServer的靴庆。

默認(rèn)情況我們獲取到的是TomcatServletWebServerFactory,通過它來創(chuàng)建

//TomcatServletWebServerFactory.java
public WebServer getWebServer(ServletContextInitializer... initializers) {
        if (this.disableMBeanRegistry) {
            Registry.disableRegistry();
        }
        Tomcat tomcat = new Tomcat();
        File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = new Connector(this.protocol);
        connector.setThrowOnFailure(true);
        tomcat.getService().addConnector(connector);
        customizeConnector(connector);
        tomcat.setConnector(connector);
        tomcat.getHost().setAutoDeploy(false);
        configureEngine(tomcat.getEngine());
        for (Connector additionalConnector : this.additionalTomcatConnectors) {
            tomcat.getService().addConnector(additionalConnector);
        }
        prepareContext(tomcat.getHost(), initializers);
        return getTomcatWebServer(tomcat);
    }

整個(gè)方法脈絡(luò)如下:

1)入?yún)⑹且粋€(gè)java8定義的函數(shù)表達(dá)式萤皂,也就是參數(shù)傳遞進(jìn)來了一個(gè)方法,使用的是函數(shù)式接口ServletContextInitializer匣椰。這個(gè)方法在后面應(yīng)該會(huì)被執(zhí)行的裆熙。

2)創(chuàng)建了核心組件Tomcat,一會(huì)可以看下它的核心脈絡(luò)禽笑,里面封裝了Server

3)創(chuàng)建和配置組件Connector入录, new Connector()、customizeConnector這個(gè)是Tomcat的Connector組件相關(guān)

4)創(chuàng)建和配置組件Engine getEngine佳镜、 configureEngine tomcat的Engine組件相關(guān)設(shè)置

5)prepareContext 準(zhǔn)備tomcat的context相關(guān)

6)getTomcatWebServer 真正啟動(dòng)tomcat

畫成圖如下所示:

file

看完這個(gè)方法后僚稿,你可能對(duì)這里涉及的很多組件比較陌生,因?yàn)樯婕暗搅撕芏鄑omcat的組件蟀伸。不過沒有關(guān)系蚀同,你可以通過之前學(xué)習(xí)的方法來梳理這些組件的關(guān)系。就算不知道每個(gè)組件是干嘛的啊掏,也可以連蒙帶猜下蠢络。

new Tomcat核心組件和脈絡(luò)分析

這里我就來帶教大家一起用之前的方法和思路分析一下它們的核心脈絡(luò)吧。

首先第一個(gè)組件就是Tomcat這個(gè)類的創(chuàng)建迟蜜。老方法刹孔,可以看下這個(gè)類脈絡(luò)、構(gòu)造方法之后娜睛,畫一個(gè)圖髓霞。

首先看下構(gòu)造方法

    public Tomcat() {
        ExceptionUtils.preload();
    }

你會(huì)發(fā)現(xiàn)什么都么有,只有一個(gè)異常工具預(yù)加載的處理畦戒。一看就不是重點(diǎn)方库。

那就再看下這個(gè)類的整體脈絡(luò)吧:

file
file

看完這個(gè)類的脈絡(luò),可以看出來Tomcat這個(gè)類主要有
1)對(duì)Wrapper相關(guān)的操作方法障斋,比如addServelt方法就是返回一個(gè)Wrapper薪捍。

2)有Context相關(guān)一些方法,createContext配喳、addWebapp之類的

3)有一堆組件的get方法酪穿,比如Connector、Engine晴裹、Service被济、Server、Host

4)最后就是一些屬性了涧团,比如Server對(duì)象只磷、端口號(hào)经磅、hostname、用戶權(quán)限相關(guān)Userpass/UserRole之類的钮追。

雖然我們不知道這個(gè)類里面的那些組件是干嘛的预厌。但是起碼我們有了一個(gè)印象≡模可以感受到這個(gè)Tomcat類轧叽,封裝了幾乎Tomcat所有的核心組件,是一個(gè)對(duì)tomcat容器的一個(gè)抽象對(duì)象刊棕,代表了整個(gè)tomcat炭晒。

最后我們可以畫圖先列舉下看完Tomcat這個(gè)類的構(gòu)造函數(shù)和類脈絡(luò)中,主要涉及概念或者說是組件甥角,如下圖所示:

file

不知道你們目前是什么感受网严,感覺有好多新的概念。如果你不了解tomcat的原理的話嗤无,第一次看到這一大堆組件震束,肯定有點(diǎn)懵的。

不過沒關(guān)系当犯,你其實(shí)可以連蒙帶猜驴一,或者抓大放小,因?yàn)槲覀冎饕€是看SpringBoot如何啟動(dòng)內(nèi)嵌tomcat灶壶,如何和tomcat整合Spring容器的肝断。

所以你沒必要非要弄清楚這些組件,等之后我們Tomcat成長記驰凛,研究tomcat的原理和源碼時(shí)候再來仔細(xì)弄清楚胸懈。

這里我們還是找到關(guān)注的重點(diǎn)就可以了。

好恰响,我們接著向下分析趣钱。

Connector基本創(chuàng)建和擴(kuò)展設(shè)計(jì)

最高層的抽象封裝Tomcat對(duì)象創(chuàng)建完成后,下一個(gè)核心創(chuàng)建的就是Connector了胚宦。創(chuàng)建它的代碼如下:

public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";

private String protocol = DEFAULT_PROTOCOL; 

public WebServer getWebServer(ServletContextInitializer... initializers) {
    //其他
    Tomcat tomcat = new Tomcat();
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    Connector connector = new Connector(this.protocol);
    connector.setThrowOnFailure(true);
    tomcat.getService().addConnector(connector);
    customizeConnector(connector);
    //其他
 }

可以看到Connector創(chuàng)建默認(rèn)傳入了一個(gè)Http11NioProtocol類的全名首有,當(dāng)然你可以通過set方法修改這個(gè)protocol的默認(rèn)值,只要獲取到TomcatServletWebServerFactory就可以修改對(duì)吧枢劝?

至于為啥傳遞了類的全名井联,你猜想下都知道,它內(nèi)部可能是通過反射創(chuàng)建了這個(gè)類您旁,并把這個(gè)組件設(shè)置給了Connector烙常。我們來看下是不是:

public Connector(String protocol) {
    boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
            AprLifecycleListener.getUseAprConnector();

    if ("HTTP/1.1".equals(protocol) || protocol == null) {
        if (aprConnector) {
            protocolHandlerClassName = "org.apache.coyote.http11.Http11AprProtocol";
        } else {
            protocolHandlerClassName = "org.apache.coyote.http11.Http11NioProtocol";
        }
    } else if ("AJP/1.3".equals(protocol)) {
        if (aprConnector) {
            protocolHandlerClassName = "org.apache.coyote.ajp.AjpAprProtocol";
        } else {
            protocolHandlerClassName = "org.apache.coyote.ajp.AjpNioProtocol";
        }
    } else {
        protocolHandlerClassName = protocol;
    }

    // Instantiate protocol handler
    ProtocolHandler p = null;
    try {
        Class<?> clazz = Class.forName(protocolHandlerClassName);
        p = (ProtocolHandler) clazz.getConstructor().newInstance();
    } catch (Exception e) {
        log.error(sm.getString(
                "coyoteConnector.protocolHandlerInstantiationFailed"), e);
    } finally {
        this.protocolHandler = p;
    }

    // Default for Connector depends on this system property
    setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));
}

這個(gè)構(gòu)造函數(shù)最關(guān)鍵的就是3行代碼:

public Connector(String protocol) {
  Class<?> clazz = Class.forName(protocolHandlerClassName);
  p = (ProtocolHandler) clazz.getConstructor().newInstance();
  this.protocolHandler = p;
 }

也就是說其實(shí)new Connector核心就做了一件事情:創(chuàng)建了一個(gè)Http11NioProtocol組件寒瓦。

這個(gè)從名字上看就是一個(gè)NIO相關(guān)的通信組件谢谦,內(nèi)部應(yīng)該會(huì)有Selector懒豹、Channel污它、Bytebuffer等NIO核心組件的。

至于Http11NioProtocol如何創(chuàng)建的這里我就不帶大家深究了驼鞭,你可以分析它的構(gòu)造函數(shù)秦驯、類脈絡(luò)、畫一個(gè)組件圖分析下它的創(chuàng)建過程挣棕,或者之后我們Tomcat成長記會(huì)詳細(xì)分析的译隘,可之后帶大家一起分析下。

到這里先畫個(gè)圖小結(jié)下:

file

new Connector之后就是非常關(guān)鍵的擴(kuò)展點(diǎn)執(zhí)行了customizeConnector()方法穴张。

這個(gè)方法實(shí)際是SpringBoot對(duì)Connector擴(kuò)展設(shè)計(jì)的接入细燎,可以修改Connector中很多配置和屬性两曼,讓我們來一起看下皂甘。

private Set<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new LinkedHashSet<>();

private Set<TomcatProtocolHandlerCustomizer<?>> tomcatProtocolHandlerCustomizers = new LinkedHashSet<>();

protected void customizeConnector(Connector connector) {
        int port = Math.max(getPort(), 0);
        connector.setPort(port);
        if (StringUtils.hasText(this.getServerHeader())) {
            connector.setAttribute("server", this.getServerHeader());
        }
        if (connector.getProtocolHandler() instanceof AbstractProtocol) {
            customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler());
        }
        invokeProtocolHandlerCustomizers(connector.getProtocolHandler());
        if (getUriEncoding() != null) {
            connector.setURIEncoding(getUriEncoding().name());
        }
        // Don't bind to the socket prematurely if ApplicationContext is slow to start
        connector.setProperty("bindOnInit", "false");
        if (getSsl() != null && getSsl().isEnabled()) {
            customizeSsl(connector);
        }
        TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(getCompression());
        compression.customize(connector);
        for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) {
            customizer.customize(connector);
        }
}

這個(gè)擴(kuò)展方法核心的邏輯就是在給connector進(jìn)行一些屬性設(shè)置,核心通過了兩個(gè)擴(kuò)展進(jìn)行調(diào)用悼凑。

1)invokeProtocolHandlerCustomizers 執(zhí)行對(duì)ProtocolHandler擴(kuò)展

2)customizer.customize(connector); 執(zhí)行對(duì)Connector的擴(kuò)展

其實(shí)可以看到觸發(fā)的都是tomcatConnectorCustomizers偿枕、tomcatProtocolHandlerCustomizers這兩個(gè)集合中的擴(kuò)展類。户辫、

整體如下圖所示:

file

我們先不著急看這些擴(kuò)展類做了什么渐夸,首先的得思考下,tomcatConnectorCustomizers渔欢、tomcatProtocolHandlerCustomizers這兩個(gè)集合中的擴(kuò)展類什么時(shí)候設(shè)置的值呢墓塌?

其實(shí)你想下,這兩個(gè)屬性屬于誰呢奥额?沒錯(cuò)苫幢,屬于TomcatServletWebServerFactory。而這個(gè)類是不是之前通過ServletWebServerApplicationContext執(zhí)行onRefresh脈絡(luò)時(shí)候獲取到的呢垫挨?如下圖:

file

對(duì)應(yīng)獲取的代碼如下:

//ServletWebServerApplicationContext.java
protected ServletWebServerFactory getWebServerFactory() {
   // Use bean names so that we don't consider the hierarchy
   String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
   if (beanNames.length == 0) {
      throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
            + "ServletWebServerFactory bean.");
   }
   if (beanNames.length > 1) {
      throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
            + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
   }
   return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

可以看到的是韩肝,這個(gè)方法核心就是通過getBean從容器獲取一個(gè)對(duì)象。但是其實(shí)容器中并沒有ServletWebServerFactory這個(gè)對(duì)象九榔,只有它的BeanDefinition哀峻。

為什么呢?因?yàn)橹拔覀儓?zhí)行ConfigurationClassPostProcessor時(shí)候哲泊,只是加載了Bean對(duì)應(yīng)的BeanDefinition而已剩蟀。

不過沒關(guān)系,getBean中的邏輯是切威,如果容器沒有但是有對(duì)應(yīng)的BeanDefinition喻旷,它會(huì)進(jìn)行Bean實(shí)例化,Bean的實(shí)例化我們下一節(jié)會(huì)詳細(xì)講牢屋,這里只是簡單提下且预。

那這個(gè)bean槽袄,ServletWebServerFactory實(shí)例化的時(shí)候會(huì)做什么呢?除了基本構(gòu)造函數(shù)外锋谐,其實(shí)Bean實(shí)例化的過程有很多擴(kuò)展點(diǎn)遍尺,可以為bean設(shè)置屬性。

好涮拗,那關(guān)鍵的就來了乾戏,tomcatConnectorCustomizers、tomcatProtocolHandlerCustomizers既然是ServletWebServerFactory的兩個(gè)屬性三热,肯定就可以通過Bean實(shí)例化時(shí)候的擴(kuò)展點(diǎn)鼓择,給這兩個(gè)屬性設(shè)置進(jìn)去值。

最終就漾,我給大家概況如下圖所示:

file

至于Connector中每個(gè)customizer做了哪些事情呐能,這里我們不去詳細(xì)分析了

大體就是初始化protocol相關(guān)的配置,比如setMaxThreads默認(rèn)200抑堡、minSpareThreads默認(rèn)10摆出、maxHttpHeaderSize默認(rèn)8192byte 、maxSwallowSize 2097152等等首妖。

熟悉了這個(gè)擴(kuò)展點(diǎn)的邏輯后偎漫,其實(shí)最關(guān)鍵的是如何使用它,你可以通過ServerProperties擴(kuò)展配置值有缆,也可以自定義tomcatConnectorCustomizers或者tomcatProtocolHandlerCustomizers象踊,只要實(shí)現(xiàn)對(duì)應(yīng)的接口就可以了。這個(gè)才是領(lǐng)悟了SpringBoot的設(shè)計(jì)思路后最關(guān)鍵的棚壁。

術(shù)語普及:Tomcat的Engine杯矩、Context、Host灌曙、Wrapper關(guān)系

分析完了Connector的創(chuàng)建之后菊碟,其他的組件其實(shí)就是普通的創(chuàng)建,建立關(guān)聯(lián)關(guān)系而已在刺。它們的關(guān)系其實(shí)不復(fù)雜逆害,屬于tomcat的基本知識(shí),這里我通過一個(gè)tomcat流程執(zhí)行圖給大家介紹下它們的關(guān)系即可蚣驼。它們之間的關(guān)系如下圖所示:

file

這些組件每個(gè)都有自己的職責(zé)魄幕,你大體了解上述組件的關(guān)系就可以了,我們就不展開分析了颖杏。

當(dāng)然SpringBoot也有一些對(duì)它們的擴(kuò)展纯陨,比如對(duì)Engine、Context閥門的擴(kuò)展。也是通過engineValves翼抠、contextValves兩個(gè)list屬性進(jìn)行擴(kuò)展咙轩。

//TomcatServletWebServerFactory.java
private List<Valve> engineValves = new ArrayList<>();

private List<Valve> contextValves = new ArrayList<>();

只不過這兩個(gè)集合默認(rèn)是空的,你可以通過TomcatServletWebServerFactory對(duì)他們進(jìn)行設(shè)置和擴(kuò)展阴颖。

這里我也不展開了活喊。

記住,只要你理解了SpringBoot圍繞TomcatServletWebServerFactory對(duì)tomcat做封裝和擴(kuò)展是關(guān)鍵量愧,就可以了钾菊。

prepareContext 中的擴(kuò)展點(diǎn)ServletContextInitializer

前面一堆組件創(chuàng)建完成后,還有一個(gè)比較有意思的操作就是prepareContext 偎肃。

讓我們來看下吧煞烫!它的代碼如下:

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
   File documentRoot = getValidDocumentRoot();
   TomcatEmbeddedContext context = new TomcatEmbeddedContext();
   if (documentRoot != null) {
      context.setResources(new LoaderHidingResourceRoot(context));
   }
   context.setName(getContextPath());
   context.setDisplayName(getDisplayName());
   context.setPath(getContextPath());
   File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
   context.setDocBase(docBase.getAbsolutePath());
   context.addLifecycleListener(new FixContextListener());
   context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
         : ClassUtils.getDefaultClassLoader());
   resetDefaultLocaleMapping(context);
   addLocaleMappings(context);
   context.setUseRelativeRedirects(false);
   try {
      context.setCreateUploadTargets(true);
   }
   catch (NoSuchMethodError ex) {
      // Tomcat is < 8.5.39. Continue.
   }
   configureTldSkipPatterns(context);
   WebappLoader loader = new WebappLoader(context.getParentClassLoader());
   loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
   loader.setDelegate(true);
   context.setLoader(loader);
   if (isRegisterDefaultServlet()) {
      addDefaultServlet(context);
   }
   if (shouldRegisterJspServlet()) {
      addJspServlet(context);
      addJasperInitializer(context);
   }
   context.addLifecycleListener(new StaticResourceConfigurer(context));
   ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
   host.addChild(context);
   configureContext(context, initializersToUse);
   postProcessContext(context);
}

整個(gè)方法的脈絡(luò)其實(shí)不復(fù)雜,主要就是:

1)new TomcatEmbeddedContext

2)為tomcat的這個(gè)Context設(shè)置了很多值

3)執(zhí)行了一個(gè)擴(kuò)展點(diǎn)ServletContextInitializer

整體如下圖所示:

file

至于擴(kuò)展點(diǎn),ServletContextInitializer執(zhí)行了什么累颂?

其實(shí)可以看下它的邏輯滞详。它是使用了java8的特性,通過一個(gè)函數(shù)式接口傳入過來的方法喘落,也就是說茵宪,通過方法參數(shù)傳遞過來了一個(gè)行為最冰,而不是一個(gè)變量瘦棋。

我們可以找到傳入的位置:

//ServletWebServerApplicationContext.java
    private void selfInitialize(ServletContext servletContext) throws ServletException {
        prepareWebApplicationContext(servletContext);
        registerApplicationScope(servletContext);
        WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
        for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
            beans.onStartup(servletContext);
        }
    }

方法其實(shí)不復(fù)雜,核心就是觸發(fā)了ServletContextInitializer的所有實(shí)現(xiàn)暖哨,執(zhí)行了擴(kuò)展方法onStartup赌朋。

默認(rèn)主要有4個(gè)實(shí)現(xiàn):

result = {ServletContextInitializerBeans@6345}  size = 4
 0 = {DispatcherServletRegistrationBean@6339} "dispatcherServlet urls=[/]"
 1 = {FilterRegistrationBean@6350} "characterEncodingFilter urls=[/*] order=-2147483648"
 2 = {FilterRegistrationBean@6351} "formContentFilter urls=[/*] order=-9900"
 3 = {FilterRegistrationBean@6352} "requestContextFilter urls=[/*] order=-105"

其實(shí)從名字就看出來了,它的含義是往ServletContext中注冊(cè)一堆Servelt篇裁、Filter等等沛慢。

這個(gè)擴(kuò)展點(diǎn)還是比較關(guān)鍵的。

整體如下圖所示:

file

思考:tomcat和SpringBoot怎么整合的?

分析完了整個(gè)WebServer的創(chuàng)建后达布,其實(shí)你就會(huì)發(fā)現(xiàn):

最終是Spring的容器ServletWebServerApplicationContext創(chuàng)建了WebServer团甲,它持有了這對(duì)象,也就有了Tomcat整個(gè)抽象封裝黍聂。

自然它們就整合到了一起了躺苦。

內(nèi)嵌Tomcat最終的啟動(dòng)

之前分析的整個(gè)邏輯都是webServer這個(gè)對(duì)象的創(chuàng)建,之前從注釋我們就知道产还,創(chuàng)建的webServer只是一個(gè)配置完成匹厘,停止的web容器,web容器并沒有啟動(dòng)脐区。只有調(diào)用webServer#start()這個(gè)方法愈诚,容器才會(huì)真正啟動(dòng)。

所以,最后我們來分析下tomcat是如何啟動(dòng)的炕柔。啟動(dòng)的代碼就是getWebServer的最后一行酌泰,代碼如下:

//TomcatServletWebServerFactory.java
    @Override
    public WebServer getWebServer(ServletContextInitializer... initializers) {
        //省略 Tomcat的創(chuàng)建、connector的創(chuàng)建和擴(kuò)展匕累、其他組件的創(chuàng)建宫莱、prepareContext的執(zhí)行和擴(kuò)展
        return getTomcatWebServer(tomcat);
    }
    protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
        return new TomcatWebServer(tomcat, getPort() >= 0);
    }
    public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
            Assert.notNull(tomcat, "Tomcat Server must not be null");
            this.tomcat = tomcat;
            this.autoStart = autoStart;
            initialize();
    }

可以看到上面的代碼脈絡(luò)是:通過一系列的方法調(diào)用最終將創(chuàng)建的tomcat對(duì)象,有封裝了一下哩罪,封裝為了TomcatWebServer對(duì)象,之后執(zhí)行了initialize()授霸。

private void initialize() throws WebServerException {
        logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
        synchronized (this.monitor) {
            try {
                addInstanceIdToEngineName();

                Context context = findContext();
                context.addLifecycleListener((event) -> {
                    if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
                        // Remove service connectors so that protocol binding doesn't
                        // happen when the service is started.
                        removeServiceConnectors();
                    }
                });

                // Start the server to trigger initialization listeners
                this.tomcat.start();

                // We can re-throw failure exception directly in the main thread
                rethrowDeferredStartupExceptions();

                try {
                    ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
                }
                catch (NamingException ex) {
                    // Naming is not enabled. Continue
                }

                // Unlike Jetty, all Tomcat threads are daemon threads. We create a
                // blocking non-daemon to stop immediate shutdown
                startDaemonAwaitThread();
            }
            catch (Exception ex) {
                stopSilently();
                destroySilently();
                throw new WebServerException("Unable to start embedded Tomcat", ex);
            }
        }
    }

上面方法邏輯看似多,其實(shí)最關(guān)鍵的就一句話际插。這里核心是你抓大放小碘耳,主要關(guān)注一句話就可以了:

tomcat.start();

這個(gè)start方法執(zhí)行的流程很有意思。它是類似一個(gè)鏈?zhǔn)秸{(diào)用框弛。

其實(shí)你從之前tomcat的組件圖就可以猜到辛辨,它們組件層級(jí)關(guān)系很多,每個(gè)組件都會(huì)觸發(fā)下一層組件的邏輯瑟枫。

每個(gè)組件都有生命周期斗搞,比如init方法-->start()-->destory()之類的。

那么也就說tomcat會(huì)以鏈的方式逐級(jí)調(diào)用各個(gè)模塊的init()方法進(jìn)行初始化, 待各個(gè)模塊都初始化后, 又會(huì)逐級(jí)調(diào)用各個(gè)模塊的start()方法啟動(dòng)各個(gè)模塊慷妙。

整體大概如下圖所示:

file

小結(jié)

最后我們小結(jié)下僻焚,今天我們主要分析了SpringBoot在onRefresh方法中如何啟動(dòng)的tomcat:

1)快速該來了 onRefresh啟動(dòng)內(nèi)嵌tomcat前的操作

2)分析了onRefresh的核心脈絡(luò)

3)思考了SpringBoot對(duì)web容器的抽象封裝和設(shè)計(jì)

4)對(duì)new Tomcat進(jìn)行了核心組件和脈絡(luò)分析

5)分析了Connector基本創(chuàng)建和擴(kuò)展設(shè)計(jì)

6)術(shù)語普及:Tomcat的Engine、Context膝擂、Host虑啤、Wrapper關(guān)系

7)prepareContext 中的擴(kuò)展點(diǎn)ServletContextInitializer

8)思考了:tomcat和SpringBoot怎么整合的?

9)內(nèi)嵌Tomcat最終的啟動(dòng)

最后補(bǔ)充一點(diǎn)思想:

整個(gè)過程中有的是知識(shí)點(diǎn),有的是一些設(shè)計(jì)的思考架馋、擴(kuò)展點(diǎn)的思考狞山。大家一定要學(xué)會(huì)抓重點(diǎn)。這個(gè)能力非常關(guān)鍵叉寂。

這就是涉及到了一個(gè)能力模型的層次萍启,可以有這樣一種劃分

第一個(gè)層次:知識(shí)、技術(shù)基本的使用屏鳍,也就是說我們從了解知識(shí)到使用它做好一件事是一個(gè)層次勘纯。這個(gè)可以體現(xiàn)在你學(xué)習(xí)技術(shù)上,帶領(lǐng)團(tuán)隊(duì)做項(xiàng)目孕蝉,或者學(xué)習(xí)任何新事物上屡律。

第二個(gè)層次:通過思考和總結(jié),抽象和提煉出事情的關(guān)鍵點(diǎn)降淮。比如一個(gè)提煉設(shè)計(jì)思想超埋,發(fā)現(xiàn)項(xiàng)目的關(guān)鍵節(jié)點(diǎn)搏讶、路徑等。

最后一個(gè)層次:站在一定高度和視角霍殴,掌控和推進(jìn)整體媒惕。這個(gè)就需要很多經(jīng)驗(yàn)和像比你成功的人學(xué)習(xí)了,因?yàn)檎莆樟κ强梢跃毩?xí)的来庭,但是視野和思想的高度妒蔚,雖然可以經(jīng)驗(yàn)積累,但是最快的方式就是像比你優(yōu)秀的人學(xué)習(xí)月弛,如果有一個(gè)導(dǎo)師的話那就更好了肴盏。

本文由博客一文多發(fā)平臺(tái) OpenWrite 發(fā)布!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帽衙,一起剝皮案震驚了整個(gè)濱河市菜皂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌厉萝,老刑警劉巖恍飘,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異谴垫,居然都是意外死亡章母,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門翩剪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乳怎,“玉大人,你說我怎么就攤上這事肢专∥杷粒” “怎么了焦辅?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵博杖,是天一觀的道長。 經(jīng)常有香客問我筷登,道長剃根,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任前方,我火速辦了婚禮狈醉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惠险。我一直安慰自己苗傅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布班巩。 她就那樣靜靜地躺著渣慕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逊桦,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天眨猎,我揣著相機(jī)與錄音,去河邊找鬼强经。 笑死睡陪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的匿情。 我是一名探鬼主播兰迫,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼炬称!你這毒婦竟也來了逮矛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤转砖,失蹤者是張志新(化名)和其女友劉穎须鼎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體府蔗,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晋控,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了姓赤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赡译。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖不铆,靈堂內(nèi)的尸體忽然破棺而出蝌焚,到底是詐尸還是另有隱情,我是刑警寧澤誓斥,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布只洒,位于F島的核電站,受9級(jí)特大地震影響劳坑,放射性物質(zhì)發(fā)生泄漏毕谴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一距芬、第九天 我趴在偏房一處隱蔽的房頂上張望涝开。 院中可真熱鬧,春花似錦框仔、人聲如沸舀武。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽银舱。三九已至衷旅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纵朋,已是汗流浹背柿顶。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留操软,地道東北人嘁锯。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像聂薪,于是被迫代替她去往敵國和親家乘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容