1. 程式人生 > 實用技巧 >SpringBoot如何內建Tomcat

SpringBoot如何內建Tomcat

SpringBoot如何內建Tomcat

一、前言

在初次接觸 SpringBoot 的時候,就很奇怪為什麼直接執行主類的main方法就可以啟動程式,但卻一直沒有深究,直到前段時間,突然聽說了一個詞,叫做 Cargo Cult ,才開始對自己有所反省,這的確是對我目前狀態的一種描述,為了從微小的細節開始,儘自己的努力擺脫這個魔鬼一樣的詞語,今天決定探究一下 SpringBoot 究竟是如何內建 Tomcat 的(能力限制,目前只嘗試進行淺易地“溯源”)。

二、正文

1. 啟動類

從我們開始學會新建第一個 SpringBoot 專案開始,我們就一定會看到這樣一個類。

package com.xfc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 啟動
 *
 * @Auther: ErDong
 * @Email: [email protected]
 * @Date: 2019/11/30 11:51
 * @Description:
 */
@SpringBootApplication
public class MuYiApplication {

    public static void main(String[] args) {
        SpringApplication.run(MuYiApplication.class, args);// 進入run()
    }

}

我們把這個類叫做 主類 ,或者說是 啟動類 ,於是,我們進入 run() 方法。

2. SpringApplication類註釋

這裡我們首先閱讀一下 SpringApplication 的類註釋:

// SpringApplication.class 類註釋

/**
 * Class that can be used to bootstrap and launch a Spring application from a Java main
 * method. By default class will perform the following steps to bootstrap your
 * application:
 *
 * <ul>
 * <li>Create an appropriate {@link ApplicationContext} instance (depending on your
 * classpath)</li>
 * <li>Register a {@link CommandLinePropertySource} to expose command line arguments as
 * Spring properties</li>
 * <li>Refresh the application context, loading all singleton beans</li>
 * <li>Trigger any {@link CommandLineRunner} beans</li>
 * </ul>
 *
 * In most circumstances the static {@link #run(Class, String[])} method can be called
 * directly from your {@literal main} method to bootstrap your application:
 *
 * <pre class="code">
 * &#064;Configuration
 * &#064;EnableAutoConfiguration
 * public class MyApplication  {
 *
 *   // ... Bean definitions
 *
 *   public static void main(String[] args) throws Exception {
 *     SpringApplication.run(MyApplication.class, args);
 *   }
 * }
 * </pre>
 *
 * <p>
 * For more advanced configuration a {@link SpringApplication} instance can be created and
 * customized before being run:
 *
 * <pre class="code">
 * public static void main(String[] args) throws Exception {
 *   SpringApplication application = new SpringApplication(MyApplication.class);
 *   // ... customize application settings here
 *   application.run(args)
 * }
 * </pre>
 *
 * {@link SpringApplication}s can read beans from a variety of different sources. It is
 * generally recommended that a single {@code @Configuration} class is used to bootstrap
 * your application, however, you may also set {@link #getSources() sources} from:
 * <ul>
 * <li>The fully qualified class name to be loaded by
 * {@link AnnotatedBeanDefinitionReader}</li>
 * <li>The location of an XML resource to be loaded by {@link XmlBeanDefinitionReader}, or
 * a groovy script to be loaded by {@link GroovyBeanDefinitionReader}</li>
 * <li>The name of a package to be scanned by {@link ClassPathBeanDefinitionScanner}</li>
 * </ul>
 *
 * Configuration properties are also bound to the {@link SpringApplication}. This makes it
 * possible to set {@link SpringApplication} properties dynamically, like additional
 * sources ("spring.main.sources" - a CSV list) the flag to indicate a web environment
 * ("spring.main.web-application-type=none") or the flag to switch off the banner
 * ("spring.main.banner-mode=off").
 */

我們知道了 SpringApplication 是用於從 Java main 方法引導和啟動Spring應用程式,預設情況下,將執行下面幾個步驟來引導我們的應用程式:

  1. 建立一個恰當的ApplicationContext例項(取決於類路徑)
  2. 註冊CommandLinePropertySource,將命令列引數公開為Spring屬性。
  3. 重新整理應用程式上下文,載入所有單例bean。
  4. 觸發全部CommandLineRunner bean。

大多數情況下,靜態 run() 方法可以在我們的啟動類的 main() 方法中呼叫。

SpringApplication可以從各種不同的源讀取bean。 通常建議使用單個@Configuration類來引導,但是我們也可以通過以下方式來設定資源:

  1. 通過AnnotatedBeanDefinitionReader載入完全限定類名。
  2. 通過XmlBeanDefinitionReader載入XML資源位置,或者是通過GroovyBeanDefinitionReader載入groovy指令碼位置。
  3. 通過ClassPathBeanDefinitionScanner掃描包名稱。

3. 根據註釋逐步進入程式碼檢視

從主類進入 SpringApplication.run() 方法:

// SpringApplication.class

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
	return run(new Class[]{primarySource}, args);// 進入run()
}

繼續進入 run() 方法:

// SpringApplication.class

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
	return (new SpringApplication(primarySources)).run(args);// 進入run()
}

從上面兩端程式碼,我們知道,程式將我們建立的 MuYiApplication.class 新增進一個名為 primarySources 的陣列,並且使用當前數建立了一個 SpringApplication 例項。

