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
在什么地方被加载?- 等等等……