0%

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”);
  • 初始化极早期资源(如启动计时器,统计总启动耗时);
  • 注册第三方组件的启动钩子(如监控系统的 “应用启动中” 状态上报)。
示例逻辑:
1
2
3
4
5
6
@Override
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”)。
示例逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
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)。
示例逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
@Override
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 定义(如将 singleton Bean 改为 prototype);
  • 注册自定义 BeanPostProcessor(用于 Bean 初始化前后增强);
  • 校验 Bean 定义(如检查是否存在必需的 Bean 定义,如 DataSource)。
示例逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
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”);
  • 初始化第三方组件(如注册到服务发现中心、启动消息消费者);
  • 健康检查前置准备(如检查依赖服务是否可用)。
示例逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
@Override
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”);
  • 启动后健康上报(如向监控系统发送 “应用已启动” 的心跳);
  • 启动后资源清理(如删除启动临时文件)。
示例逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
@Override
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”);
  • 启动失败后的资源清理(如关闭已创建的数据库连接、删除临时文件);
  • 失败告警(如发送邮件 / 钉钉通知给开发人员)。
示例逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
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 事件(如 ApplicationStartingEventApplicationEnvironmentPreparedEvent);
  • 通过 ApplicationEventMulticaster 广播事件,让其他 ApplicationListener 接收并处理(解耦监听器与事件处理逻辑)。

在源码中看到的 getRunListeners(args) 方法,本质就是加载 spring.factories 中配置的 SpringApplicationRunListener 实现类(默认是 EventPublishingRunListener)。

2. 加载流程:从 spring.factories 到监听器实例

SpringApplicationrun() 方法中通过以下步骤获取监听器:

  1. 调用 getRunListeners(args) → 内部调用 SpringFactoriesLoader.loadFactoryNames(SpringApplicationRunListener.class, classLoader)
  2. 读取类路径下所有 META-INF/spring.factories 文件,找到 org.springframework.boot.SpringApplicationRunListener 对应的实现类全路径;
  3. 通过反射创建监听器实例(需传入 SpringApplicationargs 参数);
  4. 返回所有监听器实例,按顺序调用各阶段方法。
示例:spring.factories 配置格式

若需自定义监听器,需在 src/main/resources/META-INF/spring.factories 中添加配置:

1
2
3
# 配置 SpringApplicationRunListener 的实现类
org.springframework.boot.SpringApplicationRunListener=\
com.zhanghe.demo.listener.CustomSpringApplicationRunListener

实战:自定义 SpringApplicationRunListener

掌握了原理后,我们通过一个完整示例,实现自定义监听器并集成到 Spring Boot 应用中。

步骤 1:实现 SpringApplicationRunListener 接口

创建 CustomSpringApplicationRunListener 类,实现关键方法(此处省略部分默认方法,仅实现核心逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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. 启动开始
@Override
public void starting() {
this.startTime = LocalDateTime.now();
System.out.println("=== 【自定义监听器】应用开始启动,时间:" + startTime);
}

// 2. 环境准备完成
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
String appName = environment.getProperty("spring.application.name", "未命名应用");
System.out.println("=== 【自定义监听器】环境准备完成,应用名称:" + appName);
}

// 3. 启动完全完成
@Override
public void running(ConfigurableApplicationContext context) {
Duration duration = Duration.between(startTime, LocalDateTime.now());
System.out.println("=== 【自定义监听器】应用启动完成,总耗时:" + duration.toMillis() + "ms");
}

// 4. 启动失败
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.err.println("=== 【自定义监听器】应用启动失败,原因:" + exception.getMessage());
}
}
关键注意点:
  • 必须提供带参构造函数:Spring 通过反射创建监听器时,会调用 (SpringApplication application, String[] args) 构造函数,缺少此构造函数会导致加载失败;
  • 方法默认实现:接口中 environmentPreparedcontextPrepared 等方法有默认实现(空逻辑),可按需重写,无需全部实现。

步骤 2:配置 spring.factories

src/main/resources 目录下创建 META-INF/spring.factories 文件,配置自定义监听器:

1
2
3
# SpringApplicationRunListener 配置
org.springframework.boot.SpringApplicationRunListener=\
com.zhanghe.demo.listener.CustomSpringApplicationRunListener

步骤 3:启动应用,验证效果

启动 Spring Boot 应用,控制台会输出自定义监听器的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== 【自定义监听器】应用开始启动,时间: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 启动流程的 “全能扩展点”,覆盖从启动开始到结束的全生命周期,核心价值是允许开发者在启动的关键节点插入全局逻辑

典型应用场景

  1. 启动日志埋点:记录启动各阶段耗时、环境信息、配置参数,便于问题排查;
  2. 环境校验与修正:检查必需配置(如数据库地址、服务注册中心地址),缺失则默认填充或报错;
  3. 第三方组件集成:启动时初始化监控系统(如 Prometheus)、服务发现(如 Nacos)、日志框架(如 Logback 自定义配置);
  4. 启动失败处理:启动出错时清理资源、发送告警,减少故障影响范围;
  5. 动态配置注入:在环境准备阶段从配置中心拉取配置,注入到 Environment

最佳实践建议

  1. 避免复杂逻辑:监听器方法执行在启动主线程,复杂逻辑(如耗时 IO)会阻塞启动,建议异步执行;
  2. 按阶段分工:不同阶段做对应事情(如环境校验在 environmentPrepared,资源清理在 failed),避免跨阶段操作;
  3. 配置显式化:自定义监听器需在 spring.factories 中明确配置,避免依赖扫描(监听器不被 @ComponentScan 扫描);
  4. 兼容上下文为空failed 方法中需判断 context 是否为 null,避免空指针异常

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10