Spring Boot 钩子全集实战(二):`SpringApplicationRunListener.starting()` 详解

Spring Boot 钩子全集实战(二):SpringApplicationRunListener.starting() 详解

在上一篇中,我们介绍了如何通过 addInitializersaddListeners 等方式在启动前注入逻辑。今天,我们将聚焦于 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钩子源码" 获取源码!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值