Spring Boot 项目启动时在 environmentPrepared 阶段到底干了啥?

前言

在《Spring Boot 启动流程详解》一文中详细给大家介绍了 Spring Boot 的启动流程,但是由于Spring Boot 启动时做了很多事件,有些地方没有详细给大家介绍,只是大概介绍了下做了什么,这让大家对具体的实现仍不甚了解,这其中就包括 environmentPrepared 阶段触发的事件。下面就详细给大家介绍下 environmentPrepared 阶段 Spring Boot 具体做了什么。

执行了哪些 ApplicationListener ?

首先让我看看哪些 ApplicationListener 执行了 ApplicationEnvironmentPreparedEvent 事件,通过打断点我们可以看到会执行如下这些 ApplicationListener:

注意:如果Spring Boot 如何广播事件执行的,可以先看看《Spring Boot 启动流程详解》一文。

下面让我们逐个分析每个ApplicationListener 做了什么事?

ConfigFileApplicationListener

顾名思义,该ApplicationListener可能是用来加载配置文件,由于 ConfigFileApplicationListener 内部处理操作较多,特单独写了一篇文章进行说明,详见:

ConfigFileApplicationListener 触发 ApplicationEnvironmentPreparedEvent 事件执行逻辑详解

AnsiOutputApplicationListener

该监听的主要作用是从属性配置中获取 spring.output.ansi.enabled 和 spring.output.ansi.console-available 配置并设置到 AnsiOutput 对象中,以便后续日志框架在输入日志时是否带颜色和格式。

LoggingApplicationListener

首先让我看看 LoggingApplicationListener 触发 ApplicationEnvironmentPreparedEvent 的代码:

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		if (this.loggingSystem == null) {
			this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
		}
		initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
	}

首先判断 loggingSystem 对象是否存在,不存在则创建,实际这里 loggingSystem 在触发 ApplicationStartingEvent 事件时就已经进行了创建并执行了其 beforeInitialize() 方法,源码如下:

	private void onApplicationStartingEvent(ApplicationStartingEvent event) {
		this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
		this.loggingSystem.beforeInitialize();
	}

