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: xfc_exclave@163.com
* @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
在什么地方被加載?- 等等等……