以下是关于 SpringApplicationRunListener 与 ApplicationListener / @EventListener 的完整对比说明文档,涵盖其原理、作用、生命周期差异、使用场景,并提供工业级标准代码示例,所有示例均附带详尽中文注释,完全符合你对“实际开发参考价值”和“清晰注释”的要求。
📜 Spring Boot 监听器深度对比:SpringApplicationRunListener vs ApplicationListener / @EventListener
✅ 适用版本:Spring Boot 3.x + JDK 17+
✅ 核心目标:彻底厘清两类监听器的生命周期差异、触发时机、使用边界与实战场景
一、SpringApplicationRunListener 是什么?有什么作用?
🔍 定义:
SpringApplicationRunListener 是 Spring Boot 框架内部使用的启动过程监听器接口,由 Spring Boot 在 SpringApplication.run() 方法执行的极早期阶段自动加载和调用。
它不属于 Spring 容器管理的 Bean,而是通过 Java SPI(Service Provider Interface)机制 加载的框架级事件监听器。
🎯 核心作用:
✅ 监听 Spring Boot 应用从启动到完全就绪的全过程,用于在 Spring 容器创建之前执行自定义逻辑。
它适用于那些必须在 ApplicationContext 创建前、甚至在 Environment 加载前 就要执行的操作,例如:
| 作用 | 说明 |
|---|---|
| 自定义启动日志 | 打印应用启动时间、版本、环境信息 |
| 动态加载配置 | 从远程配置中心(如 Apollo、Nacos)提前拉取配置 |
| 加密解密敏感配置 | 对 application.yml 中的加密字段进行解密 |
| 启动前健康检查 | 检查端口是否被占用、网络是否可达 |
| 集成 APM 监控探针 | 在容器初始化前注入 SkyWalking、Pinpoint 等探针 |
| 环境隔离 | 根据机器 IP 或容器标签动态切换配置源 |
⚠️ 重要:
SpringApplicationRunListener的所有方法都在 Spring 容器创建之前执行,因此:
- ❌ 不能注入
@AutowiredBean- ❌ 不能使用
@Value、@ConfigurationProperties- ✅ 只能使用 Java 原生 API、系统属性、配置文件、外部服务(如 HTTP)
- ✅ 经过本人实际测试,目前依旧依赖
META-INF/spring.factories进行注册,其他方式均不行。
注册方式 - 使用 spring.factories,可千万不要使用 @Component 或者 org.springframework.boot.autoconfigure.AutoConfiguration.imports
# src/main/resources/META-INF/spring.factories
org.springframework.boot.SpringApplicationRunListener=\
com.example.test.listener.CustomSpringApplicationRunListener
二、ApplicationListener / @EventListener 是什么?有什么作用?
🔍 定义:
ApplicationListener 是 Spring 框架提供的容器级事件监听器接口,@EventListener 是其注解形式。
它们是Spring 容器管理的 Bean,在 ApplicationContext 创建并刷新后才生效。
🎯 核心作用:
✅ 监听 Spring 容器内部发生的事件,用于业务逻辑的解耦与响应。
典型应用场景:
| 作用 | 说明 |
|---|---|
| 用户注册后发邮件 | 监听 UserRegisteredEvent |
| 订单支付成功扣库存 | 监听 OrderPaidEvent |
| 缓存预热 | 监听 ApplicationReadyEvent |
| 系统启动后初始化定时任务 | 监听 ApplicationReadyEvent |
| 优雅关闭清理资源 | 监听 ApplicationStoppingEvent |
✅ 这些监听器可以注入任何 Spring Bean(如
RedisTemplate、JdbcTemplate、RestTemplate),支持异步(@Async)、条件过滤(condition)、事务控制。
三、核心区别对比表(必须掌握)
| 维度 | SpringApplicationRunListener | ApplicationListener / @EventListener |
|---|---|---|
| 所属层级 | Spring Boot 框架层 | Spring 容器层 |
| 加载时机 | ApplicationContext 创建前 | ApplicationContext 刷新后 |
| 是否受 Spring 管理 | ❌ 否(通过 SPI 加载) | ✅ 是(@Component、@Service) |
| 能否注入 Bean | ❌ 不能(无 Spring 上下文) | ✅ 可以(@Autowired) |
| 能否使用 @Value | ❌ 不能 | ✅ 可以 |
| 是否支持 @Async | ❌ 不适用 | ✅ 支持 |
| 是否支持 condition | ❌ 不支持 | ✅ 支持(如 condition = "#event.userId > 1000") |
| 事件类型 | SpringApplicationEvent 子类 | ApplicationEvent 子类 |
| 典型事件 | ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent | ApplicationReadyEvent、ContextRefreshedEvent |
| 使用方式 | 实现接口 + META-INF/spring.factories 注册 | 实现接口或使用 @EventListener 注解 |
| 适用场景 | 启动前配置、环境初始化、安全加固 | 业务解耦、异步处理、缓存、日志、通知 |
| 调试难度 | 高(无断点、无日志上下文) | 低(标准 Spring Bean,可调试) |
✅ 一句话总结:
SpringApplicationRunListener 是“启动前的守门人”,ApplicationListener 是“启动后的协作者”。
四、SpringApplicationRunListener 事件生命周期详解
Spring Boot 启动过程中,会按顺序触发以下事件(来自 SpringApplicationRunListener):
| 事件类 | 触发时机 | 说明 |
|---|---|---|
ApplicationStartingEvent | SpringApplication.run() 第一行代码执行时 | 最早触发,此时连日志系统都未初始化 |
ApplicationEnvironmentPreparedEvent | Environment 已创建,但 ApplicationContext 尚未创建 | 可读取 application.properties,可修改 Environment |
ApplicationPreparedEvent | ApplicationContext 已创建,但 Bean 未加载 | 可修改 Bean 定义、注册自定义 BeanFactoryPostProcessor |
ContextRefreshedEvent | ApplicationContext 刷新完成,所有 Bean 加载完毕 | 注意:这是 ApplicationListener 的事件!不是 SpringApplicationRunListener 的! |
ApplicationReadyEvent | 应用完全启动,可处理请求 | ApplicationListener 监听事件 |
ApplicationFailedEvent | 启动过程中抛出异常 | 可用于发送告警 |
⚠️ 注意:
ContextRefreshedEvent、ApplicationReadyEvent是 ApplicationListener 的事件,SpringApplicationRunListener 不会监听它们!
五、标准实战示例:SpringApplicationRunListener(带详细中文注释)
✅ 场景:在 Spring 容器启动前,从远程配置中心动态加载加密密钥,用于解密数据库密码
🎯 背景:
- 数据库密码在
application.yml中是加密的:password: {AES}xxxxx - 需要在
DataSource创建前,从 Nacos 获取解密密钥 - 不能使用
@Value,因为此时 Spring 容器尚未创建
⚠️ 特别注意:如果你的项目使用了 Java 模块系统(
module-info.java),确保你的模块对spring.boot可读,并且META-INF/spring.factories仍能被正确加载。Spring Boot 3 默认支持模块化,但spring.factories机制依然有效(尽管官方推荐迁移到spring.factories的替代方案,如META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,但SpringApplicationRunListener目前仍依赖spring.factories)。经我本人测试,目前在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中注册SpringApplicationRunListener是无效的,千万要注意!!!
步骤 1:实现 SpringApplicationRunListener 接口
package com.example.demo.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义 SpringApplicationRunListener:在 Spring 容器启动前从 Nacos 获取 AES 解密密钥
* 作用:动态注入解密密钥到 Environment,供后续 Spring Boot 自动配置使用
* 注意:此监听器在 ApplicationContext 创建前执行,不能使用任何 Spring Bean!
*/
public class NacosKeyLoaderRunListener implements SpringApplicationRunListener {
private static final Logger log = LoggerFactory.getLogger(NacosKeyLoaderRunListener.class);
private final SpringApplication application; // Spring Boot 启动器实例
private final String[] args; // 启动参数
/**
* 构造函数:Spring Boot 框架自动通过 SPI 调用此构造器
* 必须有此构造器,且参数必须为 SpringApplication + String[]
*
* @param application Spring Boot 启动器对象
* @param args 命令行参数(如 --spring.profiles.active=prod)
*/
public NacosKeyLoaderRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
log.info("🔧 [SpringApplicationRunListener] 初始化:监听器已加载,但 Spring 容器尚未创建");
}
/**
* 应用启动时触发(最早触发)
* 此时日志系统可能还未初始化,建议使用 System.out 或 System.err
*/
@Override
public void starting() {
System.out.println("🚀 [SpringApplicationRunListener] 应用启动开始,此时连日志系统都未初始化");
}
/**
* Environment 准备完毕时触发
* ✅ 此处是关键:Environment 已加载 application.yml,但 ApplicationContext 未创建
* 我们可以在此修改 Environment,注入自定义属性
*/
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
System.out.println("⚙️ [SpringApplicationRunListener] Environment 已准备,开始从 Nacos 获取解密密钥...");
try {
// 模拟从 Nacos 获取密钥(真实项目中调用 Nacos SDK)
String aesKey = fetchAesKeyFromNacos();
if (aesKey != null && !aesKey.trim().isEmpty()) {
// 创建一个自定义的 PropertySource
Map<String, Object> keyMap = new HashMap<>();
keyMap.put("crypto.aes.key", aesKey); // 键名必须与解密器匹配
// 将自定义属性注入 Environment
MapPropertySource customSource = new MapPropertySource("nacos-aes-key", keyMap);
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(customSource); // 插入最前,优先级最高
System.out.println("✅ [SpringApplicationRunListener] 成功注入解密密钥到 Environment: crypto.aes.key=***");
} else {
System.err.println("❌ [SpringApplicationRunListener] 从 Nacos 获取密钥失败,将使用默认密钥(不安全)");
}
} catch (Exception e) {
System.err.println("🚨 [SpringApplicationRunListener] 从 Nacos 获取密钥异常,应用可能无法启动: " + e.getMessage());
// 不抛异常,避免阻塞启动,但会记录错误
}
}
/**
* ApplicationContext 已创建,但 Bean 尚未加载
* 可在此注册 BeanFactoryPostProcessor,修改 Bean 定义
*/
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("🏗️ [SpringApplicationRunListener] ApplicationContext 已创建,但 Bean 未加载");
}
/**
* ApplicationContext 加载完成
* 此时可访问 Bean,但框架不推荐在此做业务逻辑
*/
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("📦 [SpringApplicationRunListener] ApplicationContext 加载完成,即将刷新");
}
/**
* 应用启动成功
*/
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("🎉 [SpringApplicationRunListener] 应用启动成功,容器已就绪");
}
/**
* 应用启动失败
*/
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.err.println("💥 [SpringApplicationRunListener] 应用启动失败,异常信息:" + exception.getMessage());
// 可在此发送告警(如调用 Webhook、写文件)
}
// ==================== 模拟从 Nacos 获取密钥 ====================
/**
* 模拟从 Nacos 配置中心获取 AES 解密密钥
* 实际项目中应使用 Nacos SDK 或 HTTP 请求
*
* @return AES 密钥,或 null 表示失败
*/
private String fetchAesKeyFromNacos() {
// 模拟网络请求,实际项目中使用 NacosClient.getConfig(...)
// 为简化,此处返回固定值,生产环境应使用异步、重试、超时机制
System.out.println("🌐 [模拟] 正在从 Nacos 获取配置:dataId=crypto-key, group=DEFAULT_GROUP");
// 模拟成功响应
return "12345678901234567890123456789012"; // 32 字节 AES 密钥
// 生产示例:
// String config = nacosClient.getConfig("crypto-key", "DEFAULT_GROUP", 5000);
// return config != null ? config : null;
}
}
步骤 2:注册监听器(SPI 机制)
在 src/main/resources/META-INF/spring.factories 文件中添加:
# Spring Boot 启动监听器注册
org.springframework.boot.SpringApplicationRunListener=com.example.demo.listener.NacosKeyLoaderRunListener
✅ 关键点:
- 文件名必须为
spring.factories- 路径必须为
src/main/resources/META-INF/spring.factories- 键名必须为
org.springframework.boot.SpringApplicationRunListener- 值为全限定类名(多个用逗号分隔)
步骤 3:配套使用 Spring Boot 加密解密功能(可选)
在 application.yml 中配置加密密码:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: {AES}U2FsdGVkX1+Rl5a3d8f9J8Z7b4a6f1d2e3c4b5v6n7m8= # 加密后的密码
crypto:
aes:
key: ${crypto.aes.key:defaultKey123456789012345678901234} # 从 Environment 获取,由监听器注入
💡 Spring Boot 内置了
EnvironmentPostProcessor支持{AES}xxx解密,前提是配置了crypto.aes.key
步骤 4:启动应用,观察控制台输出
运行 main 方法,你将看到如下日志:
🚀 [SpringApplicationRunListener] 应用启动开始,此时连日志系统都未初始化
⚙️ [SpringApplicationRunListener] Environment 已准备,开始从 Nacos 获取解密密钥...
🌐 [模拟] 正在从 Nacos 获取配置:dataId=crypto-key, group=DEFAULT_GROUP
✅ [SpringApplicationRunListener] 成功注入解密密钥到 Environment: crypto.aes.key=***
🏗️ [SpringApplicationRunListener] ApplicationContext 已创建,但 Bean 未加载
📦 [SpringApplicationRunListener] ApplicationContext 加载完成,即将刷新
🎉 [SpringApplicationRunListener] 应用启动成功,容器已就绪
✅ 验证成功:
数据源能正常连接,说明{AES}xxx已被正确解密 —— 因为我们在environmentPrepared中注入了密钥!
六、ApplicationListener 示例:对比验证(业务层)
package com.example.demo.listener;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 业务层监听器:在应用完全启动后,打印数据库连接状态
* ✅ 可以注入 Spring Bean!
*/
@Component
public class DatabaseHealthListener implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger log = LoggerFactory.getLogger(DatabaseHealthListener.class);
@Resource
private DataSource dataSource; // ✅ 可以注入!Spring 容器已创建
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
try {
// ✅ 调用数据库连接测试
boolean valid = dataSource.getConnection().isValid(5);
log.info("✅ [ApplicationListener] 数据库连接正常,可用性:{}", valid);
} catch (Exception e) {
log.error("❌ [ApplicationListener] 数据库连接异常", e);
}
}
}
✅ 对比结论:
NacosKeyLoaderRunListener:必须在 DataSource 创建前注入密钥 → 用它DatabaseHealthListener:在容器启动后检查连接 → 用它
七、使用注意事项与最佳实践
| 类别 | 注意事项 |
|---|---|
| SpringApplicationRunListener | |
| ✅ 必须使用 SPI 注册 | 否则不会被加载 |
| ✅ 不能注入任何 Bean | 包括 @Autowired、@Value、@ConfigurationProperties |
| ✅ 避免耗时操作 | 会阻塞整个应用启动 |
| ✅ 异常处理要谨慎 | 抛出异常会导致启动失败,但不影响 Spring 容器(因为还没创建) |
| ✅ 日志输出建议用 System.out | 因为 SLF4J 可能未初始化 |
| ✅ 仅用于启动前配置 | 不要用于业务逻辑 |
| ApplicationListener | |
| ✅ 必须是 Spring Bean | 加 @Component 或 @Service |
| ✅ 可注入任何 Spring Bean | 数据库、缓存、HTTP 客户端均可 |
| ✅ 支持异步、条件过滤、事务 | @Async、condition、@Transactional 均可用 |
| ✅ 适用于业务解耦 | 如订单、用户、支付等事件 |
八、何时使用哪种监听器?决策树
graph TD
A[需要在 Spring 容器启动前执行?] -->|是| B[使用 SpringApplicationRunListener]
A -->|否| C[需要在容器启动后响应事件?]
C -->|是| D[使用 ApplicationListener 或 @EventListener]
B --> E[场景:加载远程配置、解密密钥、端口检查、APM 探针注入]
D --> F[场景:发邮件、缓存预热、日志记录、库存扣减、积分奖励]
✅ 记住口诀:
“启动前用 SPI,启动后用 Bean”
九、总结:开发者必须掌握的监听器选择指南
| 问题 | 推荐方案 |
|---|---|
我想在 application.yml 加载后、数据库连接前,动态修改配置 | ✅ SpringApplicationRunListener + environmentPrepared |
| 我想在应用启动完成后,初始化 Redis 缓存 | ✅ @EventListener(ApplicationReadyEvent.class) |
| 我想在用户注册后发邮件 | ✅ @EventListener(UserRegisteredEvent.class) |
| 我想在启动前检查 8080 端口是否被占用 | ✅ SpringApplicationRunListener + starting() |
我想在 /actuator/health 中暴露 Redis 状态 | ✅ HealthIndicator + @EventListener(ApplicationReadyEvent) |
我想在配置文件中使用 {AES}xxx,并从 Nacos 获取密钥 | ✅ SpringApplicationRunListener + environmentPrepared 注入密钥 |
✅ 最终建议
- 90% 的业务场景 → 使用
@EventListener - 10% 的启动前配置 → 使用
SpringApplicationRunListener - 不要混用:不要试图在
SpringApplicationRunListener中调用@Service,会报NullPointerException - 测试建议:
- 测试
ApplicationListener:使用@SpringBootTest+ApplicationEventPublisher.publishEvent() - 测试
SpringApplicationRunListener:启动真实应用,观察控制台日志
- 测试
💡 进阶建议:
若你使用 Spring Cloud Alibaba Nacos,官方已提供NacosConfigApplicationListener,你无需重复造轮子。
本示例适用于自定义配置中心、私有加密方案、企业级安全加固等特殊场景。
Spring Boot监听器对比解析


被折叠的 条评论
为什么被折叠?