LoggingSystem.get()为静态方法,主要是根据系统是否加载对应的class去加载响应的LoggingSystem。LoggingSystem 可通过系统属性 LoggingSystem 指定,如果没有指定,则按代码指定的顺序进行加载,加载是通过反射的方式实现的,如下所示:

	static {
		Map<String, String> systems = new LinkedHashMap<>();
		systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
		systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
				"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
		systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
		SYSTEMS = Collections.unmodifiableMap(systems);
	}

	public static LoggingSystem get(ClassLoader classLoader) {
		String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
		if (StringUtils.hasLength(loggingSystem)) {
			if (NONE.equals(loggingSystem)) {
				return new NoOpLoggingSystem();
			}
			return get(classLoader, loggingSystem);
		}
		return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
				.map((entry) -> get(classLoader, entry.getValue())).findFirst()
				.orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
	}

	private static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) {
		try {
			Class<?> systemClass = ClassUtils.forName(loggingSystemClass, classLoader);
			Constructor<?> constructor = systemClass.getDeclaredConstructor(ClassLoader.class);
			constructor.setAccessible(true);
			return (LoggingSystem) constructor.newInstance(classLoader);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}

从上面代码可以看出,日志系统的优先级是:logback >> log4j2 >> java logging。

让我们回到上面触发 ApplicationEnvironmentPreparedEvent 事件的地方,继续看看 LoggingApplicationListener 在触发该事件时执行了什么事情。

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
	//将属性配置文件中读取到的 logging.xxx 相关配置写到系统属性中
	new LoggingSystemProperties(environment).apply();
	this.logFile = LogFile.get(environment);
	if (this.logFile != null) {
		//将logFile属性写到系统属性中,主要就是 logging.file.name 和 logging.file.path
		this.logFile.applyToSystemProperties();
	}
	// 构建一个LoggerGroup
	this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
	// 设置springBootLogger日志等级
	initializeEarlyLoggingLevel(environment);
	// 初始化日志系统(日志系统的初始化是一个繁琐的过程,不是本篇文章的重点,不做详细说明,感兴趣的可以自己看看源码)
	initializeSystem(environment, this.loggingSystem, this.logFile);
	// 设置最终的日志等级
	initializeFinalLoggingLevels(environment, this.loggingSystem);
	// 根据logging.register-shutdown-hook 属性设置决定是否注册 ShutdownHook 钩子
	registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

从上面代码可以看出 initialize() 方法主要做了 LoggingSystem 的初始化。

ClasspathLoggingApplicationListener

该监听主要作用是日志级别为debug时,如果触发了 ApplicationEnvironmentPreparedEvent 或者 ApplicationFailedEvent 事件,则以debug模式打印 classpath.

BackgroundPreinitializer

该监听主要用于加速启动,在多核cpu的情况下并行加载一些资源。

DelegatingApplicationListener

从环境变量中读取 context.listener.classes 属性配置,context.listener.classes 属性值都是 ApplicationListener实现类,如果配置了,则进行事件广播,执行相应的 ApplicationEnvironmentPreparedEvent 事件动作。这里可以看做是执行自定义 ApplicationListener 的一种扩展点。

DubboHolderListener

该监听器不是Spring boot 默认的 ApplicationListener,是演示项目引用的 spring-boot-starter-dubbo-1.0.0.jar 中的监听器,且其内部没有对 ApplicationEnvironmentPreparedEvent 事件处理逻辑,这个就不过多介绍。

FileEncodingApplicationListener

该监听器主要作用是,如果发现项目启动时系统文件编码格式与设置的环境属性 spring.mandatory-file-encoding 对应的编码不一致,则抛出异常,中断启动。如果未设置 spring.mandatory-file-encoding,则不影响启动。

回顾总结

以上就是在环境准备阶段,哪些ApplicationListener执行了 ApplicationEnvironmentPreparedEvent 事件。通过上面代码分析,在environmentPrepared阶段主要执行了以下三件事,其他一些 ApplicationListener 都是无关紧要的。

  1. 通过 ConfigFileApplicationListener 加载属性配置文件并装载到 environment 对象中;
  2. 通过 LoggingApplicationListener 初始化 LoggingSystem,完成日志框架的初始化;
  3. 通过 DelegatingApplicationListener 提供额外实现 ApplicationListener 的扩展点,无需通过spring.factories文件来设置自定义的 ApplicationListener。

注:spring boot 版本为2.3.10

<think>我们参考用户的问题和提供的引用内容,用户希望在Spring项目启动时执行数据初始化代码。根据引用内容,有几种常见的方式:1.使用ApplicationRunner或CommandLineRunner接口(SpringBoot提供) 2.使用@PostConstruct注解(在Bean初始化时执行)3.使用init-method(XML配置中指定初始化方法)4.使用Spring的ApplicationContextInitializer接口5.使用SpringApplicationRunListener监听启动事件6.配置SpringBoot的数据库初始化脚本(通过application.properties配置) 用户特别提到“在Spring项目启动前进行数据初始化”,注意这里“启动前”可能指的是在应用完全启动之前,比如在Spring上下文初始化之前或过程中。根据引用[3],ApplicationContextInitializer的执行时机是在Spring上下文被刷新之前,而CommandLineRunner和ApplicationRunner是在SpringApplication.run()完成之前,但在所有SpringBeans都初始化之后执行。 另外,引用[4]提到可以通过配置spring.datasource.schema和spring.datasource.data来执行SQL脚本进行数据库初始化,但需要注意一些限制条件(如DataSourceAutoConfiguration可能会影响,以及druid防火墙等)。 因此,我们根据用户需求,提供几种常用的方法:###方法1:使用ApplicationRunner或CommandLineRunner(推荐) 这两个接口是Spring Boot提供的,在SpringBoot应用启动后执行,适合执行一些初始化数据操作。**步骤:** 1.创建一个类实现`ApplicationRunner`或`CommandLineRunner`接口。 2.实现`run`方法,在该方法中编写初始化代码。 3.将该类注册为Spring Bean(使用`@Component`等注解)。**示例代码:**```javaimportorg.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner; importorg.springframework.stereotype.Component; @Componentpublicclass DataInitializerimplements ApplicationRunner {@Overridepublicvoid run(ApplicationArguments args) throwsException {//在这里执行数据初始化System.out.println("执行数据初始化...");//例如:初始化数据库数据} }``` 或者使用`CommandLineRunner`: ```javaimport org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;@Componentpublic classDataInitializer implementsCommandLineRunner {@Overridepublicvoid run(String...args)throws Exception{//执行数据初始化}} ```###方法2:使用@PostConstruct注解在SpringBean的初始化方法上使用`@PostConstruct`注解,该方法会在Bean的依赖注入完成后执行。 **步骤:**1.在需要执行初始化的Bean中,定义一个方法。2.在该方法上添加`@PostConstruct`注解。**示例代码:** ```javaimport javax.annotation.PostConstruct;import org.springframework.stereotype.Component;@Componentpublic classDataInitializer {@PostConstructpublic voidinit(){//执行数据初始化System.out.println("使用@PostConstruct进行数据初始化");} }``` 注意:这种方式是在Bean初始化时执行,但如果有多个Bean,执行顺序可能不确定。###方法3:使用ApplicationContextInitializer如果需要在Spring上下文刷新之前执行初始化,可以使用`ApplicationContextInitializer`。**步骤:**1.创建一个类实现`ApplicationContextInitializer`接口。2.实现`initialize`方法。 3.注册该初始化器(通过META-INF/spring.factories或SpringApplication.addInitializers)。 **示例代码:**```javaimportorg.springframework.context.ApplicationContextInitializer;import org.springframework.context.ConfigurableApplicationContext; publicclass MyInitializer implementsApplicationContextInitializer<ConfigurableApplicationContext>{@Overridepublic voidinitialize(ConfigurableApplicationContextapplicationContext) {//在Spring上下文刷新之前执行System.out.println("使用ApplicationContextInitializer进行初始化");} }``` **注册方式1:在META-INF/spring.factories中添加**``` org.springframework.context.ApplicationContextInitializer=com.example.MyInitializer```**注册方式2:在启动类中通过SpringApplication添加** ```java@SpringBootApplicationpublicclass MyApp {publicstatic voidmain(String[] args) {SpringApplication app= newSpringApplication(MyApp.class);app.addInitializers(new MyInitializer());app.run(args);}} ```###方法4:使用数据库初始化脚本(引用[4])如果初始化是执行SQL脚本,可以直接在application.properties中配置:```properties#指定schema.sql(建表)和data.sql(插入数据)的位置spring.datasource.schema=classpath:database/schema.sqlspring.datasource.data=classpath:database/data.sql#设置初始化模式为always(始终执行)spring.datasource.initialization-mode=always```注意:如果使用Druid数据源,可能需要关闭防火墙或调整配置,否则可能不生效。另外,如果项目使用了JPA或Hibernate的自动建表,可能和这种方式冲突。###方法5:使用SpringApplicationRunListener(引用[3])通过实现`SpringApplicationRunListener`接口,可以监听启动过程中的各个事件,并在特定事件发生时执行代码。 **示例代码:**```javaimportorg.springframework.boot.SpringApplication;import org.springframework.boot.SpringApplicationRunListener; importorg.springframework.context.ConfigurableApplicationContext;import org.springframework.core.env.ConfigurableEnvironment; publicclass MySpringApplicationRunListener implementsSpringApplicationRunListener{//必须有的构造器public MySpringApplicationRunListener(SpringApplication application, String[] args) {}@Overridepublicvoid starting(ConfigurableBootstrapContextbootstrapContext) {System.out.println("应用开始启动");}@Overridepublic voidenvironmentPrepared(ConfigurableBootstrapContext bootstrapContext,ConfigurableEnvironment environment) {System.out.println("环境准备完成");}// ...其他方法} ```同样需要在META-INF/spring.factories中注册: ```org.springframework.boot.SpringApplicationRunListener=com.example.MySpringApplicationRunListener``` ###总结-对于简单的初始化,推荐使用`ApplicationRunner`或`CommandLineRunner`,它们使用简单且执行时机合适(在SpringBeans都初始化之后,应用完全启动之前)。 -如果需要在Bean初始化时立即执行,使用`@PostConstruct`。-如果需要在Spring上下文刷新之前执行(比如修改环境属性等),使用`ApplicationContextInitializer`。-如果是执行SQL脚本初始化数据库,使用配置文件指定脚本。-如果需要更细粒度的启动过程控制,使用`SpringApplicationRunListener`。 请根据实际需求选择合适的方式。 §§相关问题§§1. SpringBoot中ApplicationRunner和CommandLineRunner有什么区别?2.如何在Spring Boot中控制初始化代码的执行顺序?3.使用@PostConstruct注解进行初始化时,如果初始化方法抛出异常会怎样?4.Spring Boot的数据库初始化脚本执行机制是怎样的?如何避免重复执行?</think>在Spring项目中,启动时执行数据初始化有五种常用方式,根据需求选择合适的方案: ### 一、推荐方案:ApplicationRunner/CommandLineRunner **执行时机**:所有Spring Bean初始化完成后,SpringApplication.run()结束前 **实现步骤**: ```java @Component public class DataInitializer implements ApplicationRunner { @Override public void run(ApplicationArguments args) { // 执行初始化逻辑 initDatabase(); } } ``` 或使用CommandLineRunner: ```java @Component public class DataInitializer implements CommandLineRunner { @Override public void run(String... args) { // 初始化代码 } } ``` ### 二、Bean生命周期注解:@PostConstruct **执行时机**:Bean依赖注入完成后立即执行 ```java @Component public class InitService { @PostConstruct public void init() { // 初始化逻辑 } } ``` ### 三、Spring上下文初始化器:ApplicationContextInitializer **执行时机**:Spring上下文创建后,Bean加载前 ```java public class CustomInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext context) { // 初始化操作(此时Bean尚未加载) } } ``` **注册方式**(二选一): 1. `META-INF/spring.factories`中添加: ```properties org.springframework.context.ApplicationContextInitializer=com.example.CustomInitializer ``` 2. 启动类中手动添加: ```java SpringApplication app = new SpringApplication(MyApp.class); app.addInitializers(new CustomInitializer()); app.run(args); ``` ### 四、数据库脚本初始化(SQL文件) **配置application.properties**: ```properties # 指定初始化脚本 spring.sql.init.mode=always spring.sql.init.schema-locations=classpath:db/schema.sql spring.sql.init.data-locations=classpath:db/data.sql # 防止Druid等数据源拦截 spring.datasource.druid.filter.enabled=false ``` > **注意**:需禁用DataSourceAutoConfiguration或数据源过滤器 ### 五、启动事件监听:SpringApplicationRunListener **执行时机**:精确控制启动各阶段 ```java public class StartupListener implements SpringApplicationRunListener { public void environmentPrepared(ConfigurableEnvironment env) { // 环境准备阶段 } public void contextPrepared(ConfigurableApplicationContext ctx) { // 上下文准备阶段 } } ``` 注册到`META-INF/spring.factories`: ```properties org.springframework.boot.SpringApplicationRunListener=com.example.StartupListener ``` ### 方案对比表 | 方式 | 执行时机 | 适用场景 | 是否依赖Bean | |---------------------------|-------------------------|----------------------------|------------| | ApplicationRunner | Bean初始化后,启动完成前 | 常规数据初始化 | ✓ | | @PostConstruct | Bean注入后立即执行 | 单Bean初始化 | ✓ | | ApplicationContextInitializer | 上下文创建后,Bean加载前 | 环境预处理/早期配置 | ✗ | | SQL脚本初始化 | 数据源初始化时 | 纯SQL操作 | ✗ | | SpringApplicationRunListener | 精确生命周期阶段 | 复杂启动流程控制 | ✗ | > **关键建议**: > 1. 常规数据初始化首选`ApplicationRunner` > 2. 需访问环境变量用`ApplicationContextInitializer` > 3. 纯SQL操作建议用脚本初始化 > 4. 避免在`@PostConstruct`中执行耗时操作(会阻塞Bean初始化) [^1][^2][^3][^4]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值