SpringApplicationRunListener
Spring Boot SpringApplicationRunListener 详解:启动流程的 “事件监听器”
SpringApplicationRunListener 是 Spring Boot 应用启动过程中的核心扩展接口,它通过 “监听启动生命周期的关键节点”,允许开发者在应用启动的不同阶段插入自定义逻辑(如日志埋点、环境校验、资源初始化)。从 “核心作用→启动阶段映射→工作机制→自定义实战” 四个维度,系统讲解 SpringApplicationRunListener 的原理与使用,帮你掌握 Spring Boot 启动流程的扩展能力。
核心定位:启动流程的 “钩子”
SpringApplicationRunListener 本质是 “启动生命周期监听器”,它与 Spring Boot 应用的启动流程深度绑定 ——Spring Boot 在启动的每个关键节点(如 “开始启动”“环境准备完成”“上下文创建”)都会主动调用监听器的对应方法,从而触发开发者的自定义逻辑。
- 不是主动执行逻辑:监听器本身不主导启动流程,而是 “被动接收” 启动节点的回调;
- 专注启动阶段扩展:覆盖从 “启动开始” 到 “启动完成 / 失败” 的全流程,是比
CommandLineRunner/ApplicationRunner更早的扩展点(后者仅在启动末尾执行); - 全局作用域:监听器作用于整个应用的启动过程,可用于全局初始化(如加载全局配置、初始化第三方组件)。
启动阶段与方法映射:每个方法对应什么时机?
Spring Boot 应用的启动流程可拆分为 7 个关键阶段,SpringApplicationRunListener 的每个方法精准对应一个阶段。我们逐一解析每个方法的触发时机和典型用途:
1. starting():启动 “刚刚开始”(最早阶段)
触发时机:
SpringApplication.run() 方法执行的第一行代码,此时:
- 应用环境(
Environment)未创建; - Spring 上下文(
ApplicationContext)未创建; - 仅完成了
SpringApplication实例的初始化。
典型用途:
- 打印启动开始日志(如 “应用开始启动,版本:1.0.0”);
- 初始化极早期资源(如启动计时器,统计总启动耗时);
- 注册第三方组件的启动钩子(如监控系统的 “应用启动中” 状态上报)。
示例逻辑:
public void starting() {
System.out.println("=== 应用开始启动 ===");
// 记录启动开始时间
this.startTime = System.currentTimeMillis();
}
2. environmentPrepared(ConfigurableEnvironment environment):环境准备完成
触发时机:
应用环境(Environment)初始化完成后,Spring 上下文创建前。此时:
- 已加载配置文件(
application.yml/application.properties)、系统环境变量、命令行参数; Environment对象已可用,可修改其配置(如动态添加配置、覆盖现有配置);- 上下文(
ApplicationContext)尚未创建,无法操作 Bean。
典型用途:
- 校验环境配置(如检查是否存在
spring.profiles.active,不存在则默认设为dev); - 动态修改配置(如从配置中心拉取最新配置,注入到
Environment); - 打印环境信息(如 “当前激活环境:prod,端口:8080”)。
示例逻辑:
public void environmentPrepared(ConfigurableEnvironment environment) {
System.out.println("=== 环境准备完成 ===");
// 1. 检查激活环境,无则默认设为 dev
String[] activeProfiles = environment.getActiveProfiles();
if (activeProfiles.length == 0) {
environment.setActiveProfiles("dev");
System.out.println("未指定激活环境,默认设置为 dev");
}
// 2. 打印当前环境的端口配置
String port = environment.getProperty("server.port");
System.out.println("当前应用端口:" + (port == null ? "8080(默认)" : port));
}
3. contextPrepared(ConfigurableApplicationContext context):上下文 “准备完成”
触发时机:
Spring 上下文(ApplicationContext)初始化完成,但Bean 定义未加载前。此时:
- 上下文对象已创建,可修改其配置(如设置资源加载器、添加后置处理器);
- 尚未扫描
@Component/@Bean等注解,Bean 定义未注册到上下文; Environment已关联到上下文(可通过context.getEnvironment()获取)。
典型用途:
- 自定义上下文配置(如设置上下文的 ID、添加自定义
BeanFactoryPostProcessor); - 注册上下文级别的资源(如自定义资源加载路径);
- 上下文创建前的最后校验(如检查上下文类型是否为
AnnotationConfigServletWebServerApplicationContext)。
示例逻辑:
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("=== 上下文准备完成 ===");
// 设置上下文 ID(便于日志区分多上下文场景)
context.setId("demo-application-context");
System.out.println("当前上下文 ID:" + context.getId());
// 添加自定义 BeanFactoryPostProcessor(后续会影响 Bean 定义)
context.addBeanFactoryPostProcessor(beanFactory -> {
beanFactory.registerSingleton("customBean", new Object());
});
}
4. contextLoaded(ConfigurableApplicationContext context):上下文 “加载完成”
触发时机:
Spring 上下文加载完成,Bean 定义已注册,但上下文未刷新(即 Bean 未初始化)。此时:
- 已扫描并注册所有
@Component/@Bean/@Controller等 Bean 定义; @Autowired依赖注入尚未执行,Bean 实例未创建;- 可修改 Bean 定义(如动态调整 Bean 的作用域、添加属性)。
典型用途:
- 动态修改 Bean 定义(如将
singletonBean 改为prototype); - 注册自定义
BeanPostProcessor(用于 Bean 初始化前后增强); - 校验 Bean 定义(如检查是否存在必需的 Bean 定义,如
DataSource)。
示例逻辑:
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("=== 上下文加载完成 ===");
// 获取 BeanFactory,检查 Bean 定义
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 1. 检查是否存在 DataSource Bean 定义
boolean hasDataSource = beanFactory.containsBeanDefinition("dataSource");
System.out.println("是否存在 DataSource Bean 定义:" + hasDataSource);
// 2. 动态修改 Bean 作用域(如将 userService 改为 prototype)
if (beanFactory.containsBeanDefinition("userService")) {
beanFactory.getBeanDefinition("userService").setScope(BeanDefinition.SCOPE_PROTOTYPE);
System.out.println("已将 userService Bean 作用域改为 prototype");
}
}
5. started(ConfigurableApplicationContext context):上下文 “刷新完成”
触发时机:
Spring 上下文已刷新(Bean 初始化完成、依赖注入完成),但 CommandLineRunner/ApplicationRunner 未执行 。此时:
- 所有单例 Bean 已创建并初始化(
@PostConstruct方法已执行); - 嵌入式容器(如 Tomcat)已启动,应用端口已监听;
- 应用已具备对外提供服务的能力,但启动流程尚未完全结束。
典型用途:
- 打印启动关键信息(如 “上下文刷新完成,耗时:1200ms”);
- 初始化第三方组件(如注册到服务发现中心、启动消息消费者);
- 健康检查前置准备(如检查依赖服务是否可用)。
示例逻辑:
public void started(ConfigurableApplicationContext context) {
System.out.println("=== 上下文刷新完成 ===");
// 计算上下文刷新耗时(从 starting 阶段记录的时间开始)
long costTime = System.currentTimeMillis() - this.startTime;
System.out.println("上下文刷新耗时:" + costTime + "ms");
// 模拟注册到服务发现中心(如 Nacos/Eureka)
String appName = context.getEnvironment().getProperty("spring.application.name", "demo-app");
String port = context.getEnvironment().getProperty("server.port", "8080");
System.out.println("将应用 " + appName + "(端口:" + port + ")注册到服务发现中心");
}
6. running(ConfigurableApplicationContext context):启动 “完全完成”
触发时机:
SpringApplication.run() 方法执行的最后阶段,此时:
- 所有
CommandLineRunner/ApplicationRunner已执行; - 应用完全启动,正常对外提供服务;
- 无后续启动步骤,监听器调用完成后,
run()方法即返回。
典型用途:
- 打印启动完成日志(如 “应用启动成功,访问地址:http://localhost:8080”);
- 启动后健康上报(如向监控系统发送 “应用已启动” 的心跳);
- 启动后资源清理(如删除启动临时文件)。
示例逻辑:
public void running(ConfigurableApplicationContext context) {
System.out.println("=== 应用启动完全完成 ===");
// 打印应用访问地址
String contextPath = context.getEnvironment().getProperty("server.servlet.context-path", "");
String port = context.getEnvironment().getProperty("server.port", "8080");
String accessUrl = "http://localhost:" + port + contextPath;
System.out.println("应用访问地址:" + accessUrl);
// 向监控系统上报启动状态
System.out.println("向监控系统上报:应用 " + context.getId() + " 已启动");
}
7. failed(ConfigurableApplicationContext context, Throwable exception):启动 “失败”
触发时机:
应用启动过程中抛出异常(如配置错误、Bean 初始化失败),此时:
- 若上下文已创建(如
contextPrepared后失败),则context不为 null; - 若上下文未创建(如
starting/environmentPrepared阶段失败),则context为 null; exception参数包含启动失败的具体异常信息。
典型用途:
- 打印错误日志(如 “应用启动失败,原因:XXX”);
- 启动失败后的资源清理(如关闭已创建的数据库连接、删除临时文件);
- 失败告警(如发送邮件 / 钉钉通知给开发人员)。
示例逻辑:
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.err.println("=== 应用启动失败 ===");
// 打印失败原因
System.err.println("失败原因:" + exception.getMessage());
exception.printStackTrace();
// 清理资源(如关闭上下文)
if (context != null && context.isActive()) {
context.close();
System.err.println("已关闭 Spring 上下文");
}
// 发送失败告警(模拟)
System.err.println("已发送启动失败告警至钉钉群");
}
工作机制:Spring Boot 如何发现并调用监听器?
Spring Boot 不会主动扫描 SpringApplicationRunListener 的实现类,而是通过 Spring Factories Loader 机制 加载配置在 META-INF/spring.factories 文件中的监听器。核心流程如下:
1. 默认监听器:EventPublishingRunListener
Spring Boot 内置了一个默认的 SpringApplicationRunListener 实现 ——org.springframework.boot.context.event.EventPublishingRunListener,它的核心作用是:
- 将启动阶段的回调转换为 Spring 事件(如
ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent); - 通过
ApplicationEventMulticaster广播事件,让其他ApplicationListener接收并处理(解耦监听器与事件处理逻辑)。
在源码中看到的 getRunListeners(args) 方法,本质就是加载 spring.factories 中配置的 SpringApplicationRunListener 实现类(默认是 EventPublishingRunListener)。
2. 加载流程:从 spring.factories 到监听器实例
SpringApplication 在 run() 方法中通过以下步骤获取监听器:
- 调用
getRunListeners(args)→ 内部调用SpringFactoriesLoader.loadFactoryNames(SpringApplicationRunListener.class, classLoader); - 读取类路径下所有
META-INF/spring.factories文件,找到org.springframework.boot.SpringApplicationRunListener对应的实现类全路径; - 通过反射创建监听器实例(需传入
SpringApplication和args参数); - 返回所有监听器实例,按顺序调用各阶段方法。
示例:spring.factories 配置格式
若需自定义监听器,需在 src/main/resources/META-INF/spring.factories 中添加配置:
# 配置 SpringApplicationRunListener 的实现类
org.springframework.boot.SpringApplicationRunListener=\
com.zhanghe.demo.listener.CustomSpringApplicationRunListener
实战:自定义 SpringApplicationRunListener
掌握了原理后,我们通过一个完整示例,实现自定义监听器并集成到 Spring Boot 应用中。
步骤 1:实现 SpringApplicationRunListener 接口
创建 CustomSpringApplicationRunListener 类,实现关键方法(此处省略部分默认方法,仅实现核心逻辑):
package com.zhanghe.demo.listener;
import org.springframework.boot.ConfigurableSpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import java.time.Duration;
import java.time.LocalDateTime;
// 注意:必须提供带参构造函数(Spring 会通过反射调用此构造函数)
public class CustomSpringApplicationRunListener implements SpringApplicationRunListener {
private final ConfigurableSpringApplication application;
private final String[] args;
private LocalDateTime startTime;
// 核心:Spring 要求的构造函数(参数为 SpringApplication 和 args)
public CustomSpringApplicationRunListener(ConfigurableSpringApplication application, String[] args) {
this.application = application;
this.args = args;
}
// 1. 启动开始
public void starting() {
this.startTime = LocalDateTime.now();
System.out.println("=== 【自定义监听器】应用开始启动,时间:" + startTime);
}
// 2. 环境准备完成
public void environmentPrepared(ConfigurableEnvironment environment) {
String appName = environment.getProperty("spring.application.name", "未命名应用");
System.out.println("=== 【自定义监听器】环境准备完成,应用名称:" + appName);
}
// 3. 启动完全完成
public void running(ConfigurableApplicationContext context) {
Duration duration = Duration.between(startTime, LocalDateTime.now());
System.out.println("=== 【自定义监听器】应用启动完成,总耗时:" + duration.toMillis() + "ms");
}
// 4. 启动失败
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.err.println("=== 【自定义监听器】应用启动失败,原因:" + exception.getMessage());
}
}
关键注意点:
- 必须提供带参构造函数:Spring 通过反射创建监听器时,会调用
(SpringApplication application, String[] args)构造函数,缺少此构造函数会导致加载失败; - 方法默认实现:接口中
environmentPrepared、contextPrepared等方法有默认实现(空逻辑),可按需重写,无需全部实现。
步骤 2:配置 spring.factories
在 src/main/resources 目录下创建 META-INF/spring.factories 文件,配置自定义监听器:
# SpringApplicationRunListener 配置
org.springframework.boot.SpringApplicationRunListener=\
com.zhanghe.demo.listener.CustomSpringApplicationRunListener
步骤 3:启动应用,验证效果
启动 Spring Boot 应用,控制台会输出自定义监听器的日志:
=== 【自定义监听器】应用开始启动,时间:2024-05-20T15:30:00
=== 【自定义监听器】环境准备完成,应用名称:demo-app
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.10)
2024-05-20 15:30:01.234 INFO 1234 --- [ main] com.zhanghe.demo.DemoApplication : Starting DemoApplication using Java 1.8.0_301 on localhost with PID 1234
...(中间省略 Spring 启动日志)...
=== 【自定义监听器】应用启动完成,总耗时:1500ms
2024-05-20 15:30:01.734 INFO 1234 --- [ main] com.zhanghe.demo.DemoApplication : Started DemoApplication in 1.734 seconds (JVM running for 2.123)
与其他扩展点的区别(如 ApplicationListener、Runner)
为避免混淆,我们对比 SpringApplicationRunListener 与其他常见扩展点的差异:
| 扩展点 | 核心作用 | 触发时机 | 适用场景 |
|---|---|---|---|
| SpringApplicationRunListener | 监听启动全流程,触发自定义逻辑 | 从启动开始到完成 / 失败的全阶段 | 全局启动日志、环境校验、第三方组件初始化 |
| ApplicationListener | 监听 Spring 事件(如启动事件、上下文事件) | 依赖事件触发(如 ApplicationStartingEvent) |
事件驱动的逻辑(如监听上下文刷新事件) |
| CommandLineRunner/ApplicationRunner | 启动末尾执行初始化逻辑 | 上下文刷新完成后,应用对外服务前 | 业务数据初始化、缓存加载 |
关键区别:
SpringApplicationRunListener是 “启动流程的直接钩子”,与启动阶段强绑定;ApplicationListener是 “事件驱动的观察者”,需依赖EventPublishingRunListener广播的事件;Runner仅在启动末尾执行,无法干预早期阶段(如环境准备、上下文创建)。
总结与应用场景
SpringApplicationRunListener 是 Spring Boot 启动流程的 “全能扩展点”,覆盖从启动开始到结束的全生命周期,核心价值是允许开发者在启动的关键节点插入全局逻辑。
典型应用场景
- 启动日志埋点:记录启动各阶段耗时、环境信息、配置参数,便于问题排查;
- 环境校验与修正:检查必需配置(如数据库地址、服务注册中心地址),缺失则默认填充或报错;
- 第三方组件集成:启动时初始化监控系统(如 Prometheus)、服务发现(如 Nacos)、日志框架(如 Logback 自定义配置);
- 启动失败处理:启动出错时清理资源、发送告警,减少故障影响范围;
- 动态配置注入:在环境准备阶段从配置中心拉取配置,注入到
Environment。
最佳实践建议
- 避免复杂逻辑:监听器方法执行在启动主线程,复杂逻辑(如耗时 IO)会阻塞启动,建议异步执行;
- 按阶段分工:不同阶段做对应事情(如环境校验在
environmentPrepared,资源清理在failed),避免跨阶段操作; - 配置显式化:自定义监听器需在
spring.factories中明确配置,避免依赖扫描(监听器不被@ComponentScan扫描); - 兼容上下文为空:
failed方法中需判断context是否为 null,避免空指针异常