Appearance
Spring Boot 启动流程
本文介绍Spring Boot的启动流程。
1. 概述
Spring Boot程序启动只需要下面一行代码:
java
SpringApplication.run(BootApplication.class, args);在这行代码之后,主要分为两步:
- 第一步:创建
SpringApplication对象- 确定Bean的主来源配置类,即加了
@Configuration注解的类; - 推断应用类型;
- 获取
BootstrapRegistryInitializer; - 获取
ApplicationContextInitializer; - 获取
ApplicationListener; - 推断主类,即包含
main方法的类;
- 确定Bean的主来源配置类,即加了
- 第二步:调用
SpringApplication对象的run()方法,主要做五件事- 准备环境
Environment,就是各种配置参数; - 打印
Banner,也就是在控制台打印Spring字样标志; - 准备容器
ApplicationContext; - 调用
Runner接口,又分为CommandLineRunner和ApplicationRunner; - 在做上面四件事之间,穿插着发布消息;
- 准备环境
2. 创建SpringApplication对象
本小节具体介绍创建SpringApplication对象过程中作了哪些事,以及有什么作用。
创建SpringApplication的构造方法如下:
java
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "'primarySources' must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}请先查看附录1-SpringFactoriesLoader。
2.1 确定Bean配置类
在第4行,将Bean的配置类保存起来,也就是之后可以从这些类中获取Bean定义:
java
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));除了在构造方法中传入配置类,我们也可以通过加载XML的方式:
java
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
// 设置XML文件为加载Bean的来源
springApplication.setSources(Set.of("classpath:bean.xml"));
ConfigurableApplicationContext applicationContext = springApplication.run(args);
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bean01" class="org.example.bean.Bean01"></bean>
</beans>可以在结果中看到bean01,说明设置成功。
除了从XML中加载Bean,Spring还可以采用类路径扫描的方式加载Bean,使用的类是ClassPathBeanDefinitionScanner:
java
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
// 设置XML文件为加载Bean的来源
springApplication.setSources(Set.of("classpath:bean.xml"));
ConfigurableApplicationContext applicationContext = springApplication.run(args);
// 采用类路径扫描的方式加载Bean
ClassPathBeanDefinitionScanner scanner =
new ClassPathBeanDefinitionScanner((BeanDefinitionRegistry) applicationContext);
scanner.scan("org.example.entity");
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}注意,要加入容器的Bean类上需要标注 @Component, @Repository, @Service, 或 @Controller 注解,否则无法加入容器。
2.2 推断应用类型
在第5行,推断应用类型:
java
this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());主要的逻辑是WebApplicationType.deduceFromClasspath(),源码如下:
java
public enum WebApplicationType {
NONE,
SERVLET,
REACTIVE;
private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
}主要逻辑如下:
- 首先判断类路径下是否存在WebFlux相关的类,并且不存在WebMvc相关的类,那么推断应用类型为REACTIVE;
- 然后判断类路径是否存在Servlet相关的类,如果还不存在,则推断为普通程序,即NONE;
- 最后,推断应用程序为SERVLET;
总结就是根据类路径下是否存在某些类判断程序类型,通过程序类型,在后面可以创建不同的容器;
2.3 BootstrapRegistryInitializer
设置bootstrapRegistryInitializers:
java
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));在创建应用容器(ApplicationContext)之前,有可能需要进行一些初始化配置,在Spring中,提供了BootstrapContext(初始上下文),BootstrapRegistryInitializer则是对初始上下文创建后进行初始化。
当创建应用容器过程中,准备好环境Environment后,会发出EnvironmentPrepared事件,该事件监听器可以拿到初始上下文,从而可以扩展环境。
因此,BootstrapRegistryInitializer主要用于Spring Cloud环境中,用于初始化与 Spring Cloud Config Server 的连接,加载远程配置属性,然后加载进环境Environment中。
我们可以使用如下方法使用BootstrapRegistryInitializer:
java
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
// 设置XML文件为加载Bean的来源
springApplication.setSources(Set.of("classpath:bean.xml"));
springApplication.addBootstrapRegistryInitializer(registry -> {
// 假设下面代码是从远程获取配置
Map<String, Object> properties = new HashMap<>();
properties.put("custom.config.key", "from spring cloud config server");
MapPropertySource propertySource = new MapPropertySource("custom-bootstrap", properties);
// 注册到 BootstrapRegistry,
registry.register(MapPropertySource.class, context -> propertySource);
});
springApplication.addListeners(new ApplicationListener<ApplicationEnvironmentPreparedEvent>() {
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
// 从初始化容器中获取配置信息
ConfigurableBootstrapContext bootstrapContext = event.getBootstrapContext();
MapPropertySource mapPropertySource = bootstrapContext.getRegisteredInstanceSupplier(MapPropertySource.class).get(bootstrapContext);
// 将获取到的配置信息加入到环境中
ConfigurableEnvironment environment = event.getEnvironment();
environment.getPropertySources().addLast(mapPropertySource);
}
});
ConfigurableApplicationContext applicationContext = springApplication.run(args);
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySource<?> propertySource : environment.getPropertySources()) {
System.out.println(propertySource);
}
System.out.println(environment.getProperty("custom.config.key"));
}结果如下:
txt
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
MapPropertySource {name='custom-bootstrap'}
ApplicationInfoPropertySource {name='applicationInfo'}
from spring cloud config server可以发现,远程获取的配置已经成功加载进环境中。
2.4 ApplicationContextInitializer
设置ApplicationContextInitializer:
java
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));这是在应用容器(ApplicationContext)创建完成后,对容器进行初始化,用法如下:
java
public class Main {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
// 设置XML文件为加载Bean的来源
springApplication.setSources(Set.of("classpath:bean.xml"));
// 设置容器初始化器
springApplication.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("容器初始化...");
// 可以在这里注册组件
if (applicationContext instanceof GenericApplicationContext genericApplicationContext) {
genericApplicationContext.registerBean(MyBean.class);
}
}
});
ConfigurableApplicationContext applicationContext = springApplication.run(args);
for (String name : applicationContext.getBeanDefinitionNames()) {
System.out.println(name);
}
}
}
class MyBean{
}结果发现,容器中的组件包含MyBean。
2.5 ApplicationListener
java
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));在容器准备、启动过程中,会发布很多事件,我们可以添加事件订阅器,从而对某些事件作出反应:
java
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
// 设置XML文件为加载Bean的来源
springApplication.setSources(Set.of("classpath:bean.xml"));
// 添加事件监听器
springApplication.addListeners(new ApplicationListener<ApplicationEvent>() {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("事件为:" + event.getClass().getName());
}
});
springApplication.run(args);
}结果为:
txt
事件为:org.springframework.boot.context.event.ApplicationStartingEvent
事件为:org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
事件为:org.springframework.boot.context.event.ApplicationContextInitializedEvent
事件为:org.springframework.boot.context.event.ApplicationPreparedEvent
事件为:org.springframework.context.event.ContextRefreshedEvent
事件为:org.springframework.boot.context.event.ApplicationStartedEvent
事件为:org.springframework.boot.availability.AvailabilityChangeEvent
事件为:org.springframework.boot.context.event.ApplicationReadyEvent
事件为:org.springframework.boot.availability.AvailabilityChangeEvent
事件为:org.springframework.context.event.ContextClosedEvent因此,针对不同事件,我们可以做出不同的调整。
2.6 推断主类
java
this.mainApplicationClass = deduceMainApplicationClass();就是通过堆栈信息,判断哪个类包含main方法。
3. 调用run()方法
当创建了SpringApplication对象后,我们就可以调用run()方法了:
java
public ConfigurableApplicationContext run(String... args) {
// 记录启动过程的耗时
Startup startup = Startup.create();
if (this.properties.isRegisterShutdownHook()) {
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
// 创建初始上下文环境
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// SpringApplicationRunListeners 可以看成是事件发布器!!!
SpringApplicationRunListeners listeners = getRunListeners(args);
// 发布starting事件
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 封装启动参数args
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 打印Spring标志
Banner printedBanner = printBanner(environment);
// 创建容器
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 准备容器,包括设置环境、注册组件
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 刷新容器,BeanFactory后处理器这时开始处理
refreshContext(context);
// 没用的方法
afterRefresh(context, applicationArguments);
// 记录容器已启动
startup.started();
if (this.properties.isLogStartupInfo()) {
new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
}
// 发布容器已启动事件
listeners.started(context, startup.timeTakenToStarted());
// 调用Runner接口,包括CommandLineRunner和ApplicationRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 如果启动过程中出现错误,处理错误
throw handleRunFailure(context, ex, listeners);
}
try {
if (context.isRunning()) {
// 发布容器已准备好事件
listeners.ready(context, startup.ready());
}
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, null);
}
return context;
}3.1 准备Environment
环境准备主要是这一行代码:
java
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);在Spring Boot应用中,创建的环境是ApplicationEnvironment,默认情况下,里面配置来源只有两个:JVM环境变量和操作系统环境变量:
TIP
注意,ApplicationEnvironment是非public的,测试代码需要在org.springframework.boot包下,可以自己创建一个同名包。
java
package org.springframework.boot;
import org.springframework.core.env.PropertySource;
public class Main {
public static void main(String[] args) {
ApplicationEnvironment applicationEnvironment = new ApplicationEnvironment();
for (PropertySource<?> propertySource : applicationEnvironment.getPropertySources()) {
System.out.println(propertySource);
}
}
}结果如下:
java
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}但是,完整的配置来源如下:
java
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
springApplication.setSources(Set.of("classpath:bean.xml"));
ConfigurableApplicationContext applicationContext = springApplication.run(args);
for (PropertySource<?> propertySource : applicationContext.getEnvironment().getPropertySources()) {
System.out.println(propertySource);
}
}txt
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
ApplicationInfoPropertySource {name='applicationInfo'}注意,启动程序时需要加上命令行参数,否则第二行SimpleCommandLinePropertySource没有。
首先看SimpleCommandLinePropertySource和ApplicationInfoPropertySource,
SimpleCommandLinePropertySource:配置来源于命令行参数,具有最高优先级,所以排在前面;ApplicationInfoPropertySource:其中保存了Java程序的进程ID,如{spring.application.pid=6089};
这是在方法configurePropertySources()中添加的,添加方法如下:
java
public static void main(String[] args) {
ApplicationEnvironment applicationEnvironment = new ApplicationEnvironment();
MutablePropertySources propertySources = applicationEnvironment.getPropertySources();
System.out.println("----------前-------------------");
for (PropertySource<?> propertySource : propertySources) {
System.out.println(propertySource);
}
propertySources.addFirst(new SimpleCommandLinePropertySource(args));
propertySources.addLast(new ApplicationInfoPropertySource(Main.class));
System.out.println("----------后-------------------");
for (PropertySource<?> propertySource : propertySources) {
System.out.println(propertySource);
}
// 查看程序信息
System.out.println(propertySources.get("applicationInfo").getSource());
}txt
----------前-------------------
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
----------后-------------------
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ApplicationInfoPropertySource {name='applicationInfo'}
{spring.application.pid=6089}RandomValuePropertySource和OriginTrackedMapPropertySource是由EnvironmentPostProcessor(环境后处理器)添加的,实现类分别为RandomValuePropertySourceEnvironmentPostProcessor和ConfigDataEnvironmentPostProcessor,而环境后处理器是由EnvironmentPostProcessorApplicationListener进行调用的,这是一个事件监听器,用于在环境准备好后执行。
RandomValuePropertySource:可以用来产生一些随机值,例如random.int、random.long、random.uuid、random.xx(x x为任意值,产生一个随机字节数组,长度为16);OriginTrackedMapPropertySource:用来从类路径下的配置文件application.properties、application.yml加载配置信息;
添加逻辑如下:
java
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
springApplication.setSources(Set.of("classpath:bean.xml"));
ApplicationEnvironment applicationEnvironment = new ApplicationEnvironment();
MutablePropertySources propertySources = applicationEnvironment.getPropertySources();
System.out.println("----------前-------------------");
for (PropertySource<?> propertySource : propertySources) {
System.out.println(propertySource);
}
propertySources.addFirst(new SimpleCommandLinePropertySource(args));
propertySources.addLast(new ApplicationInfoPropertySource(Main.class));
RandomValuePropertySourceEnvironmentPostProcessor environmentPostProcessor1 =
new RandomValuePropertySourceEnvironmentPostProcessor(new DeferredLogs());
environmentPostProcessor1.postProcessEnvironment(applicationEnvironment, springApplication);
ConfigDataEnvironmentPostProcessor environmentPostProcessor2 =
new ConfigDataEnvironmentPostProcessor(new DeferredLogs(), new DefaultBootstrapContext());
environmentPostProcessor2.postProcessEnvironment(applicationEnvironment, springApplication);
System.out.println("----------后-------------------");
for (PropertySource<?> propertySource : propertySources) {
System.out.println(propertySource);
}
}结果如下:
java
----------前-------------------
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
----------后-------------------
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
ApplicationInfoPropertySource {name='applicationInfo'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.yml]' via location 'optional:classpath:/''}使用Environment产生随机值:
java
System.out.println(applicationEnvironment.getProperty("random.int"));
System.out.println(applicationEnvironment.getProperty("random.long"));
System.out.println(applicationEnvironment.getProperty("random.uuid"));
System.out.println(applicationEnvironment.getProperty("random.abc"));3.2 打印Banner
在介绍Banner之前,请先参考附录2:Binder,并且,在准备Environment之后,会调用如下方法:
java
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties));
}
catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}将配置文件中以 spring.main 开头的属性绑定到SpringApplication中的properties属性上。
与这一步相关的配置是:
txt
spring.main.banner-mode取值有三个:
- OFF:不打印Banner;
- CONSOLE:将Banner打印到控制台;
- LOG:将Banner打印到日志文件;
打印Banner调用的方法如下:
java
private Banner printBanner(ConfigurableEnvironment environment) {
// 不打印Banner
if (this.properties.getBannerMode(environment) == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(null);
// 创建Banner打印器,这里会加载Banner
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
// 打印到日志文件
if (this.properties.getBannerMode(environment) == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
// 打印到控制台
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}在bannerPrinter.print()方法中,逻辑如下:
- 首先获取Banner,如果在类路径下没有找到文件banner.txt,那么将使用默认的Banner:
SpringBootBanner;我们可以在配置文件中修改Banner的位置:spring.banner.location(默认值banner.txt); - 然后打印Banner;
可以在如下网站创建自定义的Banner:https://devops.datenkollektiv.de/banner.txt/index.html
在类路径下添加文件banner.txt,里面的内容是自定义的banner,就会打印自定义的banner。
3.3 准备容器
准备容器ApplicationContext主要分为以下内容:
- 创建容器:根据应用类型,创建不同容器;
- 初始化容器:为容器设置环境Environment、应用
ApplicationContextInitializer、从Bean来源加载Bean定义; - 刷新容器:调用容器的
refresh()方法,此时Bean工厂后处理器、Bean后处理器(对于单例Bean)起作用;
主要代码如下:
java
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);由于之前在容器已经介绍过容器相关知识,此处略过。
3.4 调用Runner
在准备好容器后,就会调用Runner:
java
callRunners(context, applicationArguments);Runner主要分为两类:
CommandLineRunner:javapublic interface CommandLineRunner extends Runner { void run(String... args) throws Exception; }ApplicationRunner:javapublic interface ApplicationRunner extends Runner { void run(ApplicationArguments args) throws Exception; }
两者最大的不同是参数的不同。调用Runner的具体方法如下:
java
private void callRunner(Runner runner, ApplicationArguments args) {
if (runner instanceof ApplicationRunner) {
callRunner(ApplicationRunner.class, runner, (applicationRunner) -> applicationRunner.run(args));
}
if (runner instanceof CommandLineRunner) {
callRunner(CommandLineRunner.class, runner,
(commandLineRunner) -> commandLineRunner.run(args.getSourceArgs()));
}
}ApplicationArguments是对args的封装:
java
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);ApplicationArguments会将args分为两类:
OptionArgs:以--开头的参数,例如--server.port;NonOptionArgs():除了OptionArgs,其他都是;
java
public static void main(String[] args) {
String[] testArgs = new String[]{
"--server.port=8080",
"--host",
"--ip=",
"debug",
"name=zs",
"-age=20"
};
ApplicationArguments applicationArguments = new DefaultApplicationArguments(testArgs);
System.out.println(applicationArguments.getNonOptionArgs());
System.out.println(applicationArguments.getOptionNames());
for (String optionName : applicationArguments.getOptionNames()) {
System.out.println(applicationArguments.getOptionValues(optionName));
}
}结果如下:
txt
[debug, name=zs, -age=20] // NonOptionArgs
[ip, server.port, host] // OptionNames
[]
[8080]
[]如何定义Runner呢?我们只需要使组件继承CommandLineRunner或ApplicationRunner接口即可:
java
public class Bean01 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("command line runner");
}
}java
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication();
springApplication.setSources(Set.of("classpath:bean.xml")); // bean.xml包含Bean01定义
springApplication.run(args);
}运行上面代码,会发现"command line runner"成功打印。
3.5 事件发布器
在准备环境、创建容器过程中的某些时间点,会发布特定的事件,这个工作由SpringApplicationRunListeners完成:
jade
SpringApplicationRunListeners listeners = getRunListeners(args);其中聚合了SpringApplicationRunListener:
TIP
SpringApplicationRunListener,看名字像监听器,但其实是事件发布器,
Details
java
public interface SpringApplicationRunListener {
default void starting(ConfigurableBootstrapContext bootstrapContext) {
}
default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) {
}
default void contextPrepared(ConfigurableApplicationContext context) {
}
default void contextLoaded(ConfigurableApplicationContext context) {
}
default void started(ConfigurableApplicationContext context, Duration timeTaken) {
}
default void ready(ConfigurableApplicationContext context, Duration timeTaken) {
}
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}
}其中一个实现类是EventPublishingRunListener,它又通过SimpleApplicationEventMulticaster与ApplicationListener产生关联。
附录
1. SpringFactoriesLoader
在 Spring Boot 中,SpringFactoriesLoader 是一个核心工具类,位于 Spring Core 模块(org.springframework.core.io.support包),用于实现 Spring 的自动配置和 SPI(Service Provider Interface)机制。它的主要作用是从类路径下的 META-INF/spring.factories 文件中加载和实例化配置的类,从而支持 Spring 的可扩展性和模块化。
工作原理:
SpringFactoriesLoader扫描类路径下所有META-INF/spring.factories文件。- 这些文件是 properties 格式,键是接口或类的全限定名,值是实现类的全限定名列表(逗号分隔)。
- 加载时,
SpringFactoriesLoader根据指定的接口类型返回对应的实现类列表或实例。
例如,现在有如下接口和实现类:
java
package org.example.bean;
public interface IRun {
void run();
}
public class Cat implements IRun{
@Override
public void run() {
System.out.println("cat is running");
}
}
public class Dog implements IRun{
@Override
public void run() {
System.out.println("dog is running");
}
}然后在META-INF/spring.factories中配置如下:
java
org.example.bean.IRun=org.example.bean.Cat,org.example.bean.Dog最后,我们就可以使用SpringFactoriesLoader加载META-INF/spring.factories中指定接口的实现类了:
java
public static void main(String[] args) {
List<IRun> iRuns = SpringFactoriesLoader.loadFactories(IRun.class, Main.class.getClassLoader());
for (IRun iRun : iRuns) {
System.out.println(iRun);
}
}结果如下:
txt
org.example.bean.Cat@2acf57e3
org.example.bean.Dog@506e6d5eConfigurationPropertySourcesPropertySource的作用是将Envirnment中的属性源适配为一个统一的入口,提供松散属性绑定,因此放在第一个,使用如下命令添加:
java
ConfigurationPropertySources.attach(environment);例如,现在配置文件中又如下配置:
properties
user-name=zs
user_age=20
userAddress=gz我们在程序中使用如下方式获取配置:
java
System.out.println(applicationEnvironment.getProperty("user-name"));
System.out.println(applicationEnvironment.getProperty("user-age"));
System.out.println(applicationEnvironment.getProperty("user-address"));如果没有ConfigurationPropertySourcesPropertySource,结果如下,可以发现只有名字完全相同才可以获取成功:
txt
zs
null
null而有了ConfigurationPropertySourcesPropertySource,则可以处理属性名称的变体(如驼峰式、连字符式、蛇形命名等),例如 server.port 和 serverPort 会被视为等价。
2. Binder
在 Spring 框架中,Binder 是 Spring Boot 提供的一个核心工具类,位于 org.springframework.boot.context.properties.bind 包,主要用于 将外部配置(如 application.properties、application.yml、命令行参数等)绑定到 Java 对象。它为 Spring Boot 的 @ConfigurationProperties 机制提供了底层支持,负责将 PropertySource 中的配置数据映射到 Java Bean 的字段或方法上。
例如:
java
public class ManualBinderExample {
public static void main(String[] args) {
// 创建 Environment 和 PropertySource
StandardEnvironment environment = new StandardEnvironment();
Map<String, Object> properties = new HashMap<>();
properties.put("app.name", "TestApp");
properties.put("app.port", "9090");
environment.getPropertySources().addFirst(new MapPropertySource("test", properties));
// 使用 Binder 绑定
Binder binder = Binder.get(environment);
AppConfig config = binder.bind("app", AppConfig.class).orElse(new AppConfig());
// 输出结果
System.out.println("App Name: " + config.getName()); // TestApp
System.out.println("App Port: " + config.getPort()); // 9090
}
}
class AppConfig{
private String name;
private String port;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
}