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"> * @Configuration * @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應用程式,預設情況下,將執行下面幾個步驟來引導我們的應用程式:
- 建立一個恰當的ApplicationContext例項(取決於類路徑)
- 註冊CommandLinePropertySource,將命令列引數公開為Spring屬性。
- 重新整理應用程式上下文,載入所有單例bean。
- 觸發全部CommandLineRunner bean。
大多數情況下,靜態 run()
方法可以在我們的啟動類的 main()
方法中呼叫。
SpringApplication可以從各種不同的源讀取bean。 通常建議使用單個@Configuration類來引導,但是我們也可以通過以下方式來設定資源:
- 通過AnnotatedBeanDefinitionReader載入完全限定類名。
- 通過XmlBeanDefinitionReader載入XML資源位置,或者是通過GroovyBeanDefinitionReader載入groovy指令碼位置。
- 通過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()
}
到這裡,我們看到抽象類 AbstractApplicationContext
的 onRefresh()
方法被以下子類重寫:
- AbstractRefreshableWebApplicationContext
- GenericWebApplicationContext
- ReactiveWebServerApplicationContext
- ServletWebServerApplicationContext
- 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
在什麼地方被載入?- 等等等……