Spring Boot 监听器深度对比:SpringApplicationRunListener vs ApplicationListener / @EventListener

Spring Boot监听器对比解析

以下是关于 SpringApplicationRunListenerApplicationListener / @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 容器创建之前执行,因此:

  • ❌ 不能注入 @Autowired Bean
  • ❌ 不能使用 @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(如 RedisTemplateJdbcTemplateRestTemplate),支持异步(@Async)、条件过滤(condition)、事务控制。


三、核心区别对比表(必须掌握)

维度SpringApplicationRunListenerApplicationListener / @EventListener
所属层级Spring Boot 框架层Spring 容器层
加载时机ApplicationContext 创建前ApplicationContext 刷新后
是否受 Spring 管理❌ 否(通过 SPI 加载)✅ 是(@Component、@Service)
能否注入 Bean❌ 不能(无 Spring 上下文)✅ 可以(@Autowired)
能否使用 @Value❌ 不能✅ 可以
是否支持 @Async❌ 不适用✅ 支持
是否支持 condition❌ 不支持✅ 支持(如 condition = "#event.userId > 1000"
事件类型SpringApplicationEvent 子类ApplicationEvent 子类
典型事件ApplicationStartingEventApplicationEnvironmentPreparedEventApplicationReadyEventContextRefreshedEvent
使用方式实现接口 + META-INF/spring.factories 注册实现接口或使用 @EventListener 注解
适用场景启动前配置、环境初始化、安全加固业务解耦、异步处理、缓存、日志、通知
调试难度高(无断点、无日志上下文)低(标准 Spring Bean,可调试)

一句话总结
SpringApplicationRunListener 是“启动前的守门人”,ApplicationListener 是“启动后的协作者”。


四、SpringApplicationRunListener 事件生命周期详解

Spring Boot 启动过程中,会按顺序触发以下事件(来自 SpringApplicationRunListener):

事件类触发时机说明
ApplicationStartingEventSpringApplication.run() 第一行代码执行时最早触发,此时连日志系统都未初始化
ApplicationEnvironmentPreparedEventEnvironment 已创建,但 ApplicationContext 尚未创建可读取 application.properties,可修改 Environment
ApplicationPreparedEventApplicationContext 已创建,但 Bean 未加载可修改 Bean 定义、注册自定义 BeanFactoryPostProcessor
ContextRefreshedEventApplicationContext 刷新完成,所有 Bean 加载完毕注意:这是 ApplicationListener 的事件!不是 SpringApplicationRunListener 的!
ApplicationReadyEvent应用完全启动,可处理请求ApplicationListener 监听事件
ApplicationFailedEvent启动过程中抛出异常可用于发送告警

⚠️ 注意:ContextRefreshedEventApplicationReadyEventApplicationListener 的事件,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 客户端均可
支持异步、条件过滤、事务@Asynccondition@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,你无需重复造轮子。
本示例适用于自定义配置中心私有加密方案企业级安全加固等特殊场景。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值