Spring Boot 钩子全集实战(二):SpringApplicationRunListener.starting() 详解
在上一篇中,我们介绍了如何通过 addInitializers、addListeners 等方式在启动前注入逻辑。今天,我们将聚焦于 Spring Boot 启动流程中最早触发的扩展点——SpringApplicationRunListener.starting(),并重点讲解它在真实生产系统中的典型应用。
一、什么是 starting()?
starting() 是 SpringApplicationRunListener 接口的第一个回调方法,在以下条件下被调用:
- 日志系统已初始化(可安全输出日志);
- 但 Environment 尚未加载(
application.yml还没读); - ApplicationContext 尚未创建(Spring 容器完全不存在)。
✅ 这意味着:这是整个应用生命周期中你能介入的“最早时刻”。
在生产环境中,这个“黄金窗口期”常被用于执行与业务无关但对系统稳定性至关重要的底层操作。
二、场景 1:全链路启动耗时监控(定位启动性能瓶颈)
业务痛点
生产环境中,应用启动慢是高频问题,但常规日志只能看到总耗时,无法定位 “哪个阶段拖慢了启动”(比如环境加载慢、Bean 初始化慢、Runner 执行慢),排查效率极低。
解决方案
基于SpringApplicationRunListener记录每个启动阶段的耗时,输出结构化监控日志,精准定位瓶颈。
实现代码
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/** * 生产级启动耗时监控监听器 */
public class StartupTimeMonitorListener implements SpringApplicationRunListener {
// 记录各阶段开始时间
private final Map<String, LocalDateTime> stageStartTime = new HashMap<>();
private final SpringApplication application;
private final String[] args;
// 必须的构造方法
public StartupTimeMonitorListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
// 记录启动开始时间
stageStartTime.put("starting", LocalDateTime.now());
logStage("启动开始", "应用启动流程初始化");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
recordStageCost("starting", "environmentPrepared");
logStage("环境准备完成", "配置文件/环境变量加载完毕,当前环境:" +
String.join(",", environment.getActiveProfiles()));
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
recordStageCost("environmentPrepared", "contextPrepared");
logStage("上下文准备完成", "ApplicationContext创建完毕,未加载Bean定义");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
recordStageCost("contextPrepared", "contextLoaded");
logStage("上下文加载完成", "Bean定义已注册,等待上下文刷新");
}
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
recordStageCost("contextLoaded", "started");
logStage("应用启动完成", "上下文刷新完毕,Runner未执行,累计耗时:" + timeTaken.toMillis() + "ms");
}
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
recordStageCost("started", "ready");
logStage("应用完全就绪", "所有Runner执行完毕,可对外提供服务,总耗时:" + timeTaken.toMillis() + "ms");
// 输出全链路耗时汇总(生产环境可上报到监控平台如Prometheus/Grafana)
System.out.println("=== 启动阶段耗时汇总 ===");
stageStartTime.forEach((stage, time) -> {
long cost = Duration.between(time, LocalDateTime.now()).toMillis();
System.out.println(stage + " 阶段累计耗时:" + cost + "ms");
});
}
// 记录两个阶段之间的耗时
private void recordStageCost(String prevStage, String currentStage) {
LocalDateTime prevTime = stageStartTime.get(prevStage);
if (prevTime != null) {
long cost = Duration.between(prevTime, LocalDateTime.now()).toMillis();
stageStartTime.put(currentStage, LocalDateTime.now());
System.out.println(prevStage + " -> " + currentStage + " 耗时:" + cost + "ms");
}
}
// 结构化日志输出(生产环境建议用SLF4J)
private void logStage(String stage, String desc) {
System.out.printf("[%s] %s - %s%n", LocalDateTime.now(), stage, desc);
}
}
配置加载
在resources/META-INF/spring.factories中配置:
org.springframework.boot.SpringApplicationRunListener=\
com.example.demo.listener.StartupTimeMonitorListener
输出
/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:54168,suspend=y,server=n -javaagent:/Users/wangmingfei/Library/Caches/JetBrains/IdeaIC2024.3/captureAgent/debugger-agent.jar -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/wangmingfei/Documents/个人/05 java天梯之路/01 源码/03 每日打卡系列/daily-check-in/springboot钩子/demo/target/classes:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-web/3.5.8/spring-boot-starter-web-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter/3.5.8/spring-boot-starter-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot/3.5.8/spring-boot-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-autoconfigure/3.5.8/spring-boot-autoconfigure-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-logging/3.5.8/spring-boot-starter-logging-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-classic/1.5.21/logback-classic-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-core/1.5.21/logback-core-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-to-slf4j/2.24.3/log4j-to-slf4j-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-api/2.24.3/log4j-api-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/jul-to-slf4j/2.0.17/jul-to-slf4j-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/usr/local/apache-maven-3.9.9/repository/org/yaml/snakeyaml/2.4/snakeyaml-2.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-json/3.5.8/spring-boot-starter-json-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-databind/2.19.4/jackson-databind-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-annotations/2.19.4/jackson-annotations-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-core/2.19.4/jackson-core-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.19.4/jackson-datatype-jdk8-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.19.4/jackson-datatype-jsr310-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.19.4/jackson-module-parameter-names-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-tomcat/3.5.8/spring-boot-starter-tomcat-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.49/tomcat-embed-core-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.49/tomcat-embed-el-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.49/tomcat-embed-websocket-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-web/6.2.14/spring-web-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-beans/6.2.14/spring-beans-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-observation/1.15.6/micrometer-observation-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-commons/1.15.6/micrometer-commons-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-webmvc/6.2.14/spring-webmvc-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-aop/6.2.14/spring-aop-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-context/6.2.14/spring-context-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-expression/6.2.14/spring-expression-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-core/6.2.14/spring-core-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-jcl/6.2.14/spring-jcl-6.2.14.jar:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar com.example.demo.DemoApplication
已连接到地址为 ''127.0.0.1:54168',传输: '套接字'' 的目标虚拟机
[2025-12-10T14:56:40.087923] 启动开始 - 应用启动流程初始化
starting -> environmentPrepared 耗时:128ms
[2025-12-10T14:56:40.217733] 环境准备完成 - 配置文件/环境变量加载完毕,当前环境:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.5.8)
environmentPrepared -> contextPrepared 耗时:31ms
[2025-12-10T14:56:40.247800] 上下文准备完成 - ApplicationContext创建完毕,未加载Bean定义
2025-12-10T14:56:40.249+08:00 INFO 7823 --- [demo] [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 21.0.9 with PID 7823 (/Users/wangmingfei/Documents/个人/05 java天梯之路/01 源码/03 每日打卡系列/daily-check-in/springboot钩子/demo/target/classes started by wangmingfei in /Users/wangmingfei/Documents/个人/05 java天梯之路/01 源码/03 每日打卡系列/daily-check-in/springboot钩子/demo)
2025-12-10T14:56:40.250+08:00 INFO 7823 --- [demo] [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
contextPrepared -> contextLoaded 耗时:21ms
[2025-12-10T14:56:40.269456] 上下文加载完成 - Bean定义已注册,等待上下文刷新
2025-12-10T14:56:40.544+08:00 INFO 7823 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2025-12-10T14:56:40.550+08:00 INFO 7823 --- [demo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2025-12-10T14:56:40.550+08:00 INFO 7823 --- [demo] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.49]
2025-12-10T14:56:40.569+08:00 INFO 7823 --- [demo] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2025-12-10T14:56:40.570+08:00 INFO 7823 --- [demo] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 301 ms
2025-12-10T14:56:40.704+08:00 INFO 7823 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2025-12-10T14:56:40.710+08:00 INFO 7823 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.642 seconds (process running for 0.786)
contextLoaded -> started 耗时:441ms
[2025-12-10T14:56:40.711450] 应用启动完成 - 上下文刷新完毕,Runner未执行,累计耗时:642ms
started -> ready 耗时:0ms
[2025-12-10T14:56:40.712131] 应用完全就绪 - 所有Runner执行完毕,可对外提供服务,总耗时:643ms
=== 启动阶段耗时汇总 ===
ready 阶段累计耗时:0ms
environmentPrepared 阶段累计耗时:496ms
started 阶段累计耗时:1ms
starting 阶段累计耗时:625ms
contextPrepared 阶段累计耗时:465ms
contextLoaded 阶段累计耗时:443ms
生产价值
- 精准定位启动瓶颈(比如发现
environmentPrepared阶段耗时久,可排查配置中心拉取配置慢的问题); - 输出结构化耗时日志,可接入 ELK 进行可视化分析;
- 为启动性能优化提供数据支撑(比如优化 Bean 初始化、减少配置加载项)。
三、场景 2:启动失败全链路告警(及时止损)
业务痛点
生产环境中应用启动失败若不能及时发现,会导致服务不可用且排查滞后。常规日志告警无法覆盖 “上下文未创建就失败” 的场景,告警不全面。
解决方案
基于failed方法实现全场景启动失败告警,结合企业微信 / 钉钉机器人推送告警信息,包含失败阶段、异常栈、机器信息等关键内容。
核心实现
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
public class StartupFailerMonitorListener implements SpringApplicationRunListener {
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
// 1. 收集核心告警信息
String alertContent = buildAlertContent(context, exception);
// 2. 推送告警(生产环境建议异步推送)
try {
System.out.println(String.format("{%s}-{%s}", "【生产环境应用启动失败】", alertContent));
} catch (Exception e) {
System.err.println("告警推送失败:" + e.getMessage());
}
// 3. 释放已初始化的资源(避免资源泄漏)
releaseResources(context);
}
// 构建告警内容(包含生产环境关键信息)
private String buildAlertContent(ConfigurableApplicationContext context, Throwable exception) {
StringBuilder sb = new StringBuilder();
sb.append("机器IP:").append("127.0.0.1").append("\n");
sb.append("应用名称:").append("测试").append("\n");
sb.append("失败阶段:").append(getFailedStage(context)).append("\n");
sb.append("异常信息:").append(exception.getMessage()).append("\n");
sb.append("上下文状态:").append(context == null ? "未创建" : "已创建但刷新失败").append("\n");
return sb.toString();
}
// 释放资源(比如关闭已创建的数据库连接、缓存客户端)
private void releaseResources(ConfigurableApplicationContext context) {
if (context != null && context.isActive()) {
try {
// 关闭自定义资源池
System.out.println("关闭资源成功");
} catch (Exception e) {
System.err.println("资源释放失败:" + e.getMessage());
}
}
}
private String getFailedStage(ConfigurableApplicationContext context) {
return "启动阶段";
}
}
构造失败场景:
spring:
application:
name: demo
# 故意添加非法语法
xxxxx
输出
/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:54261,suspend=y,server=n -javaagent:/Users/wangmingfei/Library/Caches/JetBrains/IdeaIC2024.3/captureAgent/debugger-agent.jar -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/wangmingfei/Documents/个人/05 java天梯之路/01 源码/03 每日打卡系列/daily-check-in/springboot钩子/demo/target/classes:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-web/3.5.8/spring-boot-starter-web-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter/3.5.8/spring-boot-starter-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot/3.5.8/spring-boot-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-autoconfigure/3.5.8/spring-boot-autoconfigure-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-logging/3.5.8/spring-boot-starter-logging-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-classic/1.5.21/logback-classic-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-core/1.5.21/logback-core-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-to-slf4j/2.24.3/log4j-to-slf4j-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-api/2.24.3/log4j-api-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/jul-to-slf4j/2.0.17/jul-to-slf4j-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/usr/local/apache-maven-3.9.9/repository/org/yaml/snakeyaml/2.4/snakeyaml-2.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-json/3.5.8/spring-boot-starter-json-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-databind/2.19.4/jackson-databind-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-annotations/2.19.4/jackson-annotations-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-core/2.19.4/jackson-core-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.19.4/jackson-datatype-jdk8-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.19.4/jackson-datatype-jsr310-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.19.4/jackson-module-parameter-names-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-tomcat/3.5.8/spring-boot-starter-tomcat-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.49/tomcat-embed-core-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.49/tomcat-embed-el-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.49/tomcat-embed-websocket-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-web/6.2.14/spring-web-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-beans/6.2.14/spring-beans-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-observation/1.15.6/micrometer-observation-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-commons/1.15.6/micrometer-commons-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-webmvc/6.2.14/spring-webmvc-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-aop/6.2.14/spring-aop-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-context/6.2.14/spring-context-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-expression/6.2.14/spring-expression-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-core/6.2.14/spring-core-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-jcl/6.2.14/spring-jcl-6.2.14.jar:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar com.example.demo.DemoApplication
已连接到地址为 ''127.0.0.1:54261',传输: '套接字'' 的目标虚拟机
[2025-12-10T14:58:32.464881] 启动开始 - 应用启动流程初始化
{【生产环境应用启动失败】}-{机器IP:127.0.0.1
应用名称:测试
失败阶段:启动阶段
异常信息:while scanning a simple key
in 'reader', line 5, column 1:
xxxxx
^
could not find expected ':'
in 'reader', line 5, column 6:
xxxxx
^
上下文状态:未创建
}
14:58:32.552 [main] ERROR org.springframework.boot.SpringApplication -- Application run failed
org.yaml.snakeyaml.scanner.ScannerException: while scanning a simple key
in 'reader', line 5, column 1:
xxxxx
^
could not find expected ':'
in 'reader', line 5, column 6:
xxxxx
^
生产价值
- 覆盖所有启动失败场景(包括上下文未创建的早期失败);
- 告警信息包含机器 IP、异常栈等生产排查关键信息,缩短定位时间;
- 自动释放已初始化资源,避免数据库连接、线程池等资源泄漏。
四、场景 3:环境配置校验(提前拦截非法配置)
业务痛点
生产环境中常因配置错误(比如数据库地址写错、缺少核心环境变量)导致应用启动后不可用,问题发现滞后。
解决方案
在environmentPrepared阶段校验核心配置,若配置不合法直接抛出异常,终止启动流程,避免 “启动成功但服务不可用” 的无效状态。
核心实现
package com.example.demo.listener;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.core.env.ConfigurableEnvironment;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.UUID;
public class StartupEnvConfigValidListener implements SpringApplicationRunListener {
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
// 1. 校验核心配置项(生产环境可配置化校验规则)
validateCoreConfig(environment, "spring.datasource.url");
validateCoreConfig(environment, "redis.host");
validateCoreConfig(environment, "app.prod.mode");
// 2. 校验环境一致性(比如生产环境禁止使用test profile)
validateProfile(environment);
// 3. 补充生产环境必要配置(比如动态注入机器IP到配置中)
try {
supplementProdConfig(environment);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
// 校验核心配置是否存在且合法
private void validateCoreConfig(ConfigurableEnvironment environment, String configKey) {
String value = environment.getProperty(configKey);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("生产环境核心配置缺失:" + configKey);
}
// 自定义规则校验(比如数据库URL格式)
if (configKey.equals("spring.datasource.url") && !value.startsWith("jdbc:mysql://")) {
throw new IllegalArgumentException("生产环境数据库URL格式非法:" + value);
}
}
// 校验Profile合法性(生产环境禁止test/dev)
private void validateProfile(ConfigurableEnvironment environment) {
String[] activeProfiles = environment.getActiveProfiles();
for (String profile : activeProfiles) {
if ("test".equals(profile) || "dev".equals(profile)) {
throw new IllegalStateException("生产环境禁止激活test/dev Profile:" + profile);
}
}
}
// 补充生产配置(比如注入机器IP、应用实例ID)
private void supplementProdConfig(ConfigurableEnvironment environment) throws UnknownHostException {
environment.getSystemProperties().put("app.server.ip", InetAddress.getLocalHost());
environment.getSystemProperties().put("app.instance.id", UUID.randomUUID().toString().substring(0, 8));
}
}
输出
/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:54360,suspend=y,server=n -javaagent:/Users/wangmingfei/Library/Caches/JetBrains/IdeaIC2024.3/captureAgent/debugger-agent.jar -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/wangmingfei/Documents/个人/05 java天梯之路/01 源码/03 每日打卡系列/daily-check-in/springboot钩子/demo/target/classes:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-web/3.5.8/spring-boot-starter-web-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter/3.5.8/spring-boot-starter-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot/3.5.8/spring-boot-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-autoconfigure/3.5.8/spring-boot-autoconfigure-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-logging/3.5.8/spring-boot-starter-logging-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-classic/1.5.21/logback-classic-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-core/1.5.21/logback-core-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-to-slf4j/2.24.3/log4j-to-slf4j-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-api/2.24.3/log4j-api-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/jul-to-slf4j/2.0.17/jul-to-slf4j-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/usr/local/apache-maven-3.9.9/repository/org/yaml/snakeyaml/2.4/snakeyaml-2.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-json/3.5.8/spring-boot-starter-json-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-databind/2.19.4/jackson-databind-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-annotations/2.19.4/jackson-annotations-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-core/2.19.4/jackson-core-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.19.4/jackson-datatype-jdk8-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.19.4/jackson-datatype-jsr310-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.19.4/jackson-module-parameter-names-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-tomcat/3.5.8/spring-boot-starter-tomcat-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.49/tomcat-embed-core-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.49/tomcat-embed-el-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.49/tomcat-embed-websocket-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-web/6.2.14/spring-web-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-beans/6.2.14/spring-beans-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-observation/1.15.6/micrometer-observation-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-commons/1.15.6/micrometer-commons-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-webmvc/6.2.14/spring-webmvc-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-aop/6.2.14/spring-aop-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-context/6.2.14/spring-context-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-expression/6.2.14/spring-expression-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-core/6.2.14/spring-core-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-jcl/6.2.14/spring-jcl-6.2.14.jar:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar com.example.demo.DemoApplication
已连接到地址为 ''127.0.0.1:54360',传输: '套接字'' 的目标虚拟机
[2025-12-10T15:01:17.628507] 启动开始 - 应用启动流程初始化
starting -> environmentPrepared 耗时:131ms
[2025-12-10T15:01:17.761066] 环境准备完成 - 配置文件/环境变量加载完毕,当前环境:
2025-12-10T15:01:17.867+08:00 ERROR 7888 --- [demo] [ main] o.s.boot.SpringApplication : Application run failed
java.lang.IllegalArgumentException: 生产环境核心配置缺失:spring.datasource.url
at com.example.demo.listener.StartupEnvConfigValidListener.validateCoreConfig(StartupEnvConfigValidListener.java:34) ~[classes/:na]
at com.example.demo.listener.StartupEnvConfigValidListener.environmentPrepared(StartupEnvConfigValidListener.java:15) ~[classes/:na]
at org.springframework.boot.SpringApplicationRunListeners.lambda$environmentPrepared$2(SpringApplicationRunListeners.java:64) ~[spring-boot-3.5.8.jar:3.5.8]
at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:118) ~[spring-boot-3.5.8.jar:3.5.8]
at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:112) ~[spring-boot-3.5.8.jar:3.5.8]
at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:63) ~[spring-boot-3.5.8.jar:3.5.8]
at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:353) ~[spring-boot-3.5.8.jar:3.5.8]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:313) ~[spring-boot-3.5.8.jar:3.5.8]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) ~[spring-boot-3.5.8.jar:3.5.8]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) ~[spring-boot-3.5.8.jar:3.5.8]
at com.example.demo.DemoApplication.main(DemoApplication.java:11) ~[classes/:na]
已与地址为 ''127.0.0.1:54360',传输: '套接字'' 的目标虚拟机断开连接
进程已结束,退出代码为 1
生产价值
- 提前拦截非法配置,避免 “启动成功但服务不可用” 的无效状态;
- 统一校验规则,避免因配置问题导致的生产故障;
- 动态补充生产环境必要配置,减少配置文件维护成本。
五、场景 4:启动前初始化敏感资源(安全管控)
业务痛点
生产环境中部分敏感资源(比如加密密钥、证书文件)需要在应用启动早期加载,且需做权限校验、防篡改校验。
解决方案
在starting阶段加载敏感资源,做前置校验,确保资源合法后再继续启动流程。
package com.example.demo.listener;
import cn.hutool.core.io.FileUtil;
import cn.hutool.crypto.digest.MD5;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplicationRunListener;
import java.io.File;
import java.time.LocalDateTime;
public class StartupSensResPreloaderListener implements SpringApplicationRunListener {
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
logStage("敏感资源初始化", "开始加载生产环境加密证书");
// 1. 加载证书文件(从安全目录读取,非classpath)
File certFile = new File("/opt/prod/cert/prod_rsa.crt");
if (!certFile.exists()) {
throw new IllegalStateException("生产环境证书文件缺失:/opt/prod/cert/prod_rsa.crt");
}
// 2. 校验证书权限(生产环境要求证书文件仅root可读写)
checkFilePermission(certFile);
// 3. 校验证书完整性(防篡改)
if (!checkCertIntegrity(certFile)) {
throw new IllegalStateException("证书文件已被篡改,终止启动");
}
// 4. 加载密钥到全局上下文
System.out.println("加载密钥到全局上下文");
logStage("敏感资源初始化完成", "证书加载成功,权限及完整性校验通过");
}
// 校验文件权限(生产环境安全管控)
private void checkFilePermission(File file) {
// 简化实现,生产环境需结合操作系统校验文件属主和权限
if (file.canRead() && file.canWrite()) {
throw new IllegalStateException("证书文件权限过高,仅允许只读");
}
}
// 校验证书完整性(比如MD5校验)
private boolean checkCertIntegrity(File file) {
String prodMd5 = "xxx"; // 生产环境MD5值,可从配置中心拉取
String fileMd5 = MD5.create().digestHex16(file);
return prodMd5.equals(fileMd5);
}
// 结构化日志输出(生产环境建议用SLF4J)
private void logStage(String stage, String desc) {
System.out.printf("[%s] %s - %s%n", LocalDateTime.now(), stage, desc);
}
}
输出
/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:55025,suspend=y,server=n -javaagent:/Users/wangmingfei/Library/Caches/JetBrains/IdeaIC2024.3/captureAgent/debugger-agent.jar -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/wangmingfei/Documents/个人/05 java天梯之路/01 源码/03 每日打卡系列/daily-check-in/springboot钩子/demo/target/classes:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-web/3.5.8/spring-boot-starter-web-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter/3.5.8/spring-boot-starter-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot/3.5.8/spring-boot-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-autoconfigure/3.5.8/spring-boot-autoconfigure-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-logging/3.5.8/spring-boot-starter-logging-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-classic/1.5.21/logback-classic-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/ch/qos/logback/logback-core/1.5.21/logback-core-1.5.21.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-to-slf4j/2.24.3/log4j-to-slf4j-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/logging/log4j/log4j-api/2.24.3/log4j-api-2.24.3.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/jul-to-slf4j/2.0.17/jul-to-slf4j-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/usr/local/apache-maven-3.9.9/repository/org/yaml/snakeyaml/2.4/snakeyaml-2.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-json/3.5.8/spring-boot-starter-json-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-databind/2.19.4/jackson-databind-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-annotations/2.19.4/jackson-annotations-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/core/jackson-core/2.19.4/jackson-core-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.19.4/jackson-datatype-jdk8-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.19.4/jackson-datatype-jsr310-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.19.4/jackson-module-parameter-names-2.19.4.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/boot/spring-boot-starter-tomcat/3.5.8/spring-boot-starter-tomcat-3.5.8.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.49/tomcat-embed-core-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.49/tomcat-embed-el-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.49/tomcat-embed-websocket-10.1.49.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-web/6.2.14/spring-web-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-beans/6.2.14/spring-beans-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-observation/1.15.6/micrometer-observation-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/io/micrometer/micrometer-commons/1.15.6/micrometer-commons-1.15.6.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-webmvc/6.2.14/spring-webmvc-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-aop/6.2.14/spring-aop-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-context/6.2.14/spring-context-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-expression/6.2.14/spring-expression-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-core/6.2.14/spring-core-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/org/springframework/spring-jcl/6.2.14/spring-jcl-6.2.14.jar:/usr/local/apache-maven-3.9.9/repository/cn/hutool/hutool-all/5.8.28/hutool-all-5.8.28.jar:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar com.example.demo.DemoApplication
已连接到地址为 ''127.0.0.1:55025',传输: '套接字'' 的目标虚拟机
[2025-12-10T15:14:31.615386] 启动开始 - 应用启动流程初始化
[2025-12-10T15:14:31.616603] 敏感资源初始化 - 开始加载生产环境加密证书
Exception in thread "main" java.lang.IllegalStateException: 生产环境证书文件缺失:/opt/prod/cert/prod_rsa.crt
at com.example.demo.listener.StartupSensResPreloaderListener.starting(StartupSensResPreloaderListener.java:18)
at org.springframework.boot.SpringApplicationRunListeners.lambda$starting$0(SpringApplicationRunListeners.java:54)
at java.base/java.lang.Iterable.forEach(Iterable.java:75)
at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:118)
at org.springframework.boot.SpringApplicationRunListeners.starting(SpringApplicationRunListeners.java:54)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:310)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350)
at com.example.demo.DemoApplication.main(DemoApplication.java:11)
已与地址为 ''127.0.0.1:55025',传输: '套接字'' 的目标虚拟机断开连接
进程已结束,退出代码为 1
生产价值
- 敏感资源早期加载,避免启动后期因资源缺失导致失败;
- 严格的权限和完整性校验,符合生产环境安全规范;
- 资源加载失败直接终止启动,避免安全风险。
六、总结
SpringApplicationRunListener.starting() 是 Spring Boot 启动生命周期中最早可介入的扩展点。此时日志系统已就绪,但配置未加载、容器未创建,是执行底层、非业务、高优先级初始化逻辑的理想时机。starting() 虽小,却是构建高可用、可观测、安全可控的 Spring Boot 应用的第一道防线。
📌 关注我,每天5分钟,带你从 Java 小白变身编程高手!
👉 点赞 + 关注+转发,让更多小伙伴一起进步!
👉 私信"SpringBoot钩子源码" 获取源码!
1万+

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



