SpringBoot 的启动
SpringBoot 的启动过程是比较复杂的,涉及了非常多的组件,这些组件有些是用于定制启动过程,有些是定制 Spring 容器的配置,当这些东西都混杂到一起时,理解 SpringBoot 的启动就是一项非常考验脑力的活动了,我曾经多次研究多 SpringBoot 的启动过程,不过没有记录笔记,有些过程在看的时候是理解的,不过随着时间的流逝,再拾起时又有一些模糊的地方。下面我将和大家一起探究 SpringBoot 的启动过程,并整理成这系列笔记,方便自己和大家理解。
为什么要探究 SpringBoot 的启动过程
是啊,为什么要探究 SpringBoot 的启动过程呢?把它当成一个魔法黑盒,不也可以使用的很好吗?确实如此,即使不去探究 SpringBoot 的启动,也可以将 SpringBoot 使用的非常熟练,SpringBoot 官方提供了非常详尽的操作手册以指导用户正确的使用。不过我个人不太喜欢这种魔法式的效果,我希望打开 SpringBoot 的盒子,看下它是如何工作的,想了解它的工作原理,更进一步的想理解它是为何如此设计。
SpringBoot 的示例代码
示例项目的代码非常简单
-
pom 主要配置
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.2</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> -
启动类
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
SpringBoot 构造函数
代码 SpringApplication.run(DemoApplication.class, args)的最终调用方法是下面
public class SpringApplication {
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
}
这个方法中,它首先利用传入的DemoApplication.class作为 primarySources 参数创建了一个 SpringApplication 对象,然后调用了 run 方法,参数是 main 方法的 args。所以我们首先看下 SpringBoot 的构造函数,它在构造函数中初始化了哪些内容。
SpringBoot 提供了两个构造函数,第一个构造函数 SpringApplication(Class... primarySources),只接受一组Class作为参数,并在内部调用了第二个构造函数 SpringApplication(ResourceLoader resourceLoader, Class... primarySources).
第二个构造函数有两个参数 ResourceLoader ,和 Class 数组,我把每行的作用注释在代码中,方便大家阅读。
public class SpringApplication {
//资源加载器
private ResourceLoader resourceLoader;
//主配置类数组
private final Set<Class<?>> primarySources;
//当前应用类型(枚举 NONE、SERVLET和REACTIVE)
private WebApplicationType webApplicationType;
//BootstrapRegistry初始化回调接口数组(SpringBoot独有的)
private final List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;
//ApplicationContext初始化回调接口数组(Spring有的)
private List<ApplicationContextInitializer<?>> initializers;
//应用事件监听器
private List<ApplicationListener<?>> listeners;
//应用主类
private Class<?> mainApplicationClass;
//构造函数一
//参数 primarySources 主配置类数组
public SpringApplication(Class<?>... primarySources) {
//内部调用构造函数二,primarySources是直接透传,而resourceLoader的参数为null
this(null, primarySources);
}
//构造函数二
//参数 resourceLoader 资源加载器
//参数 primarySources 主配置类数组
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 1. 设置资源加载器(为null也不要紧,后面有默认值)
this.resourceLoader = resourceLoader;
// 2. 校验并设置primarySources ,不能为null
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 3. 推测当前应用的类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 4. 从spring.factories中加载BootstrapRegistryInitializer
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
//5. 从spring.factories中加载ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//6. 从spring.factories中加载ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//7. 从调用堆栈中找出应用主类
this.mainApplicationClass = deduceMainApplicationClass();
}
}
关于构造函数有几点需要额外介绍的
- 应用类型推断
- 从 spring.factories 加载配置
- 应用主类推断
应用类型推断
应用类型
SpringBoot 使用类 WebApplicationType 定义了三种应用类型
- NONE
- SERVLET
- REACTIVE
分别表示了 普通应用、Servlet Web 应用、和响应式 Web 应用三种类型。这些应用类型分别适用于不同的业务场景。不需要提供 web 访问的应用是普通应用,传统的 JavaServlet 标准的 Servlet Web 应用,和 Spring 基于 reactor 研发的响应式 web 应用。
这三种类型在技术上实际是决定了 SpringBoot 将要使用什么样类型的 ApplicationContext。具体的代码我们在 run 方法中仔细探究,这里先介绍一下
| 类型 | 常规 | Native |
|---|---|---|
| NONE | AnnotationConfigApplicationContext | GenericApplicationContext |
| SERVLET | AnnotationConfigServletWebServerApplicationContext | ServletWebServerApplicationContext |
| REACTIVE | AnnotationConfigReactiveWebServerApplicationContext | ReactiveWebServerApplicationContext |
类型推断方法
类型推断方法是委托给类 WebApplicationType,大致是下面三步
- 只有 DispatcherHandler 时,为 Reactive 类型
- 没有 Servlet 或 ConfigurableWebApplicationContext 时,为 None 类型
- 其余都是 Servlet 类型
这里需要注意的一点是,即使 ClassPath 中同时有 Servlet 和 Reactive 的类,还是会推断成 Servlet 类型。
public enum WebApplicationType {
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext" }
//从ClassPath中推断应用类型
static WebApplicationType deduceFromClasspath() {
//1 只有DispatcherHandler 时,为Reactive类型
// 这里可以看出 如果Servlet和Reactive的类都在时,实际类型推断出来是Servlet类型。
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
//2 没有 Servlet 或 ConfigurableWebApplicationContext时,为None类型
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
//3 其余都是Servlet类型
return WebApplicationType.SERVLET;
}
}
从 spring.factories 加载配置
SpringBoot 中很多配置都是从 spring.factories 中加载的。spring.factories 本质是一个 Java properties 文件,里面定义很多的 KV 映射。key 一般是接口的全限定名,而值则是 key 的实现列表(使用逗号分隔)。其实这里和 Java 的 SPI 机制非常类似,不过 SPI 会将接口全限定名作为文件名,然后实现类的名字直接写在文件内部,位置一般是 META-INF/services/接口全限定名。而 spring.factories 文件位置是META-INF/spring.factories。
spring-boot 和 spring-boot-autocofiigure 中都有这个文件。我截取 springboot 的 spring.factories 一部分内容:
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
...
springboot 使用类 SpringFactoriesLoader 来负责从 spring.factories 中读取 KV,并实例化成对象。
public class SpringFactoriesLoader {
// 默认资源位置
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
//缓存
//结构为
//{
// ClassLoaderA: {
// loacation1: SpringFactoriesLoader
// loacation2: SpringFactoriesLoader
// },
// ClassLoaderB: {
// loacation3: SpringFactoriesLoader
// }
//}
static final Map<ClassLoader, Map<String, SpringFactoriesLoader>> cache = new ConcurrentReferenceHashMap<>();
//SpringFactoriesLoader的创建
public static SpringFactoriesLoader forResourceLocation(String resourceLocation, @Nullable ClassLoader classLoader) {
//1 确定类加载器
ClassLoader resourceClassLoader = (classLoader != null ? classLoader : SpringFactoriesLoader.class.getClassLoader());
//2 从缓存中读取,没有就创建
Map<String, SpringFactoriesLoader> loaders = cache.computeIfAbsent(resourceClassLoader, key -> new ConcurrentReferenceHashMap<>());
//3 从缓存中读取,没有就创建SpringFactoriesLoader 并加载资源
return loaders.computeIfAbsent(resourceLocation, key ->
new SpringFactoriesLoader(classLoader, loadFactoriesResource(resourceClassLoader, resourceLocation)));
}
//从resourceLocation中加载资源
protected static Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
Map<String, List<String>> result = new LinkedHashMap<>();
try {
// 从resourceLocation读取所有的文件,然后依次读取并按照properties类型解析,并且按照分隔符切割成List
Enumeration<URL> urls = classLoader.getResources(resourceLocation);
while (urls.hasMoreElements()) {
UrlResource resource = new UrlResource(urls.nextElement());
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
properties.forEach((name, value) -> {
List<String> implementations = result.computeIfAbsent(((String) name).trim(), key -> new ArrayList<>());
Arrays.stream(StringUtils.commaDelimitedListToStringArray((String) value))
.map(String::trim).forEach(implementations::add);
});
}
// 某个接口对应的List去重
result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
}
return Collections.unmodifiableMap(result);
}
//SpringFactoriesLoader加载指定的接口实现
// factoryType 就是接口类型
public <T> List<T> load(Class<T> factoryType, @Nullable ArgumentResolver argumentResolver,
@Nullable FailureHandler failureHandler) {
//从缓存中读取实现列表
List<String> implementationNames = loadFactoryNames(factoryType);
List<T> result = new ArrayList<>(implementationNames.size());
FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_FAILURE_HANDLER;
//依次实例化
for (String implementationName : implementationNames) {
T factory = instantiateFactory(implementationName, factoryType, argumentResolver, failureHandlerToUse);
if (factory != null) {
result.add(factory);
}
}
//按照order排序
AnnotationAwareOrderComparator.sort(result);
return result;
}
}
应用主类推断
这个应用主类是 main 方法的所在类,只能在单线程环境和启动过程中才能正确推断。在 Java8 及之前,可以通过创建一个 Throwable 对象,并通过 Throwable 的 getStackTrace()方法可以获取调用的栈结构,从而在遍历整个栈找到 main 方法的所在类。Java9 中加入了类 StackWalker,可以通过该类来边界的遍历整个调用栈。比如下面的示例代码:
public class StackWalkerDemo {
public static void main(String[] args) {
new StackWalkerDemo().start();
}
private void start() {
init();
}
private void init() {
//在这里推断
StackWalker walker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
List<String> walk = walker.walk(stream -> {
return stream.map(frame -> {
return frame.getClassName() + "->" + frame.getMethodName();
}).collect(Collectors.toList());
});
System.out.println(walk);
//[support.StackWalkerDemo->init, support.StackWalkerDemo->start, support.StackWalkerDemo->main]
}
}
上面的示例代码中,我们在 init 方法中查看调用的栈结构,把类名和方法名收集到 List 中,并最终打印整个 List。结果中显示了我们 init 的栈顺序。
springboot 中也是利用了 StackWalker,来查找 main 方法所在的类,代码如下
public class SpringApplication {
//推断主类
private Class<?> deduceMainApplicationClass() {
return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk(this::findMainClass)
.orElse(null);
}
//找主类的方法
private Optional<Class<?>> findMainClass(Stream<StackFrame> stack) {
//过滤,找出第一个方法名字是main的类
return stack.filter((frame) -> Objects.equals(frame.getMethodName(), "main")).findFirst()
.map(StackWalker.StackFrame::getDeclaringClass);
}
}
run 方法
run 方法就是启动 springboot 应用的方法。这里面有很多的步骤,示意图如下
run 方法的启动步骤的数量可能和其他博主的有些出入,不过整体基本是一致的,我在示意图中省略了一些不紧要的步骤,并且仅记录了 run 方法内部的代码步骤,没有记录 run 方法调用的方法内的步骤,比如准备环境时,实际上还会发送环境准备完毕事件,这部分我没有记录,我会在下面单独阐述 SpringBoot 的启动过程监听器和事件。
springboot 的 run 方法的代码如下,代码上都有注释
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
//1 计时开始!
long startTime = System.nanoTime();
//2 创建引导上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
//配置headless属性(可以忽略)
configureHeadlessProperty();
//3 准备好所有的SpringBoot应用启动过程事件监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
//4 发送开始启动事件
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 5 准备环境
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
//6 打印应用标语
Banner printedBanner = printBanner(environment);
//7 创建应用上下文
context = createApplicationContext();
//设置启动步骤记录器(可忽略
context.setApplicationStartup(this.applicationStartup);
//8 配置应用上下文
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//9 刷新应用上下文
refreshContext(context);
//10 刷新后操作(springboot预留方法,可忽略
afterRefresh(context, applicationArguments);
//11 记录整体耗时
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
//12 发送应用已启动事件
listeners.started(context, timeTakenToStartup);
//13 执行所有的ApplicationRunner 和 CommandLineRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 失败处理
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
//14 发送应用就绪事件
if (context.isRunning()) {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
}
catch (Throwable ex) {
// 失败处理
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
}
看了上面复杂的启动步骤,我们来看下具体每个步骤做了什么事情。
创建引导上下文
springboot 会创建一个引导上下文,用于存储、共享启动过程中需要的各个组件。不是 ApplicationContext,而是 BootstrapContext,具体的实现类型是 DefaultBootstrapContext。
下面是创建的步骤:
- 创建一个类型为 DefaultBootstrapContext 的引导上下文
- 依次调用所有的 引导上下文初始化器的回调方法
public class SpringApplication {
private final List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;
private DefaultBootstrapContext createBootstrapContext() {
//1 创建了一个默认引导上下文
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
//2 调用所有的BootstrapRegistryInitializer来初始化这个引导上下文
// 这里的 BootstrapRegistryInitializer 是在构造函数中设置的
this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
return bootstrapContext;
}
}
那么 springboot 中默认有多少个 BootstrapRegistryInitializer 呢?答案是 0 个!没错 SpringBoot 和 SpringBootAutoConfigure 中默认没有配置任何的 BootstrapRegistryInitializer,不过在 SpringCloud 中配置了很多,这部分我们后面有机会探究。
准备所有的启动过程事件监听器
springboot 在启动的过程中也有一套事件发布和监听的机制,这部分相比于 Spring 的事件机制来说简单了不少,因为这部分的事件主要是 SpringBoot 启动过程中产生和消费的,与应用本身无关。
public class SpringApplication {
//SpringBoot 3.0 新增的,可以通过ThreadLocal来配置SpringBoot
//SpringApplicationHook 目前可以只能添加SpringApplicationRunListener
private static final ThreadLocal<SpringApplicationHook> applicationHook = new ThreadLocal<>();
private SpringApplicationRunListeners getRunListeners(String[] args) {
//1 解析启动的命令行参数
ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
argumentResolver = argumentResolver.and(String[].class, args);
//2 从spring.factories中加载所有的 SpringApplicationRunListener
List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class, argumentResolver);
//3 把应用Hook中的监听器加入进来(3.0.0新增)
SpringApplicationHook hook = applicationHook.get();
SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null;
if (hookListener != null) {
listeners = new ArrayList<>(listeners);
listeners.add(hookListener);
}
//4 把所有的监听器包装到一起
return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup);
}
}
启动过程中的事件
SpringBoot 的启动过程的事件下面 7 种:
- starting
- environmentPrepared
- contextPrepared
- contextLoaded
- started
- ready
- failed
发生的时机见图:
发送开始启动事件
发送 starting 事件的代码只有一行,如下
listeners.starting(bootstrapContext, this.mainApplicationClass);
我们稍稍深入研究下 springboot 启动过程中是如何发布事件。上面谈到过,springboot 会收集所有的 SpringApplicationRunListener,然后统一放置于 SpringApplicationRunListeners 中,然后在 SpringApplicationRunListeners 的内部进行统一的事件发布。下面就是 SpringApplicationRunListeners 的 starting 代码,我们看下它是如何调用的,其他类型的方法也是类似的,理解了 starting 的调用,也就理解了整个 springboot 启动过程中的事件的调用。
class SpringApplicationRunListeners {
void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
// 直接调用内部函数 doWithListeners
//
doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
(step) -> {
if (mainApplicationClass != null) {
step.tag("mainApplicationClass", mainApplicationClass.getName());
}
});
}
//调用所有的监听器,并触发对应的步骤记录
// stepName 当前SpringApplicaiton进行的步骤
// listenerAction 当前所有监听器需要进行的操作函数
// stepAction 步骤记录器需要进行的操作
private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction, Consumer<StartupStep> stepAction) {
StartupStep step = this.applicationStartup.start(stepName);
//调用所有SpringApplicationRunListener的某个方法
this.listeners.forEach(listenerAction);
if (stepAction != null) {
stepAction.accept(step);
}
step.end();
}
}
- 首先可以看到 SpringApplicationRunListeners 并不是 public 修饰的,所以这个类仅仅用于 springboot 内部,普通用户不需要使用。
- 在 starting 方法内部调用了 doWithListeners 方法,传入了"spring.boot.application.starting",这表示当前步骤的名字,后面是两个 Lambda 函数,第一个是监听器函数,第二个是启动步骤函数
- 监听器函数
(listener) -> listener.starting(bootstrapContext)在 doWithListeners 中被this.listeners.forEach(listenerAction);调用,这意味着调用了所有的 SpringApplicationRunListener 的 starting 方法 - 启动步骤函数的调用是在
doWithListeners的stepAction.accept(step);处被调用,关于启动步骤,此文不在进行详细讲解。
这里我们需要扩展一下,springboot 项目和 autoconfigure 项目默认有多少个 SpringApplicationRunListener,他们的每一步的作用又是什么? 首先看下在 spring.factories 中配置了多少 SpringApplicationRunListener
springboot 中
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
autoconfigure 默认没有监听器。
那我们看下 springboot 的 EventPublishingRunListener 的作用吧。其实从名字就可以看出来这个类的作用大概是发布和广播事件, 它是用来把 springboot 启动的事件转换并广播到上下文中的监听器中,是连接 springboot 事件和 spring 事件的桥梁。
我们看下它的构造函数和 starting 方法,它广播了一个 ApplicationStartingEvent 事件
class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
private final SimpleApplicationEventMulticaster initialMulticaster;
//构造函数
EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
//创建一个事件广播器
this.initialMulticaster = new SimpleApplicationEventMulticaster();
}
//
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
multicastInitialEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
}
//广播事件
private void multicastInitialEvent(ApplicationEvent event) {
//首先是找出当前时间点所有的 ApplicationListener
refreshApplicationListeners();
//然后进行事件的广播
this.initialMulticaster.multicastEvent(event);
}
//
private void refreshApplicationListeners() {
this.application.getListeners().forEach(this.initialMulticaster::addApplicationListener);
}
}
可以看出 springnboot 的启动过程中的各个事件都会被广播到 Spring 中,我总结下如下的 springboot 事件和 spring 事件的对应关系。
| springboot 事件 | spring 事件 | 备注 |
|---|---|---|
| starting | ApplicationStartingEvent | |
| environmentPrepared | ApplicationEnvironmentPreparedEvent | |
| contextPrepared | ApplicationContextInitializedEvent | |
| contextLoaded | ApplicationPreparedEvent | |
| started | ApplicationStartedEvent | 还会有事件 AvailabilityChangeEvent |
| ready | ApplicationReadyEvent | 还会有事件 AvailabilityChangeEvent |
| failed | ApplicationFailedEvent |
上表中的各个 spring 事件,并不是 spring 项目原始的事件,都是 springboot 基于 spring 而扩展的事件类型。
应用存活与就绪状态 在微服务的架构中,我们需要知道每个服务的状态,比如这个服务还有没有,是不是可以访问。springboot 基于此类需求,设计了应用的存活状态和就绪状态。使用枚举类 LivenessState 和 ReadinessState 来表示。
- LivenessState
- 应用的存活状态,有 CORRECT 和 BROKEN 两种,分别表示应用正常和失败
- ReadinessState
- 应用的可用性状态,有 ACCEPTING_TRAFFIC 和 REFUSING_TRAFFIC 两种,分别表示应用可以提供服务和不能提供服务
springboot 在 started 和 ready 事件中,也会修改上述的存活和就绪状态。在 started 之后应用进入了 LivenessState.CORRECT 状态,在 ready 之后应用进入了 ReadinessState.ACCEPTING_TRAFFIC 状态。
相关代码如下:
class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, timeTaken));
//发布应用存活状态变更事件
//AvailabilityChangeEvent(state=LivenessState.CORRECT)
AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, timeTaken));
//发布应用就绪状态变更事件
//AvailabilityChangeEvent(state=ReadinessState.ACCEPTING_TRAFFIC)
AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}
}
准备环境
springboot 会把创建一个环境,并设置到后面的上下文中。
springboot 的环境是基于 spring 的 ConfigurableEnvironment,但是增加了很多的扩展点。主要步骤如下:
- 查找或创建一个环境
- 配置环境,设置类型转换服务、加入其他源
- 发送环境准备好事件
- 设置当前 SpringApplication 的一些属性值
- 如果环境类型和当前应用类型不匹配还需要进行转换
public class SpringApplication {
private boolean isCustomEnvironment = false;
//准备环境
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
//1 查找或创建一个环境
ConfigurableEnvironment environment = getOrCreateEnvironment();
//2 配置环境
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 转换propertySource
ConfigurationPropertySources.attach(environment);
//3 发送环境准备好事件
listeners.environmentPrepared(bootstrapContext, environment);
// 把defaultProperties移动到最后一位
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
//4 设置SprnigApplication的一些字段值
bindToSpringApplication(environment);
//5 转换环境类型(非用户创建的环境需要进行转换成和当前应用场景适配的环境类型)
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
}
查找或创建一个环境 springboot 提供了 setEnvironment 方法,可以在 run 方法之前调用,来设置用户自己的环境实现,不过用户在多数情况下都不会设置自定义的环境,而是让 springboot 自己创建一个。
- 如果用户定义了环境,就直接使用
- 根据当前的应用类型来创建不同场景的环境,见表格
- 如没有创建,则使用默认策略创建环境
- 如没有创建,则使用 ApplicationEnvironment
应用类型与环境类型
| 类型 | 常规 | Native |
|---|---|---|
| NONE | ApplicationEnvironment | 相同 |
| SERVLET | ApplicationServletEnvironment | 相同 |
| REACTIVE | ApplicationReactiveWebEnvironment | 相同 |
public class SpringApplication {
private ConfigurableEnvironment getOrCreateEnvironment() {
//1 使用用户定义的环境(如果有
if (this.environment != null) {
return this.environment;
}
//2 按照应用类型创建环境
ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(this.webApplicationType);
//3 没有创建出来,则使用默认策略创建
if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
environment = ApplicationContextFactory.DEFAULT.createEnvironment(this.webApplicationType);
}
//4 没有创建出来,最后的兜底策略,使用ApplicationEnvironment
return (environment != null) ? environment : new ApplicationEnvironment();
}
}
配置环境 环境创建好后,需要一些初始化步骤:
- 配置类型转换服务(环境中的值都是字符串,而对象的属性则是各种不同的类型)
- 加入其他的配置源(默认配置、命令行参数)
public class SpringApplication {
private boolean addConversionService = true;
private boolean addCommandLineProperties = true;
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
//1 设置类型转换服务
if (this.addConversionService) {
environment.setConversionService(new ApplicationConversionService());
}
//2 加入其他的配置源
configurePropertySources(environment, args);
//3 按照profile配置(预留方法,空实现)
configureProfiles(environment, args);
}
//向环境中加入其它配置源
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
MutablePropertySources sources = environment.getPropertySources();
//1 如有设置默认配置项则加入,名字是 defaultProperties,(一般都没有,可以调用setDefaultProperties设置)
// 这个源的顺序是最后一位
if (!CollectionUtils.isEmpty(this.defaultProperties)) {
DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources);
}
//2 加入命令行参数配置源,名字为 commandLineArgs,顺序是第一位
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
}
}
不同类型环境的默认配置源
环境中可以有多种配置源,也就是一组 PropertySource,并且他们的顺序是非常重要的,越在前面的优先级越高。环境被创建时,都会内置一些默认的配置源,不同类型的环境的配置源可能不同。 下面列出了 springboot 中使用的不同类型环境的内置的配置源列表
ApplicationEnvironment 的默认 PropertySource 如下:
- systemProperties (系统属性)
- systemEnvironment (系统环境变量)
ApplicationServletEnvironment 的默认 PropertySource 如下:
- servletConfigInitParams (Servle Config 的参数,创建是占位的,后面创建 webServer 才再填充值)
- servletContextInitParams (Servlet Context 初始化参数,创建是占位的,后面创建 webServer 才再填充值)
- systemProperties (系统属性)
- systemEnvironment (系统环境变量)
如果使用 JNDI 的话,还有一个 jndiProperties 的 PropertySource 会排在 servletContextInitParams 之后,不过一般 springboot 的项目都没有使用 JNDI。
ApplicationReactiveWebEnvironment 的默认 PropertySource 和 ApplicationEnvironment 是一样的:
- systemProperties (系统属性)
- systemEnvironment (系统环境变量)
转换 propertySource
转换 propertySource,即代码ConfigurationPropertySources.attach(environment);,springboot 支持键的松散绑定,即环境变量 A_B_C 是可以绑定到 a.b.c 类型的值上的。
提供这种松散绑定的能力需要 PropertySourcesPropertyResolver 的支持,springboot 把所有的 propertySoure 组合到一起,创建了一个名为'configurationProperties',并放置到环境 propertysource 的首位,统一提供带松散绑定能力的键值获取。
public final class ConfigurationPropertySources {
private static final String ATTACHED_PROPERTY_SOURCE_NAME = "configurationProperties";
public static void attach(Environment environment) {
Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
//1 读取出当前环境的所有propertysource
MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
//2 查看当前的环境是不是已经有 configurationProperties了
PropertySource<?> attached = getAttached(sources);
//3 如果已经有了,则复合propertysource即可
if (attached == null || !isUsingSources(attached, sources)) {
attached = new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
new SpringConfigurationPropertySources(sources));
}
//4 把configurationProperties放置到环境首位,优先级最高
// 内部propertysource的顺序依旧是保持的
sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
sources.addFirst(attached);
}
}
设置 SprnigApplication 的一些字段值
springboot 应用支持一些前缀为spring.main的属性,这些属性就在此时会设置到当前 SpringApplication 对象的字段上。绑定的属性值示例如下:
- spring.main.sources
- spring.main.web-application-type=none
- spring.main.banner-mode=off
SpringApplication 中几乎所有的属性值都可以这样设置
public class SpringApplication {
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
//使用 Binder 把environment中前缀为spring.main的键值设置到this上
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
}
catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}
}
转换环境类型 如果 springboot 发现创建的环境类型与当前的应用的场景不匹配的话,则会将创建的环境进行类型的转换。说是类型的转换,实际是创建一个匹配当前场景的环境子类型,然后把当前环境的 properysource、类型转换服务、profile 等参数复制过去而已。
final class EnvironmentConverter {
ConfigurableEnvironment convertEnvironmentIfNecessary(ConfigurableEnvironment environment,
Class<? extends ConfigurableEnvironment> type) {
// 类型一致不需要转换
if (type.equals(environment.getClass())) {
return environment;
}
// 类型不一致,需要转换
return convertEnvironment(environment, type);
}
private ConfigurableEnvironment convertEnvironment(ConfigurableEnvironment environment,
Class<? extends ConfigurableEnvironment> type) {
//1 创建一个指定类型的环境
ConfigurableEnvironment result = createEnvironment(type);
//2 复制profile
result.setActiveProfiles(environment.getActiveProfiles());
//3 复制类型转换服务
result.setConversionService(environment.getConversionService());
//4 复制propertysource
copyPropertySources(environment, result);
return result;
}
}
打印应用标语
打印标语是 springboot 一项非常有意思的内容,但不是核心的内容,我对此不再深入展开,简要介绍下。
标语默认支持三种模式,定义在 Banner.Mode 中
- OFF 关闭
- CONSOLE 标准输出
- LOG 日志
sprinboot 默认会从 banner.txt 中读取标语内容,不过也可以通过参数spring.banner.location指定
public class SpringApplication {
private Banner.Mode bannerMode = Banner.Mode.CONSOLE;
private Banner printBanner(ConfigurableEnvironment environment) {
//1 如果用户设置了关闭标语,则直接退出
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
//2 查找资源加载器(下面加载banner文件需要)
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(null);
//3 创建一个Banner打印器
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
//4,按照不同的标语模式输出标语
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
}
创建应用上下文
springboot 会根据不同的场景创建不同的应用上下文。创建的过程是交给上下文工厂 ApplicationContextFactory 来进行的,默认使用的是 DefaultApplicationContextFactory。
public class SpringApplication {
private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;
protected ConfigurableApplicationContext createApplicationContext() {
//直接交给工厂按照类型创建即可
return this.applicationContextFactory.create(this.webApplicationType);
}
}
DefaultApplicationContextFactory 的创建策略
- 首先根据 WebApplicationType 的类型,在 spring.factories 查找配置的 ApplicationContextFactory
- 如果找到了,就使用找的 ApplicationContextFactory 进行创建
- 如果找不到,则自己创建
springboot 中另外提供了 Servlet 和 Reactive 环境的 ApplicationContextFactory,分别是 ServletWebServerApplicationContextFactory 和 ReactiveWebServerApplicationContextFactory。 创建出的 ApplicationContext 的类型,我再次展示下之前的表格
| 类型 | 工厂 | 常规 | Native |
|---|---|---|---|
| NONE | DefaultApplicationContextFactory | AnnotationConfigApplicationContext | GenericApplicationContext |
| SERVLET | ServletWebServerApplicationContextFactory | AnnotationConfigServletWebServerApplicationContext | ServletWebServerApplicationContext |
| REACTIVE | ReactiveWebServerApplicationContextFactory | AnnotationConfigReactiveWebServerApplicationContext | ReactiveWebServerApplicationContext |
配置应用上下文
配置应用上下文即设置上下文的各项属性,也是非常复杂的过程,基本步骤如下:
- 设置环境
- 初始化一些基本属性
- 调用应用上下文初始化器来初始化应用上下文
- 发送应用上下文准备好事件
- 关闭引导上下文
- 注册一些单例对象(命令行参数、标语)
- 设置 IOC 的属性(是否支持循环依赖,是否支持 BeanDefintion 覆盖)
- 加入延迟初始化工厂后处理器
- 加入一个配置源排序工厂后处理器
- 加载配置的 source 中的所有 BeanDefinition
- 发送应用上下文加载好事件
public class SpringApplication {
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
//1 设置环境
context.setEnvironment(environment);
//2 初始化一些基本属性
postProcessApplicationContext(context);
// aot设置(暂时忽略
addAotGeneratedInitializerIfNecessary(this.initializers);
//3 调用上下文初始化器来初始化上下文
applyInitializers(context);
//4 发送上下文准备好事件
listeners.contextPrepared(context);
//5 关闭引导上下文
bootstrapContext.close(context);
// 记录启动步骤(暂时忽略
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
//6 注册一些单例对象(命令行参数、标语)
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
//7 设置 IOC 的属性(是否支持循环依赖,是否支持 BeanDefintion 覆盖)
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences);
if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
}
//8 加入延迟初始化工厂后处理器
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
//9 加入一个配置源排序工厂后处理器
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
//10 加载配置的source中的所有BeanDefinition (非native)
// 一般情形中,仅仅把标注@SpringApplication注册为BeanDefinition,不会解析上面的注解。
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
}
//11 发送应用上下文加载好事件
listeners.contextLoaded(context);
}
}
初始化一些基本属性
主要是根据当前 SpringApplition 的一些配置来设置应用上下文的属性:
- 设置 beanname 生成器
- 设置资源加载器
- 设置类型转换服务(和环境中使用的同一个类型转换器)
public class SpringApplication {
private BeanNameGenerator beanNameGenerator;
private ResourceLoader resourceLoader;
private boolean addConversionService = true;
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
//1 设置beanname生成器
if (this.beanNameGenerator != null) {
context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, this.beanNameGenerator);
}
//2 设置资源加载器
if (this.resourceLoader != null) {
if (context instanceof GenericApplicationContext genericApplicationContext) {
genericApplicationContext.setResourceLoader(this.resourceLoader);
}
if (context instanceof DefaultResourceLoader defaultResourceLoader) {
defaultResourceLoader.setClassLoader(this.resourceLoader.getClassLoader());
}
}
//3 设置类型转换服务(和环境中使用的同一个类型转换器)
if (this.addConversionService) {
context.getBeanFactory().setConversionService(context.getEnvironment().getConversionService());
}
}
}
调用应用上下文初始化器来初始化应用上下文
在 SpringApplication 的构造函数中,我们看到它加载了所有的 ApplicationContextInitializer,这里则是回调这些应用上下文初始化器的位置。
这里会取出所有的 ApplicationContextInitializer,然后依次调用方法 initialize 来初始化
public class SpringApplication {
protected void applyInitializers(ConfigurableApplicationContext context) {
//1 遍历所有的ApplicationContextInitializer
for (ApplicationContextInitializer initializer : getInitializers()) {
//2 解析并判断类型
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
//3 初始化应用上下文
initializer.initialize(context);
}
}
}
此处我列出 springboot 和 autoconfigure 项目中配置的 ApplicationContextInitializer,并探究他们的作用。
在 springboot 中默认在 spring.factories 中配置了一些 ApplicationContextInitializer 的实现,如下
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
在 springboot-autoconfigure 中配置如下的实现
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
上面的这些 ApplicationContextInitializer 会在执行前进行排序。下面按照执行顺序依次简单介绍下各个初始化器的功能和作用
-
DelegatingApplicationContextInitializer
- 代理应用上下文初始化器,可以从环境中找出配置的 ApplicationContextInitializer。order 为 0,第一个应用的
- springboot 默认是从 spring.factories 文件中查找 ApplicationContextInitializer 的实现类名的
- 第一点:这样实际需要外部的 jar 包都遵循 META-INF/spring.facoties 的配置方式,对于一些项目来说可能是不太灵活
- 第二点:这样的配置太死板,文件中写的类都会被加载并应用,对于需要条件判断来加载的不太适合。
- 于是 springboot 于是提供了 DelegatingApplicationContextInitializer,可以从环境中读取
context.initializer.classes条目的类型,这个值就是另外的 ApplicationContextInitializer 实现类名,比如 在 SpringBoot 启动前,执行如下代码,即可添加进来,并立马执行
System.setProperty("context.initializer.classes", "com.wpp.run.MyApplicationContextInitializer"); -
SharedMetadataReaderFactoryContextInitializer
- 创建一个在 ConfigurationClassPostProcessor 和 SpringBoot 中共享的 CachingMetadataReaderFactoryPostProcessor
- 这个 CachingMetadataReaderFactoryPostProcessor 是一个 BeanDefinitionRegistryPostProcessor,并且优先级高于 ConfigurationClassPostProcessor 还高(因为需要修改 ConfigurationClassPostProcessor 的属性,所以必须高),它实际上会最终向容器中配置一个 ConcurrentReferenceCachingMetadataReaderFactory,用于保存 .class 文件和 MetadataReader 的缓存,可以优化类元数据的读取性能
- 并且这个 ConcurrentReferenceCachingMetadataReaderFactory 会替换成 Spring 的配置解析类 ConfigurationClassPostProcessor 属性 metadataReaderFactory 上,以达到 Springboot 和 ConfigurationClassPostProcessor 的缓存共享
- 这个 ConcurrentReferenceCachingMetadataReaderFactory 在 IOC 中的 benaName 是 org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
- 简言之,这个初始化器用于把 ConfigurationClassPostProcessor 的 metadataReaderFactory 设置成 ConcurrentReferenceCachingMetadataReaderFactory
-
ContextIdApplicationContextInitializer
- 设置 applicationContext 的 ID
- 优先使用属性
spring.application.name,没有配置则使用固定字符串application - IOC 容器中应用上下文的 Id 是一个 ContextId 类型的对象,并且也会添加到容器中
-
ConfigurationWarningsApplicationContextInitializer
- 向上下文中添加 ConfigurationWarningsPostProcessor
- ConfigurationWarningsPostProcessor 会检查@ComponentScan 的配置,如配置了
org或org.springframework就会打印类似下面的警告信息
** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of 'org'.- 原因也很简单,org 或者 org.springframework 包下许多包都是需要各种条件化配置评估的才能运行加载的,如果盲目的写,一般也会缺少各种 class,根本启动不了项目
-
RSocketPortInfoApplicationContextInitializer
- 向 ApplicationContext 中加入一个监听·RSocketServerInitializedEvent· 事件的监听器
- 该监听器听到 RSocketServerInitializedEvent 事件时,会修改(没有就创建)环境(当前和所有父亲环境)的配置源
server.ports,向里面添加一个属性local.rsocket.server.port=$port - 这里的 port 是 socket 的端口
-
ServerPortInfoApplicationContextInitializer
- 这个 ServerPortInfoApplicationContextInitializer 个上面的 RSocketPortInfoApplicationContextInitializer 类似,是加入一个监听 WebServerInitializedEvent 事件的监听器
- 该监听器听到 WebServerInitializedEvent 事件时,会修改(没有就创建)环境(当前和所有父亲环境)的配置源
server.ports,先里面添加一个属性"local.${上下文名字}.port=${webserver.port}" - 这里的 port 是 web 服务器的端口
-
ConditionEvaluationReportLoggingListener
- 向 ApplicationContext 中加入一个 ConditionEvaluationReportListener 监听器
- ConditionEvaluationReportListener 会在事件 ContextRefreshedEvent 或者 ApplicationFailedEvent 时,打印出所有的条件评估日志
关闭引导上下文
随着应用上下文的初始化完成,引导上下文也随之需要进行关闭。调用bootstrapContext.close(context);来进行关闭。
在 DefaultBootstrapContext 的 close 实现中,他会广播一个 BootstrapContextClosedEvent 事件。这个事件在 springboot 项目中没有监听器感兴趣,不过在 springcloud 中确实监听的实现。
public class DefaultBootstrapContext implements ConfigurableBootstrapContext {
private final ApplicationEventMulticaster events = new SimpleApplicationEventMulticaster();
public void close(ConfigurableApplicationContext applicationContext) {
// 调用自己的事件广播器进行广播
this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext));
}
}
加入延迟初始化工厂后处理器
默认情况下 springboot 不会加入 LazyInitializationBeanFactoryPostProcessor。用户可以通过方法 setLazyInitialization 或属性spring.main.lazyInitialization=true来开启懒加载模式。此处主要探究下 LazyInitializationBeanFactoryPostProcessor。用户可以通过方法是如何设置 Bean 的懒加载的。
- 找出当前 IOC 的所有延迟加载排除器
- 遍历当前 IOC 所有的 BeanDefinition,
- 如果 BeanDefinition 不是被排除的或者延迟加载属性没有被设置过,就设置为 true,表示延迟加载
public final class LazyInitializationBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
//1 找出当前IOC的所有延迟加载排除器
Collection<LazyInitializationExcludeFilter> filters = getFilters(beanFactory);
//2 遍历当前IOC所有的BeanDefinition,
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
if (beanDefinition instanceof AbstractBeanDefinition abstractBeanDefinition) {
//3 如果BeanDefinition不是被排除的或者延迟加载属性没有被设置过,就设置为true,表示延迟加载
postProcess(beanFactory, filters, beanName, abstractBeanDefinition);
}
}
}
private void postProcess(ConfigurableListableBeanFactory beanFactory,
Collection<LazyInitializationExcludeFilter> filters, String beanName,
AbstractBeanDefinition beanDefinition) {
//1 如果BeanDefinition的属性lazyInit已经被赋值了,就不处理
Boolean lazyInit = beanDefinition.getLazyInit();
if (lazyInit != null) {
return;
}
//2 查看当前bean的类型,是不是被排除的
Class<?> beanType = getBeanType(beanFactory, beanName);
if (!isExcluded(filters, beanName, beanDefinition, beanType)) {
beanDefinition.setLazyInit(true);
}
}
}
LazyInitializationExcludeFilter
关于这个延迟加载排除器,它是从 BeanFactory 中 get 出来的,所以我们可以使用@Bean 和其它的方式添加。这个接口的代码如下:
public interface LazyInitializationExcludeFilter {
//确认是否排除该BeanDefinition
boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class<?> beanType);
}
在 autoconfigure 中仅有一个实现类 ScheduledBeanLazyInitializationExcludeFilter,它用于排除以下类
- 实现了接口 AopInfrastructureBean、TaskScheduler、ScheduledExecutorService 的类
- 方法上有
@Scheduled或@Schedules注解的类
这部分关于调度的类不能延迟加载也是可以理解的,调度任务本来就必须是自动执行的,如有设置成延迟加载了,那么根本就不会有人来触发这些调度,这会导致所有的调度任务失效。
加入一个配置源排序工厂后处理器
类 PropertySourceOrderingBeanFactoryPostProcessor 是一个内部类,它的作用只有一个就是确保 默认配置源 defaultProperties 一定在所有配置源最末尾。实现代码如下:
private static class PropertySourceOrderingBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
//把defaultProperties移动到末尾
DefaultPropertiesPropertySource.moveToEnd(this.context.getEnvironment());
}
}
加载配置的 source 中的 BeanDefinition
这里的加载 BeanDefinition 和 Spring 的配置解析并不相同,这里是确保通过 SpringApplition 中的 primarySources 和 sources 设置的配置类自身会被加载成 IOC 的 BeanDefintion。
比如文章前面的的示例代码(如下)中提到的 DemoApplication,仅仅是把 DemoApplication 这个类自己解析成 BeanDefinition,加载到 IOC 中。至于它的注解 @SpringBootApplication 中的自动配置和包扫描等工作不会在此时展开,而是由 Spring 中配置解析类 ConfigurationClassPostProcessor 来完成的。
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
source 支持的类型
springboot 的 source 支持非常多的类型,他们的加载方式略微不同,这部分工作是交给 BeanDefinitionLoader 来完成的。
支持的类型如下
- 类(直接做成 BeanDefinition)
- Resource(xml 文件、groovy 脚本)
- Package 类(会扫描这个 Package)
- 字符串(实际是字符串表示的类、Resource 或 Package)
刷新应用上下文
在做了那么多的铺垫和准备后,终于到了刷新应用上下文!不过 springboot 并没有重写 spring 的刷新,而是直接调用 AplicationConetext 的 refresh 就完成了刷新。所以说,springboot 的核心还是 spring,springboot 项目的关键是boot,即启动。
public class SpringApplication {
private boolean registerShutdownHook = true;
static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();
private void refreshContext(ConfigurableApplicationContext context) {
//1 注册JVM停止的回调钩子
if (this.registerShutdownHook) {
shutdownHook.registerApplicationContext(context);
}
//2 刷新
refresh(context);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {
//调用applicationContext的refresh方法完成刷新
applicationContext.refresh();
}
}
shutdownHook
Java 的 Runtime 类提供了一个 addShutdownHook(Thread hook) 方法。使用示例代码如下:
public static void main(String[] args) throws InterruptedException {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("停止Hook");
}));
Thread.currentThread().join();
}
- 程序正常退出,hook 会被回调
- 程序异常退出,hook 会被回调
- kill -15 pid ,hook 会被回调 (SIGTERM 信号)
- kill -9 pid ,hook 不会被回调 (SIGKILL 信号)
springboot 则使用类 SpringApplicationShutdownHook 把这部分进行了封装,SpringApplicationShutdownHook 本身会被注册到 Hook 中,当 JVM 停止并回调时,会执行 SpringApplicationShutdownHook 的 run 方法:
class SpringApplicationShutdownHook implements Runnable {
private final Handlers handlers = new Handlers();
@Override
public void run() {
Set<ConfigurableApplicationContext> contexts;
Set<ConfigurableApplicationContext> closedContexts;
Set<Runnable> actions;
synchronized (SpringApplicationShutdownHook.class) {
this.inProgress = true;
contexts = new LinkedHashSet<>(this.contexts);
closedContexts = new LinkedHashSet<>(this.closedContexts);
actions = new LinkedHashSet<>(this.handlers.getActions());
}
//1 遍历调用每个ApplicationContext的 closeAndWait 方法
contexts.forEach(this::closeAndWait);
closedContexts.forEach(this::closeAndWait);
//2 遍历调用注册到Handler上的Runnable
actions.forEach(Runnable::run);
}
}
那么如何向 Handlers 中注册我们自己的 Runnable 呢?在 SpringApplication 中提供了静态方法 getShutdownHandlers ,代码如下:
public class SpringApplication {
public static SpringApplicationShutdownHandlers getShutdownHandlers() {
return shutdownHook.getHandlers();
}
}
我们注册 shutdownHook 的使用示例如下:
public class HookDemo {
public static void main(String[] args) {
SpringApplication.getShutdownHandlers().add(() -> {System.out.println("my shutdown hook");});
SpringApplication.run(HookDemo.class, args);
}
}
刷新后操作(springboot 预留方法,可忽略
这部分是 springboot 的预留方法,暂无作用
public class SpringApplication {
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
}
记录整体耗时
代码Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);计算出了从
run 方法开始到 afterRefresh 的中间的耗时. 从事件来分析是同 starting 事件到 started 之间的耗时情况。
发送应用已启动事件
代码listeners.started(context, timeTakenToStartup); 调用监听器的 started,上文已经分析过,此处不再赘述。
执行所有的 ApplicationRunner 和 CommandLineRunner
在容器刷新完成后,容器会依次调用配置的 ApplicationRunner 和 CommandLineRunner.这两种 Runner 的调用顺序是由它们配置的 Order 决定的,与类型无关。 这两种 Runner 的区别如下,仅仅是调用参数的不同
- ApplicationRunner 把启动命令参数封装成易用的 ApplicationArguments
- CommandLineRunner 把启动命令参数透传过来
public class SpringApplication {
private void callRunners(ApplicationContext context, ApplicationArguments args) {
// 1 实例化所有的 ApplicationRunner 和 CommandLineRunner
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//2 按照Order排序
AnnotationAwareOrderComparator.sort(runners);
//3 依次调用
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner applicationRunner) {
callRunner(applicationRunner, args);
}
if (runner instanceof CommandLineRunner commandLineRunner) {
callRunner(commandLineRunner, args);
}
}
}
}
启动失败处理
springboot 启动过程中如果发送了异常,就会进入失败处理
- 确定并处理程序退出码,如果是非正常退出,还会发布 ExitCodeEvent 事件
- 发布 failed 事件
- 打印当前的错误原因
- 关闭上下文,并从 shutdownHook 中移除该上下文(如果上下文是创建成功的情况)
- 再从抛出该异常
public class SpringApplication {
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, SpringApplicationRunListeners listeners) {
try {
try {
//1. 确定并处理程序退出码
handleExitCode(context, exception);
//2 发布failed事件
if (listeners != null) {
listeners.failed(context, exception);
}
}
finally {
//3 打印当前的错误原因
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
//4 关闭上下文
context.close();
//5 JVMshutdown回调中移除该context
shutdownHook.deregisterFailedApplicationContext(context);
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
//6 抛出该异常
ReflectionUtils.rethrowRuntimeException(exception);
}
//处理退出码
private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {
//1 确定退出码
// 内部会借助ExitCodeGenerators从ExitCodeExceptionMapper中获取退出码
int exitCode = getExitCodeFromException(context, exception);
if (exitCode != 0) {
if (context != null) {
// 2 如果是非正常退出 ,则发布ExitCodeEvent事件
context.publishEvent(new ExitCodeEvent(context, exitCode));
}
//3 设定exitCode,用于程序结束的退出码
SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
if (handler != null) {
handler.registerExitCode(exitCode);
}
}
}
}
SpringBoot 启动总结
至此 SpringBoot 的启动步骤就介绍完毕了,小结一下 springboot 的启动:
在 SpringApplication 的构造函数中,首先设置了主配置源,然后推断出应用的类型,这个应用类型用于确定 Environment 和 ApplicationContext 的类型,再从 spring.factories 中读取了引导上下文的初始化器、应用上下文的初始化器、应用监听器等,最后从调用栈中找出 main 方法的所在主类。
在启动方法 run 中,首先创建并初始化了引导上下文、创建启动过程监听器,发布 starting 事件表示 springboot 应用启动了,然后创建并配置了环境,并发出 environmentPrepared 事件,之后就打印出标语。在环境都准备好了之后,就要开始创建应用上下文了,这里上下文的类型是由构造函数中推断出的应用类型决定的,调用上下文工厂创建出上下文后,就开始了配置上下文的过程,先是设置应用上下文的环境,然后调用上下文初始化器进行初始化,初始化完毕后,就可以认为上下文是准备好了的,于是发布 contextPrepared 事件。接着关闭引导上下文,向应用上下文 IOC 中添加单例对象和工厂后处理器,随后把配置的 source 全部加载成 IOC 的 BeanDefinition。 在这些源都加载完毕后,就可以刷新上下文了。刷新完毕,发布 started 事件,应用就是启动好了,紧接着把注册的 Runner 全部调用,至此容器就进入了 ready,可以对外提供服务了。
更新记录:
- 2023-02-17
- 把BootStrapContext的名字从"启动上下文"改为"引导上下文"