Spring Boot 钩子全集实战(二):SpringApplicationRunListener.starting()详解
在上一篇中,我们介绍了如何通过addInitializers、addListeners等方式在启动前注入逻辑。今天,我们将聚焦于 Spring Boot 启动流程中最早触发的扩展点——SpringApplicationRunListener.starting(),并重点讲解它在真实生产系统中的典型应用。
一、什么是starting()?
starting()是SpringApplicationRunListener接口的第一个回调方法,在以下条件下被调用:
- 日志系统已初始化(可安全输出日志);
- 但 Environment 尚未加载(
application.yml还没读); - ApplicationContext 尚未创建(Spring 容器完全不存在)。
✅这意味着:这是整个应用生命周期中你能介入的“最早时刻”。
在生产环境中,这个“黄金窗口期”常被用于执行与业务无关但对系统稳定性至关重要的底层操作。
二、场景 1:全链路启动耗时监控(定位启动性能瓶颈)
业务痛点
生产环境中,应用启动慢是高频问题,但常规日志只能看到总耗时,无法定位 “哪个阶段拖慢了启动”(比如环境加载慢、Bean 初始化慢、Runner 执行慢),排查效率极低。
解决方案
基于SpringApplicationRunListener记录每个启动阶段的耗时,输出结构化监控日志,精准定位瓶颈。
实现代码
importorg.springframework.boot.ConfigurableBootstrapContext;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.SpringApplicationRunListener;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.core.env.ConfigurableEnvironment;importjava.time.Duration;importjava.time.LocalDateTime;importjava.util.HashMap;importjava.util.Map;/** * 生产级启动耗时监控监听器 */publicclassStartupTimeMonitorListenerimplementsSpringApplicationRunListener{// 记录各阶段开始时间privatefinalMap<String,LocalDateTime>stageStartTime=newHashMap<>();privatefinalSpringApplicationapplication;privatefinalString[]args;// 必须的构造方法publicStartupTimeMonitorListener(SpringApplicationapplication,String[]args){this.application=application;this.args=args;}@Overridepublicvoidstarting(ConfigurableBootstrapContextbootstrapContext){// 记录启动开始时间stageStartTime.put("starting",LocalDateTime.now());logStage("启动开始","应用启动流程初始化");}@OverridepublicvoidenvironmentPrepared(ConfigurableBootstrapContextbootstrapContext,ConfigurableEnvironmentenvironment){recordStageCost("starting","environmentPrepared");logStage("环境准备完成","配置文件/环境变量加载完毕,当前环境:"+String.join(",",environment.getActiveProfiles()));}@OverridepublicvoidcontextPrepared(ConfigurableApplicationContextcontext){recordStageCost("environmentPrepared","contextPrepared");logStage("上下文准备完成","ApplicationContext创建完毕,未加载Bean定义");}@OverridepublicvoidcontextLoaded(ConfigurableApplicationContextcontext){recordStageCost("contextPrepared","contextLoaded");logStage("上下文加载完成","Bean定义已注册,等待上下文刷新");}@Overridepublicvoidstarted(ConfigurableApplicationContextcontext,DurationtimeTaken){recordStageCost("contextLoaded","started");logStage("应用启动完成","上下文刷新完毕,Runner未执行,累计耗时:"+timeTaken.toMillis()+"ms");}@Overridepublicvoidready(ConfigurableApplicationContextcontext,DurationtimeTaken){recordStageCost("started","ready");logStage("应用完全就绪","所有Runner执行完毕,可对外提供服务,总耗时:"+timeTaken.toMillis()+"ms");// 输出全链路耗时汇总(生产环境可上报到监控平台如Prometheus/Grafana)System.out.println("=== 启动阶段耗时汇总 ===");stageStartTime.forEach((stage,time)->{longcost=Duration.between(time,LocalDateTime.now()).toMillis();System.out.println(stage+" 阶段累计耗时:"+cost+"ms");});}// 记录两个阶段之间的耗时privatevoidrecordStageCost(StringprevStage,StringcurrentStage){LocalDateTimeprevTime=stageStartTime.get(prevStage);if(prevTime!=null){longcost=Duration.between(prevTime,LocalDateTime.now()).toMillis();stageStartTime.put(currentStage,LocalDateTime.now());System.out.println(prevStage+" -> "+currentStage+" 耗时:"+cost+"ms");}}// 结构化日志输出(生产环境建议用SLF4J)privatevoidlogStage(Stringstage,Stringdesc){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方法实现全场景启动失败告警,结合企业微信 / 钉钉机器人推送告警信息,包含失败阶段、异常栈、机器信息等关键内容。
核心实现
importorg.springframework.boot.SpringApplicationRunListener;importorg.springframework.context.ConfigurableApplicationContext;publicclassStartupFailerMonitorListenerimplementsSpringApplicationRunListener{@Overridepublicvoidfailed(ConfigurableApplicationContextcontext,Throwableexception){// 1. 收集核心告警信息StringalertContent=buildAlertContent(context,exception);// 2. 推送告警(生产环境建议异步推送)try{System.out.println(String.format("{%s}-{%s}","【生产环境应用启动失败】",alertContent));}catch(Exceptione){System.err.println("告警推送失败:"+e.getMessage());}// 3. 释放已初始化的资源(避免资源泄漏)releaseResources(context);}// 构建告警内容(包含生产环境关键信息)privateStringbuildAlertContent(ConfigurableApplicationContextcontext,Throwableexception){StringBuildersb=newStringBuilder();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");returnsb.toString();}// 释放资源(比如关闭已创建的数据库连接、缓存客户端)privatevoidreleaseResources(ConfigurableApplicationContextcontext){if(context!=null&&context.isActive()){try{// 关闭自定义资源池System.out.println("关闭资源成功");}catch(Exceptione){System.err.println("资源释放失败:"+e.getMessage());}}}privateStringgetFailedStage(ConfigurableApplicationContextcontext){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阶段校验核心配置,若配置不合法直接抛出异常,终止启动流程,避免 “启动成功但服务不可用” 的无效状态。
核心实现
packagecom.example.demo.listener;importorg.springframework.boot.ConfigurableBootstrapContext;importorg.springframework.boot.SpringApplicationRunListener;importorg.springframework.core.env.ConfigurableEnvironment;importjava.net.InetAddress;importjava.net.UnknownHostException;importjava.util.UUID;publicclassStartupEnvConfigValidListenerimplementsSpringApplicationRunListener{@OverridepublicvoidenvironmentPrepared(ConfigurableBootstrapContextbootstrapContext,ConfigurableEnvironmentenvironment){// 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(UnknownHostExceptione){thrownewRuntimeException(e);}}// 校验核心配置是否存在且合法privatevoidvalidateCoreConfig(ConfigurableEnvironmentenvironment,StringconfigKey){Stringvalue=environment.getProperty(configKey);if(value==null||value.trim().isEmpty()){thrownewIllegalArgumentException("生产环境核心配置缺失:"+configKey);}// 自定义规则校验(比如数据库URL格式)if(configKey.equals("spring.datasource.url")&&!value.startsWith("jdbc:mysql://")){thrownewIllegalArgumentException("生产环境数据库URL格式非法:"+value);}}// 校验Profile合法性(生产环境禁止test/dev)privatevoidvalidateProfile(ConfigurableEnvironmentenvironment){String[]activeProfiles=environment.getActiveProfiles();for(Stringprofile:activeProfiles){if("test".equals(profile)||"dev".equals(profile)){thrownewIllegalStateException("生产环境禁止激活test/dev Profile:"+profile);}}}// 补充生产配置(比如注入机器IP、应用实例ID)privatevoidsupplementProdConfig(ConfigurableEnvironmentenvironment)throwsUnknownHostException{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阶段加载敏感资源,做前置校验,确保资源合法后再继续启动流程。
packagecom.example.demo.listener;importcn.hutool.core.io.FileUtil;importcn.hutool.crypto.digest.MD5;importorg.springframework.boot.ConfigurableBootstrapContext;importorg.springframework.boot.SpringApplicationRunListener;importjava.io.File;importjava.time.LocalDateTime;publicclassStartupSensResPreloaderListenerimplementsSpringApplicationRunListener{@Overridepublicvoidstarting(ConfigurableBootstrapContextbootstrapContext){logStage("敏感资源初始化","开始加载生产环境加密证书");// 1. 加载证书文件(从安全目录读取,非classpath)FilecertFile=newFile("/opt/prod/cert/prod_rsa.crt");if(!certFile.exists()){thrownewIllegalStateException("生产环境证书文件缺失:/opt/prod/cert/prod_rsa.crt");}// 2. 校验证书权限(生产环境要求证书文件仅root可读写)checkFilePermission(certFile);// 3. 校验证书完整性(防篡改)if(!checkCertIntegrity(certFile)){thrownewIllegalStateException("证书文件已被篡改,终止启动");}// 4. 加载密钥到全局上下文System.out.println("加载密钥到全局上下文");logStage("敏感资源初始化完成","证书加载成功,权限及完整性校验通过");}// 校验文件权限(生产环境安全管控)privatevoidcheckFilePermission(Filefile){// 简化实现,生产环境需结合操作系统校验文件属主和权限if(file.canRead()&&file.canWrite()){thrownewIllegalStateException("证书文件权限过高,仅允许只读");}}// 校验证书完整性(比如MD5校验)privatebooleancheckCertIntegrity(Filefile){StringprodMd5="xxx";// 生产环境MD5值,可从配置中心拉取StringfileMd5=MD5.create().digestHex16(file);returnprodMd5.equals(fileMd5);}// 结构化日志输出(生产环境建议用SLF4J)privatevoidlogStage(Stringstage,Stringdesc){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钩子源码" 获取源码!