本文以SpringBoot Web项目为例子分析(只引入web包)
如题所示,本文主要划分两个部分进行介绍,SpringBoot的启动和SpringBoot的初始化。 相信大家第一次启动SpringBoot的时候都感到非常神奇,一个简单的java –jar xxx.jar命令就能把一个web应用启动了,甚至不用放到Tomcat容器里,这实在是令人叹服的优雅和简洁!
究其本质,SpringBoot将应用打包成了一个fat jar包,而不是我们常见的jar包。fat jar在启动时会做一系列隐藏复杂的准备工作,最终呈现为如此简单的启动命令。fat jar技术并不是SpringBoot首创,但确实是SpringBoot将其发扬光大。下面我们一起来了解一下这个启动过程。
SpringBoot的启动
首先我们来看一下SpringBoot fat jar的结构
blockchain-0.0.1-SNAPSHOT.jar
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── BlockchinaApplication.class
│ │ └── 应用程序
│ └── lib
│ └── spring-core.jar
│ └── 第三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── JarLauncher
└── WarLauncher
└── springboot启动程序
每个jar包都存在一个META-INF/ MANIFEST.MF文件,可粗略理解为jar包的配置文件。
一个典型的SpringBoot fat jar包含以下几个关键部分
- Spring-Boot-Version: 2.1.4.RELEASE
- Main-Class: org.springframework.boot.loader.JarLauncher
- Start-Class: org.ypq.its.blockchain.BlockchainApplication
- Spring-Boot-Classes: BOOT-INF/classes/
- Spring-Boot-Lib: BOOT-INF/lib/
- Created-By: Apache Maven 3.3.9
- Build-Jdk: 1.8.0_45
Main-Class说明了该fat jar的入口启动类JarLauncher,执行命令java –jar blockchain-0.0.1-SNAPSHOT.jar的时候JVM会找到JarLauncher并运行它的main方法,源码如下
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
new JarLauncher会调用父类ExecutableArchiveLauncher的无参构造方法
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
archive是SpringBoot对归档文件的一个抽象,对于jar包是JarFileArchive,对于文件目录是ExplodedArchive。
createArchive方法会找到当前类所在的路径,构造一个Archive。
launch方法
protected void launch(String[] args) throws Exception {
// 注册Handler
JarFile.registerUrlProtocolHandler();
// 找出fat jar里包含的所有archive,将其所有URL找出来构建LaunchedURLClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 将LaunchedURLClassLoader设置到线程上下文,调起我们应用的main方法
launch(args, getMainClass(), classLoader);
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
// 可以看到SpringBoot应用使用的不是APPClassLoader,而是自定义的LaunchedURLClassLoader
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
// 设置线程的ContextLoader
Thread.currentThread().setContextClassLoader(classLoader);
// 调起应用main方法
createMainMethodRunner(mainClass, args, classLoader).run();
}
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 找出MANIFEST.MF的Start-Class属性,作为入口启动类
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
public void run() throws Exception {
// 使用LaunchedURLClassLoader加载应用启动类
Class<?> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
// 反射找出main方法并调用
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
每个jar都会对应一个url,如
- jar:file:/blockchain-0.0.1-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/
jar中的资源,也会对应一个url,并以'!/'分割,如
- jar:file:/ blockchain-0.0.1-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class
对于原始的JarFile URL,只支持一个'!/',SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源,如
- jar:file:/ blockchain-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class
在JarFile.registerUrlProtocolHandler()方法里,SpringBoot将org.springframework.boot.loader.jar. Handler注册,该Handler继承了URLStreamHandler,支持多个jar的嵌套(即jar in jar),是SpringBoot fat jar加载内部jar资源的基础。
public class Handler extends URLStreamHandler {
}
接下来扫描所有嵌套的jar,构建自定义的LaunchedURLClassLoader,设置到线程上下文,然后找出应用的启动类,调用main方法。因此到我们应用的main方法之前,SpringBoot已经帮我们配置好LaunchedURLClassLoader,并且具有加载BOOT-INF/class(应用本身的类)和BOOT-INF/lib(第三方依赖类)下面的所有类的能力,以上过程用一个图简要概括一下。
如果我们用IDE(Intellij IDEA或者eclipse)来启动SpringBoot应用,由于依赖的jar都已经放到classpath中,故不存在以上过程。本地调试与服务器运行的场景还是有少许差异。
接下来就到SpringBoot应用的初始化
SpringBoot应用的初始化十分简洁,只有一行,对应调用SpringApplication.run静态方法。跟踪查看该静态方法,主要完成两个操作,一是创建SpringApplication对象,二是调用该对象的run方法。这两个操作看似简单,实际上包含了大量复杂的初始化操作,下面我们就一起来一探究竟。
public static void main(String[] args) {
SpringApplication.run(BlockchainApplication.class, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
我们先看一下SpringApplication的构造方法:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 解析applicationType
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 设置Initializers
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
// 设置Listeners
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
主要包括三个比较重要的地方 deduceFromClasspath会根据classpath特定类是否存在来决定applicationType,总共有三种类型,分别是REACTIVE,SERVLET和NONE。 REACTIVE是响应式web,如果包含
org.springframework.web.reactive.DispatcherHandler
就会认为是响应式类型 NONE是普通应用程序,如果不包含
javax.servlet.Servlet
org.springframework.web.context.ConfigurableWebApplicationContext
就认为是普通应用程序
其余情况就是SERVLET,也是我们最常用的类型
接下来是设置initializer和listener,参数中都调用了getSpringFactoriesInstances,这是SpringBoot一种新的拓展机制,它会扫描classpath下所有包中的META-INF/spring.factories,将特定的类实例化(使用无参构造方法)。 一个典型spring-boot-starter.jar的spring.factories包含以下内容,initializer有4个,listener有9个。
实际上,算上其他依赖包,initializer应该是有6个,listener有10个。所以SpringApplication有6个实例化后的initializer,10个实例化后的listener。 到此为止SpringApplication的构造方法结束。
接下来就是run方法了,以下源码在关键地方进行了一些简单的注释
public ConfigurableApplicationContext run(String... args) {
// 开启定时器,统计启动时间
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 获取并初始化所有RunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
// 发布启动事件
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 准备好环境environment,即配置文件等
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
// 打印SpringBoot Logo
Banner printedBanner = printBanner(environment);
// 创建我们最常用的ApplicationContext
context = createApplicationContext();
// 获取异常报告器,在启动发生异常的时候用友好的方式提示用户
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 准备Context,加载启动类作为source
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
// Spring初始化的核心逻辑,构建整个容器
refreshContext(context);
afterRefresh(context, applicationArguments);
// 停止计时,统计启动耗时
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
// 调用runner接口供应用自定义初始化
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 处理启动中抛出的异常,使用异常报告器输出
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
我们通过注释大概了解了一下run方法,先不急着往下分析,我们来看下SpringApplicationRunListener,从RunListener这个名字看出它是run方法的listener,监听事件覆盖了启动过程的生命周期,从它下手再好不过了。总共有7个状态如下所示:
将其整理成表格
SpringApplicationRunListener
| 顺序 | 方法名 | 说明 |
|---|---|---|
| 1 | starting Run | 方法调用时马上执行,最早执行,因此可以做一些很早期的工作,这个方法没有参数,能做的事情也非常有限 |
| 2 | environmentPrepared | 当environment准备好后执行,此时ApplicationContext尚未创建 |
| 3 | contextPrepared | 当ApplicationContext准备好后执行,此时尚未加载source |
| 4 | contextLoaded | 加载source后调用,此时尚未refresh |
| 5 | started | RefreshContext后执行,说明应用已经基本启动完毕,尚未调用ApplicationRunner等初始化 |
| 6 | running | 调用ApplicationRunner后执行,已经进入应用的就绪状态 |
| 7 | failed | 启动过程中出现异常时执行 |
而EventPublishingRunListener是唯一一个Runlistener,将上面不同时间点包装成一个个事件传播出去,对应关系如下
SpringApplicationEvent
| 顺序 | 方法名 | 对应事件 |
|---|---|---|
| 1 | starting | ApplicationStartingEvent |
| 2 | environmentPrepared | ApplicationEnvironmentPreparedEvent |
| 3 | contextPrepared | ApplicationContextInitializedEvent |
| 4 | contextLoaded | ApplicationPreparedEvent |
| 5 | started | ApplicationStartedEvent |
| 6 | running | ApplicationReadyEvent |
| 7 | failed | ApplicationFailedEvent |
上面提到的各个事件都是指SpringBoot里新定义的事件,与原来Spring的事件不同(起码名字不同)
EventPublishingRunListener在初始化的时候会读取SpringApplication里面的10个listener(上文已经提到过),每当有对应的事件就会通知这10个listener,其中ConfigFileApplicationListener和LoggingApplicationListener与我们的开发密切相关,简单介绍如下,有机会再仔细研究。
ConfigFileApplicationListener
| 响应事件 | 实现功能 |
|---|---|
| ApplicationEnvironmentPreparedEvent | 查找配置文件,并对其进行解析 |
| ApplicationPreparedEvent | 对defaultProperties的配置文件进行排序,基本没用到 |
LoggingApplicationListener
| 响应事件 | 实现功能 |
|---|---|
| ApplicationStartingEvent | 按照logback、log4j、javaLogging的优先顺序确定日志系统,并预初始化 |
| ApplicationEnvironmentPreparedEvent | 对日志系统进行初始化,此后就可以使用日志系统了 |
| ApplicationPreparedEvent | 将日志系统注册到spring容器中 |
| ContextClosedEvent | 清理日志系统 |
| ApplicationFailedEvent | 清理日志系统 |
Starting阶段
初始化上文提到的SpringApplicationRunListener,然后发布ApplicationStartingEvent事件。
environmentPrepared阶段
Environment在Spring的两个关键部分是profiles和properties,引申出来的两个关键属性是propertySources(属性源,即环境变量、启动参数和配置文件等)和propertyResolver(属性解析器)。
propertySources SpringBoot根据applicationType(REACTIVE,SERVLET和NONE)创建Environment,在本例中是SERVLET,会创建StandardServletEnvironment,此时有4个PropertySources,分别是
- servletConfigInit
- servletContextInit
- systemProperties(user.dir)
- systemEnviroment(环境变量)
propertyResolver 接下来就是配置propertyResolver,它有一个很重要的属性是ConversionService,默认包含了各种各样的转换器,共132个,根据代码直观感受一下,光是scalar数量相关的就几十个了。。。
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
最后就是获取profile,并且发布ApplicationEnvironmentPreparedEvent事件。上文提到的ConfigFileApplicationListener在收到该事件后,就会对配置文件进行解析工作。
contextPrepared阶段
此时配置文件已经解析完成,可以尽情享用了。SpringBoot将spring.main的属性绑定到SpringApplication,打印banner(默认寻找classpath下的banner.png/jpg/txt等),然后开始着手构建context。
Context的构建与environment类似,根据ApplicationType(本例是SERVLET)构建AnnotationConfigServletWebServerApplicationContext,这个类的继承关系非常复杂,我觉得比较关键的几点是:
- 拥有beanFactory属性,在父类GenericApplicationContext里初始化为DefaultListableBeanFactory,这也是我们后面会经常用到的beanFactory实现类
- 拥有reader属性,实现类是AnnotatedBeanDefinitionReader,主要用于编程式注册的bean。
- 拥有scanner属性,实现类是ClassPathBeanDefinitionScanner,用于寻找Classpath上的候选bean,默认包括被@Component, @Repository,@Service和 @Controller 注解的bean。
然后准备异常报告器exceptionReporters,它也以getSpringFactoriesInstances的方式获取内置的FailureAnalyzers,FailureAnalyzers以同样的方式从获取FailureAnalyzer,默认情况下总共有17个。
class ConnectorStartFailureAnalyzer
extends AbstractFailureAnalyzer<ConnectorStartFailedException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure,
ConnectorStartFailedException cause) {
return new FailureAnalysis(
"The Tomcat connector configured to listen on port " + cause.getPort()
+ " failed to start. The port may already be in use or the"
+ " connector may be misconfigured.",
"Verify the connector's configuration, identify and stop any process "
+ "that's listening on port " + cause.getPort()
+ ", or configure this application to listen on another port.",
cause);
}
}
值得一提的是,所有FailureAnalyzer都继承了AbstractFailureAnalyzer
public abstract class AbstractFailureAnalyzer<T extends Throwable>
implements FailureAnalyzer {
@Override
public FailureAnalysis analyze(Throwable failure) {
T cause = findCause(failure, getCauseType());
if (cause != null) {
return analyze(failure, cause);
}
return null;
}
}
SpringBoot在根据泛型寻找合适的FailureAnalyzer时,使用了Spring提供的ResolvableType类。该类广泛应用于Spring的源码中,是Spring设计的基础。
@Override
public FailureAnalysis analyze(Throwable failure) {
T cause = findCause(failure, getCauseType());
if (cause != null) {
return analyze(failure, cause);
}
return null;
}
// 找出当前类的泛型
protected Class<? extends T> getCauseType() {
return (Class<? extends T>) ResolvableType
.forClass(AbstractFailureAnalyzer.class, getClass()).resolveGeneric();
}
// 判断抛出的异常是否当前类泛型的一个实例
protected final <E extends Throwable> E findCause(Throwable failure, Class<E> type) {
while (failure != null) {
if (type.isInstance(failure)) {
return (E) failure;
}
failure = failure.getCause();
}
return null;
}
个人认为上述设计针对一个FailureAnalyzer对应处理一种Exception的场景十分适合,而ApplicationListener对多种Event进行监听的场景更适合使用supportsEventType模式。
扯远了,再次回到我们的contextPrepared阶段,最后一步是调用上文提到的6个initializer,它们都继承了ApplicationContextInitializer
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C applicationContext);
}
与上文的FailureAnalyzer类似,SpringBoot根据不同的ApplicationContext寻找适合的ApplicationContextInitializer进行调用,所以说这种设计思路在Spring应用十分广泛。
其中一个initializer是ConditionEvaluationReportLoggingListener,它会在启动成功或失败后打印SpringBoot自动配置(AutoConfiguration)的Condition匹配信息,对于AutoConfiguration的调试十分有用。
最后发布ApplicationContextInitializedEvent事件,至此contextPrepared阶段结束。
contextLoaded阶段
这个阶段比较简单,主要往spring容器注册一些重要的类(此时Spring称其为source),其中最最最重要的就是SpringBoot的启动类了,称为PrimarySource。
SpringBoot支持在配置文件中指定附加的Source,但大多数情况下我们只有一个启动类作为PrimarySource,在此阶段注册到spring容器,作为后续refreshContext的依据。 接下来发布ApplicationPreparedEvent事件,本阶段结束。
Started阶段
终于到了重头戏,本阶段调用了著名的AbstractApplicationContext.refresh()方法,大多数Spring的功能特性都在此处实现,但里面的逻辑又十分复杂,还夹杂着各种细枝末节,我也在抽空重新理清其主干脉络,限于篇幅,会在下一期的文章中着重介绍AbstractApplicationContext.refresh(),此处先行略过,目前我们只要大概知道它完成了扫描bean,解析依赖关系,实例化单例对象等工作即可。
发布ApplicationStartedEvent事件,本阶段结束。
Running阶段
此时Spring本身已经启动完了,SpringBoot设计了ApplicationRunner接口供应用进行一些自定义初始化,都会在这阶段逐一调用。
发布ApplicationReadyEvent事件,本阶段结束。
Failed阶段
如果在上述的阶段中抛出异常,就会进入Failed阶段,发布ApplicationFailedEvent事件通知其他listener,利用上文介绍的FailureAnalyzers报告失败原因。
小结
将上面过程用一张来简要概括下
至此run方法结束,那么SpringBoot应用main方法也会跟着结束了,main线程退出。对于普通应用,由于没有其他守护线程,JVM会马上关闭。对于web应用,Tomcat会启动一条守护线程,JVM依然保持工作,待Tomcat收到shutdown指令关闭守护线程后,JVM才会关闭。
关于Spring refreshContext和Tomcat的内容,我将在下期进行介绍,下期再见!