接著,進入 SpringBoot 啟動的主要邏輯程式碼段:

// SpringApplication.class

public ConfigurableApplicationContext run(String... args) {
    // 使用StopWatch對程式部分程式碼進行計時
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();// 開始計時
    ConfigurableApplicationContext context = null;
    // 使用Collection收集錯誤報告並處理
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
    this.configureHeadlessProperty();
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting();

    Collection exceptionReporters;
    try {
        // 解析引數args
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 從這裡進入prepareEnvironment()後,可檢視註冊CommandLinePropertySource,並將命令列引數公開為Spring屬性的邏輯,並返回當前程式的配置環境,這裡暫不擴充套件說明
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
        // 讀取程式配置中的spring.beaninfo.ignore內容
        this.configureIgnoreBeanInfo(environment);
        // 列印資源目錄下banner.txt檔案中的內容
        Banner printedBanner = this.printBanner(environment);
        // 根據應用型別建立應用上下文
        context = this.createApplicationContext();
        exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
        this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        // 進入refreshContext()進行擴充套件
        this.refreshContext(context);
        // 允許上下文子類對bean工廠進行後置處理
        this.afterRefresh(context, applicationArguments);
        stopWatch.stop();// 計時結束
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
        }

        listeners.started(context);
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
        this.handleRunFailure(context, var10, exceptionReporters, listeners);
        throw new IllegalStateException(var10);
    }

    try {
        listeners.running(context);
        return context;
    } catch (Throwable var9) {
	this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
	throw new IllegalStateException(var9);
	}
}

進入 refreshContext() 重新整理應用上下文:

// SpringApplication.class

private void refreshContext(ConfigurableApplicationContext context) {
    this.refresh(context);// 進入refresh()
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
        } catch (AccessControlException var3) {
        }
    }
}

繼續進入 refresh() 方法:

// SpringApplication.class

protected void refresh(ApplicationContext applicationContext) {
    Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
    // 轉換為AbstractApplicationContext並呼叫重新整理。
    ((AbstractApplicationContext)applicationContext).refresh();// 進入refresh()
}

4. 進入AbstractApplicationContext

繼續進入 refresh() 方法:

// AbstractApplicationContext.class

public void refresh() throws BeansException, IllegalStateException {
    synchronized(this.startupShutdownMonitor) {
        this.prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        this.prepareBeanFactory(beanFactory);

        try {
            this.postProcessBeanFactory(beanFactory);
            this.invokeBeanFactoryPostProcessors(beanFactory);
            this.registerBeanPostProcessors(beanFactory);
            this.initMessageSource();
            this.initApplicationEventMulticaster();
            // 這裡暫時只關注onRefresh()方法
            this.onRefresh();// 進入onRefresh()
            this.registerListeners();
            this.finishBeanFactoryInitialization(beanFactory);
            this.finishRefresh();
        } catch (BeansException var9) {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
            }

            this.destroyBeans();
            this.cancelRefresh(var9);
            throw var9;
        } finally {
            this.resetCommonCaches();
        }

    }
}

進入 onRefresh() 方法:

// AbstractApplicationContext.class

protected void onRefresh() throws BeansException {// 進入其子類ServletWebServerApplicationContext.class重寫的onRefresh()
}

到這裡,我們看到抽象類 AbstractApplicationContextonRefresh() 方法被以下子類重寫:

  1. AbstractRefreshableWebApplicationContext
  2. GenericWebApplicationContext
  3. ReactiveWebServerApplicationContext
  4. ServletWebServerApplicationContext
  5. StaticWebApplicationContext

4. 揭曉

我們重點關注 ServletWebServerApplicationContext ,進入該實現類:

// ServletWebServerApplicationContext.class

protected void onRefresh() {
    super.onRefresh();
    try {
        // 建立web服務
        this.createWebServer();// 進入createWebServer()
    } catch (Throwable var2) {
        throw new ApplicationContextException("Unable to start web server", var2);
    }
}

根據名稱,我們知道,這裡即將進入一個與服務建立相關的方法,進入 createWebServer()

// ServletWebServerApplicationContext.class

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

    this.initPropertySources();
}

進入ServletWebServerFactory:

@FunctionalInterface
public interface ServletWebServerFactory {
    WebServer getWebServer(ServletContextInitializer... initializers);// 檢視getWebServer()的所有實現
}

至此,我們可以看到有三個類均實現了 ServletWebServerFactory 介面中的 getWebServer 方法,他們分別是:

JettyServletWebServerFactory.class (org.springframework.boot.web.embedded.jetty)

TomcatServletWebServerFactory.class (org.springframework.boot.web.embedded.tomcat)

UndertowServletWebServerFactory.class (org.springframework.boot.web.embedded.undertow)

5. 繼續探究

從這個方向繼續探究下去,我們還可以在原始碼中找到更多內容。

當然,從此前的任何一個分支探究下去,都同樣會使我們獲益匪淺。

按照這樣的方法,我們可以找到很多問題的答案,例如:

  • SpringBoot 如何載入 application.yml
  • 自定義的 MyServletInitializer 何時被載入?
  • 啟動類 main() 方法中的 args 有什麼用?
  • logback.xml 在什麼地方被載入?
  • 等等等……