本文以SpringBoot Web项目为例子分析(只引入web包)java
如题所示,本文主要划分两个部分进行介绍,SpringBoot的启动和SpringBoot的初始化。 相信你们第一次启动SpringBoot的时候都感到很是神奇,一个简单的java –jar xxx.jar命令就能把一个web应用启动了,甚至不用放到Tomcat容器里,这实在是使人叹服的优雅和简洁!react
究其本质,SpringBoot将应用打包成了一个fat jar包,而不是咱们常见的jar包。fat jar在启动时会作一系列隐藏复杂的准备工做,最终呈现为如此简单的启动命令。fat jar技术并非SpringBoot独创,但确实是SpringBoot将其发扬光大。下面咱们一块儿来了解一下这个启动过程。web
首先咱们来看一下SpringBoot fat jar的结构spring
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包含如下几个关键部分springboot
Main-Class说明了该fat jar的入口启动类JarLauncher,执行命令java –jar blockchain-0.0.1-SNAPSHOT.jar的时候JVM会找到JarLauncher并运行它的main方法,源码以下bash
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。app
launch方法less
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中的资源,也会对应一个url,并以'!/'分割,如
对于原始的JarFile URL,只支持一个'!/',SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源,如
在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应用的初始化十分简洁,只有一行,对应调用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 | 清理日志系统 |
初始化上文提到的SpringApplicationRunListener,而后发布ApplicationStartingEvent事件。
Environment在Spring的两个关键部分是profiles和properties,引伸出来的两个关键属性是propertySources(属性源,即环境变量、启动参数和配置文件等)和propertyResolver(属性解析器)。
propertySources SpringBoot根据applicationType(REACTIVE,SERVLET和NONE)建立Environment,在本例中是SERVLET,会建立StandardServletEnvironment,此时有4个PropertySources,分别是
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在收到该事件后,就会对配置文件进行解析工做。
此时配置文件已经解析完成,能够尽情享用了。SpringBoot将spring.main的属性绑定到SpringApplication,打印banner(默认寻找classpath下的banner.png/jpg/txt等),而后开始着手构建context。
Context的构建与environment相似,根据ApplicationType(本例是SERVLET)构建AnnotationConfigServletWebServerApplicationContext,这个类的继承关系很是复杂,我以为比较关键的几点是:
而后准备异常报告器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阶段结束。
这个阶段比较简单,主要往spring容器注册一些重要的类(此时Spring称其为source),其中最最最重要的就是SpringBoot的启动类了,称为PrimarySource。
SpringBoot支持在配置文件中指定附加的Source,但大多数状况下咱们只有一个启动类做为PrimarySource,在此阶段注册到spring容器,做为后续refreshContext的依据。 接下来发布ApplicationPreparedEvent事件,本阶段结束。
终于到了重头戏,本阶段调用了著名的AbstractApplicationContext.refresh()方法,大多数Spring的功能特性都在此处实现,但里面的逻辑又十分复杂,还夹杂着各类细枝末节,我也在抽空从新理清其主干脉络,限于篇幅,会在下一期的文章中着重介绍AbstractApplicationContext.refresh(),此处先行略过,目前咱们只要大概知道它完成了扫描bean,解析依赖关系,实例化单例对象等工做便可。
发布ApplicationStartedEvent事件,本阶段结束。
此时Spring自己已经启动完了,SpringBoot设计了ApplicationRunner接口供应用进行一些自定义初始化,都会在这阶段逐一调用。
发布ApplicationReadyEvent事件,本阶段结束。
若是在上述的阶段中抛出异常,就会进入Failed阶段,发布ApplicationFailedEvent事件通知其余listener,利用上文介绍的FailureAnalyzers报告失败缘由。
将上面过程用一张来简要归纳下
至此run方法结束,那么SpringBoot应用main方法也会跟着结束了,main线程退出。对于普通应用,因为没有其余守护线程,JVM会立刻关闭。对于web应用,Tomcat会启动一条守护线程,JVM依然保持工做,待Tomcat收到shutdown指令关闭守护线程后,JVM才会关闭。
关于Spring refreshContext和Tomcat的内容,我将在下期进行介绍,下期再见!