# Spring Boot 钩子全集实战(四):`SpringApplicationRunListener.environmentPrepared()` 详解

在上一篇中,我们深入剖析了配置加载阶段的核心扩展点 EnvironmentPostProcessor,解决了配置中心化、加密解密、动态覆盖等核心问题。今天,我们将聚焦 Spring Boot 环境准备完成阶段的关键扩展点 ------SpringApplicationRunListener.environmentPrepared(),它是连接「环境初始化」与「容器创建」的桥梁,也是实现环境校验、配置增强、启动上下文传递的核心入口。

一、什么是 environmentPrepared()

SpringApplicationRunListener.environmentPrepared()SpringApplicationRunListener 生命周期中第二个核心回调方法,触发时机具有明确的边界特征:

  • 触发时机Environment 完全初始化(包含所有 EnvironmentPostProcessor 处理结果),但 ApplicationContext 尚未创建;
  • 核心状态:所有配置源(配置中心、本地文件、环境变量、启动参数)已加载完成,配置最终生效;
  • 执行顺序 :早于 ApplicationContext 初始化;
  • 核心能力:可读取最终生效的配置、校验环境合法性、扩展环境上下文、终止非法启动流程。

核心价值:在容器创建前对最终生效的环境做「最终校验」和「上下文增强」,是拦截非法环境、传递启动上下文的最后一道关卡。

生产环境中,该扩展点常用于解决「环境合法性校验」「启动上下文传递」「多环境隔离兜底」「配置最终增强」等问题。

二、场景 1:环境合法性终极校验(拦截非法部署)

业务痛点

生产环境中,即使通过 EnvironmentPostProcessor 做了配置校验,仍可能出现以下问题:

  • 配置中心拉取的配置与本地残留配置冲突,导致最终生效配置非法;
  • 多环境配置叠加后超出合理范围(如生产环境使用测试数据库地址);
  • 启动参数覆盖配置中心配置,导致环境污染;
  • 非法环境(如生产代码部署到测试机器)无法提前拦截。

解决方案

基于 environmentPrepared() 对最终生效的环境做「终极校验」,校验不通过直接终止启动,避免非法部署导致的生产事故。

实现代码

java 复制代码
package com.example.demo.listener;
​
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
​
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;
​
/**
 * 环境合法性终极校验监听器
 */
public class EnvValidationRunListener implements SpringApplicationRunListener {
​
    // 生产环境允许的IP段(生产环境可从配置中心/权限系统获取)
    private static final Set<String> PROD_ALLOWED_IP_SEGMENTS = new HashSet<>();
    // 生产环境必须的配置前缀
    private static final String[] PROD_REQUIRED_CONFIGS = {
            "spring.datasource.url", "redis.host", "app.prod.mode", "app.nacos.server-addr"
    };
    // 生产环境禁止的配置特征(防止测试配置污染)
    private static final String[] PROD_FORBIDDEN_CONFIG_FEATURES = {
            "test-mysql", "localhost", "127.0.0.1", "dev-"
    };
​
    static {
        // 初始化生产环境允许的IP段
        PROD_ALLOWED_IP_SEGMENTS.add("10.0.");
        PROD_ALLOWED_IP_SEGMENTS.add("172.16.");
        PROD_ALLOWED_IP_SEGMENTS.add("192.168.10.");
    }
​
    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
                                    ConfigurableEnvironment environment) {
        System.out.println("[环境校验] 开始终极环境合法性校验");
​
        // 1. 获取最终激活的环境
        String[] activeProfiles = environment.getActiveProfiles();
        String activeEnv = activeProfiles.length > 0 ? activeProfiles[0] : "dev";
        System.out.printf("[环境校验] 当前激活环境:%s%n", activeEnv);
​
        // 2. 按环境执行差异化校验
        switch (activeEnv) {
            case "prod":
                validateProdEnvironment(environment);
                break;
            case "test":
                validateTestEnvironment(environment);
                break;
            case "dev":
                validateDevEnvironment(environment);
                break;
            default:
                throw new IllegalArgumentException("[环境校验] 不支持的环境:" + activeEnv);
        }
​
        System.out.println("[环境校验] 环境合法性校验通过");
    }
​
    /**
     * 生产环境终极校验
     */
    private void validateProdEnvironment(ConfigurableEnvironment environment) {
        try {
            // 校验1:生产环境必须部署在指定IP段
            String ip = InetAddress.getLocalHost().getHostAddress();
            boolean isAllowedIp = PROD_ALLOWED_IP_SEGMENTS.stream().anyMatch(ip::startsWith);
            if (!isAllowedIp) {
                throw new IllegalStateException(
                        String.format("[环境校验] 生产环境部署在非法IP段:%s,仅允许:%s",
                                ip, PROD_ALLOWED_IP_SEGMENTS)
                );
            }
​
            // 校验2:生产环境必须包含核心配置
            for (String configKey : PROD_REQUIRED_CONFIGS) {
                String value = environment.getProperty(configKey);
                if (!StringUtils.hasText(value)) {
                    throw new IllegalStateException("[环境校验] 生产环境缺失核心配置:" + configKey);
                }
            }
​
            // 校验3:生产环境禁止包含测试配置特征
            MutablePropertySources propertySources = environment.getPropertySources();
            for (PropertySource<?> propertySource : propertySources) {
                if (propertySource.getName().startsWith("system")) {
                    continue;
                }
                for (String forbiddenFeature : PROD_FORBIDDEN_CONFIG_FEATURES) {
                    for (String configKey : PROD_REQUIRED_CONFIGS) {
                        String value = environment.getProperty(configKey);
                        if (StringUtils.hasText(value) && value.contains(forbiddenFeature)) {
                            throw new IllegalStateException(
                                    String.format("[环境校验] 生产环境配置包含非法特征:%s = %s(禁止特征:%s)",
                                            configKey, value, forbiddenFeature)
                            );
                        }
                    }
                }
            }
​
            // 校验4:生产环境必须开启生产模式
            String prodMode = environment.getProperty("app.prod.mode");
            if (!"true".equals(prodMode)) {
                throw new IllegalStateException("[环境校验] 生产环境未开启生产模式:app.prod.mode = " + prodMode);
            }
​
        } catch (UnknownHostException e) {
            throw new RuntimeException("[环境校验] 获取生产机器IP失败", e);
        }
    }
​
    /**
     * 测试环境校验
     */
    private void validateTestEnvironment(ConfigurableEnvironment environment) {
        // 测试环境禁止使用生产配置中心地址
        String nacosAddr = environment.getProperty("app.nacos.server-addr");
        if (StringUtils.hasText(nacosAddr) && nacosAddr.contains("prod-nacos")) {
            throw new IllegalStateException("[环境校验] 测试环境使用生产配置中心:" + nacosAddr);
        }
        // 测试环境日志级别必须为DEBUG
        String logLevel = environment.getProperty("app.log.level");
        if (!"DEBUG".equals(logLevel)) {
            System.out.println("[环境校验] 测试环境日志级别非DEBUG,自动修正为DEBUG");
            environment.getSystemProperties().put("app.log.level", "DEBUG");
        }
    }
​
    /**
     * 开发环境校验
     */
    private void validateDevEnvironment(ConfigurableEnvironment environment) {
        // 开发环境允许宽松校验,仅提示非核心问题
        String dbUrl = environment.getProperty("spring.datasource.url");
        if (StringUtils.hasText(dbUrl) && dbUrl.contains("prod")) {
            System.out.println("[环境校验] 警告:开发环境使用生产数据库地址,请注意!");
        }
    }
}

配置加载

resources/META-INF/spring.factories 中配置:

ini 复制代码
org.springframework.boot.SpringApplicationRunListener=\
com.example.demo.listener.EnvValidationRunListener

启动测试(生产环境非法 IP 部署)

启动参数:--spring.profiles.active=prod机器 IP:192.168.1.100(不在生产允许 IP 段)

错误输出

less 复制代码
[环境校验] 开始终极环境合法性校验
[环境校验] 当前激活环境:prod
2025-12-14T21:15:17.906+08:00 ERROR 22746 --- [           main] o.s.boot.SpringApplication               : Application run failed
​
java.lang.IllegalStateException: [环境校验] 生产环境部署在非法IP段:127.0.0.1,仅允许:[10.0., 172.16., 192.168.10.]
    at com.example.demo.listener.EnvValidationRunListener.validateProdEnvironment(EnvValidationRunListener.java:76) ~[classes/:na]
    at com.example.demo.listener.EnvValidationRunListener.environmentPrepared(EnvValidationRunListener.java:51) ~[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]

生产价值

  • 拦截非法部署(如生产代码部署到测试机器、测试配置污染生产环境);
  • 对最终生效的配置做兜底校验,避免配置叠加导致的隐性问题;
  • 按环境差异化校验,兼顾生产环境严格性和开发环境灵活性;
  • 提前终止非法启动流程,避免应用启动后出现生产事故。

三、场景 2:启动上下文传递(跨生命周期共享数据)

业务痛点

Spring Boot 启动生命周期中,不同扩展点之间需要共享上下文数据,但存在以下问题:

  • EnvironmentPostProcessor 中生成的上下文数据(如配置中心拉取的元数据)无法传递到 ApplicationContext 初始化阶段;
  • 启动参数解析结果、环境校验结果需要在容器创建后被 @Configuration 类读取;
  • 多扩展点之间共享数据需要依赖全局变量,易出现线程安全问题。

解决方案

基于 environmentPrepared() 将启动上下文数据注入到 Environment 的「自定义属性源」中,实现跨生命周期的上下文传递,且天然支持线程安全。

实现代码

java 复制代码
package com.example.demo.listener;
​
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
​
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
​
/**
 * 启动上下文传递监听器
 */
public class ContextTransferRunListener implements SpringApplicationRunListener {
​
    // 上下文属性源名称(确保唯一性)
    private static final String CONTEXT_PROPERTY_SOURCE_NAME = "bootStartupContext";
​
    public ContextTransferRunListener(SpringApplication application, String[] args) {
    }
​
​
    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
                                    ConfigurableEnvironment environment) {
        System.out.println("[上下文传递] 开始构建并注入启动上下文");
​
        // 1. 构建启动上下文数据
        Map<String, Object> startupContext = new HashMap<>();
        // 生成唯一启动ID(用于链路追踪)
        startupContext.put("app.startup.id", UUID.randomUUID().toString());
        // 记录启动时间戳
        startupContext.put("app.startup.timestamp", System.currentTimeMillis());
        // 记录激活环境
        String activeEnv = environment.getActiveProfiles().length > 0 ?
                environment.getActiveProfiles()[0] : "dev";
        startupContext.put("app.startup.activeEnv", activeEnv);
        // 记录配置中心元数据
        String configCenterVersion = environment.getProperty("config.center.version", "unknown");
        startupContext.put("app.startup.configVersion", configCenterVersion);
        // 记录机器信息
        startupContext.put("app.startup.hostname", System.getenv("HOSTNAME") == null ?
                "unknown" : System.getenv("HOSTNAME"));
​
        // 2. 将上下文注入Environment(优先级最低,避免覆盖业务配置)
        MutablePropertySources propertySources = environment.getPropertySources();
        propertySources.addLast(new MapPropertySource(CONTEXT_PROPERTY_SOURCE_NAME, startupContext));
​
        // 3. 打印上下文信息(生产环境建议用SLF4J)
        System.out.println("[上下文传递] 启动上下文注入完成:");
        startupContext.forEach((key, value) ->
                System.out.printf("[上下文传递] %s = %s%n", key, value));
    }
​
    // 提供静态方法,方便其他扩展点/配置类读取上下文
    public static Map<String, Object> getStartupContext(ConfigurableEnvironment environment) {
        MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources()
                .get(CONTEXT_PROPERTY_SOURCE_NAME);
        return propertySource != null ? propertySource.getSource() : new HashMap<>();
    }
}

配置加载

ini 复制代码
org.springframework.boot.SpringApplicationRunListener=\
com.example.demo.listener.ContextTransferRunListener

@Configuration 中读取上下文

typescript 复制代码
package com.example.demo.config;
​
import com.example.demo.listener.ContextTransferRunListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
​
import java.util.Map;
​
@Configuration
public class StartupContextConfig {
​
    @Bean
    public String startupId(Environment environment) {
        // 读取启动上下文
        Map<String, Object> startupContext = ContextTransferRunListener.getStartupContext((ConfigurableEnvironment) environment);
        String startupId = (String) startupContext.get("app.startup.id");
        System.out.printf("[配置类] 读取到启动ID:%s%n", startupId);
        return startupId;
    }
}

输出

ini 复制代码
[上下文传递] 开始构建并注入启动上下文
[上下文传递] 启动上下文注入完成:
[上下文传递] app.startup.id = a59314ec-6d2f-46bc-9435-eb8f6bd118be
[上下文传递] app.startup.timestamp = 1765718508354
[上下文传递] app.startup.activeEnv = prod
[上下文传递] app.startup.configVersion = unknown
[上下文传递] app.startup.hostname = unknown
​
  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )___ | '_ | '_| | '_ / _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |___, | / / / /
 =========|_|==============|___/=/_/_/_/
​
 :: Spring Boot ::                (v3.5.8)
​
2025-12-14T21:21:48.383+08:00  INFO 22847 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 21.0.9 with PID 22847 (/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-14T21:21:48.384+08:00  INFO 22847 --- [           main] com.example.demo.DemoApplication         : The following 1 profile is active: "prod"
2025-12-14T21:21:48.693+08:00  INFO 22847 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-12-14T21:21:48.698+08:00  INFO 22847 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-12-14T21:21:48.699+08:00  INFO 22847 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.49]
2025-12-14T21:21:48.716+08:00  INFO 22847 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-12-14T21:21:48.716+08:00  INFO 22847 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 317 ms
[配置类] 读取到启动ID:a59314ec-6d2f-46bc-9435-eb8f6bd118be
2025-12-14T21:21:48.846+08:00  INFO 22847 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-12-14T21:21:48.850+08:00  INFO 22847 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.655 seconds (process running for 0.798)

生产价值

  • 实现跨启动生命周期的上下文共享,替代全局变量,避免线程安全问题;
  • 启动上下文可包含链路追踪 ID、配置版本、机器信息等,便于问题排查;
  • 上下文数据可被 @Configuration@Value、其他扩展点读取,使用灵活;
  • 上下文注入到 Environment 中,符合 Spring 生态规范,易于扩展。

四、场景 3:多环境隔离兜底(防止配置逃逸)

业务痛点

生产环境中,多环境配置隔离常出现「配置逃逸」问题:

  • 测试环境的配置通过配置中心灰度发布到生产环境;
  • 启动参数 --spring.profiles.active 被篡改,导致生产环境加载测试配置;
  • 容器化部署时,环境变量覆盖导致多环境配置混乱;
  • 配置中心的多环境配置隔离失效,出现跨环境配置泄露。

解决方案

基于 environmentPrepared() 实现多环境隔离兜底,强制绑定「环境标识」与「配置特征」,即使前面的配置处理环节出现问题,也能通过兜底规则确保环境隔离。

实现代码

java 复制代码
package com.example.demo.listener;
​
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
​
import java.util.HashMap;
import java.util.Map;
​
/**
 * 多环境隔离兜底监听器
 */
public class EnvIsolationRunListener implements SpringApplicationRunListener {
​
    // 环境-配置特征绑定规则(生产环境可从配置中心加载)
    private static final Map<String, EnvConfigRule> ENV_CONFIG_RULES = new HashMap<>();
​
    static {
        // 生产环境规则
        ENV_CONFIG_RULES.put("prod", new EnvConfigRule(
                new String[]{"prod-mysql", "prod-redis", "prod-nacos"}, // 必须包含的特征
                new String[]{"test-", "dev-", "localhost"} // 必须排除的特征
        ));
        // 测试环境规则
        ENV_CONFIG_RULES.put("test", new EnvConfigRule(
                new String[]{"test-mysql", "test-redis"},
                new String[]{"prod-", "dev-"}
        ));
        // 开发环境规则
        ENV_CONFIG_RULES.put("dev", new EnvConfigRule(
                new String[]{"localhost", "dev-"},
                new String[]{"prod-", "test-"}
        ));
    }
​
    public EnvIsolationRunListener(SpringApplication application, String[] args) {
    }
​
​
    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
                                    ConfigurableEnvironment environment) {
        System.out.println("[环境隔离] 开始多环境隔离兜底校验");
​
        // 1. 获取最终激活的环境
        String[] activeProfiles = environment.getActiveProfiles();
        String activeEnv = activeProfiles.length > 0 ? activeProfiles[0] : "dev";
​
        // 2. 获取当前环境的隔离规则
        EnvConfigRule rule = ENV_CONFIG_RULES.get(activeEnv);
        if (rule == null) {
            throw new IllegalArgumentException("[环境隔离] 无隔离规则的环境:" + activeEnv);
        }
​
        // 3. 遍历所有配置源,校验隔离规则
        MutablePropertySources propertySources = environment.getPropertySources();
        for (PropertySource<?> propertySource : propertySources) {
            // 跳过系统配置源
            if (propertySource.getName().startsWith("system")) {
                continue;
            }
​
            // 核心配置项校验
            String[] coreConfigs = {"spring.datasource.url", "redis.host", "app.nacos.server-addr"};
            for (String configKey : coreConfigs) {
                String configValue = environment.getProperty(configKey);
                if (StringUtils.hasText(configValue)) {
                    // 校验必须包含的特征
                    boolean containsRequired = false;
                    for (String requiredFeature : rule.requiredFeatures) {
                        if (configValue.contains(requiredFeature)) {
                            containsRequired = true;
                            break;
                        }
                    }
                    if (!containsRequired && activeEnv.equals("prod")) {
                        throw new IllegalStateException(
                                String.format("[环境隔离] 生产环境配置缺失必需特征:%s = %s(必需:%s)",
                                        configKey, configValue, String.join(",", rule.requiredFeatures))
                        );
                    }
​
                    // 校验必须排除的特征
                    for (String forbiddenFeature : rule.forbiddenFeatures) {
                        if (configValue.contains(forbiddenFeature)) {
                            throw new IllegalStateException(
                                    String.format("[环境隔离] %s环境配置包含非法特征:%s = %s(禁止:%s)",
                                            activeEnv, configKey, configValue, forbiddenFeature)
                            );
                        }
                    }
                }
            }
        }
​
        // 4. 兜底:强制设置环境标识(防止配置逃逸)
        environment.getSystemProperties().put("app.env.isolation", activeEnv);
        System.out.printf("[环境隔离] %s环境隔离校验通过,已设置隔离标识%n", activeEnv);
    }
​
    // 环境配置规则内部类
    private static class EnvConfigRule {
        private String[] requiredFeatures; // 必须包含的特征
        private String[] forbiddenFeatures; // 必须排除的特征
​
        public EnvConfigRule(String[] requiredFeatures, String[] forbiddenFeatures) {
            this.requiredFeatures = requiredFeatures;
            this.forbiddenFeatures = forbiddenFeatures;
        }
    }
}

配置加载

ini 复制代码
org.springframework.boot.SpringApplicationRunListener=\
com.example.demo.listener.EnvIsolationRunListener

application.yml

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://test-mysql:3306/prod_db?useSSL=false
    username: prod_user
    # 密文存储,前缀标识需要解密
    password: encrypt:DC76b3+IyNwp+f/1QxPiIA==
redis:
  host: prod-redis:6379
  password: encrypt:DC76b3+IyNwp+f/1QxPiIA==
app:
  api:
    secret: encrypt:DC76b3+IyNwp+f/1QxPiIA==

错误输出(生产环境包含 test 特征)

less 复制代码
[环境隔离] 开始多环境隔离兜底校验
2025-12-14T21:48:10.802+08:00 ERROR 23312 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: [环境隔离] 生产环境配置缺失必需特征:spring.datasource.url = jdbc:mysql://test-mysql:3306/prod_db?useSSL=false(必需:prod-mysql,prod-redis,prod-nacos)
    at com.example.demo.listener.EnvIsolationRunListener.environmentPrepared(EnvIsolationRunListener.java:82) ~[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]

生产价值

  • 实现多环境配置隔离的最后一道兜底防线,防止配置逃逸;
  • 强制绑定环境与配置特征,即使前面的配置处理环节出错也能拦截;
  • 可动态调整环境规则,适配不同部署架构(如容器化、物理机);
  • 避免因配置中心隔离失效、启动参数篡改导致的环境污染。

五、场景 4:配置最终增强(动态调整运行参数)

业务痛点

生产环境中,配置最终生效后仍需要根据环境特征动态调整:

  • 生产环境需要调大线程池、连接池大小,测试环境调小;
  • 灰度机器需要降低日志级别、开启监控采样;
  • 不同机器规格(CPU / 内存)需要适配不同的 JVM 参数、连接池参数;
  • 配置中心的默认参数无法适配所有机器的硬件特征。

解决方案

基于 environmentPrepared() 在配置最终生效后、容器创建前,根据机器特征、环境特征动态增强配置,实现「配置自适应」。

实现代码

java 复制代码
package com.example.demo.listener;

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 org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.MapPropertySource;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.util.HashMap;
import java.util.Map;

/**
 * 配置最终增强监听器
 */
public class ConfigEnhanceRunListener implements SpringApplicationRunListener {


    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
                                    ConfigurableEnvironment environment) {
        System.out.println("[配置增强] 开始根据机器特征动态增强配置");

        // 1. 获取机器硬件信息
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
        int cpuCount = osBean.getAvailableProcessors();
        // 修复:兼容获取物理内存大小
        long totalMemory = getTotalPhysicalMemorySize(osBean); // GB
        String activeEnv = environment.getActiveProfiles().length > 0 ?
                environment.getActiveProfiles()[0] : "dev";

        // 2. 打印机器信息
        System.out.printf("[配置增强] 机器特征:CPU核心数=%d,内存=%dGB,环境=%s%n",
                cpuCount, totalMemory, activeEnv);

        // 3. 动态增强配置
        Map<String, Object> enhanceConfig = new HashMap<>();
        if ("prod".equals(activeEnv)) {
            // 生产环境:连接池大小 = CPU核心数 * 2
            enhanceConfig.put("spring.datasource.hikari.maximum-pool-size", cpuCount * 2);
            enhanceConfig.put("spring.datasource.hikari.minimum-idle", cpuCount);
            // 生产环境:线程池大小 = CPU核心数 * 4
            enhanceConfig.put("app.thread.pool.core-size", cpuCount * 4);
            enhanceConfig.put("app.thread.pool.max-size", cpuCount * 8);
            // 大内存机器(>16GB)调大缓存
            if (totalMemory > 16) {
                enhanceConfig.put("spring.cache.redis.time-to-live", "3600s");
                enhanceConfig.put("app.redis.pool.max-active", "200");
            } else {
                enhanceConfig.put("spring.cache.redis.time-to-live", "600s");
                enhanceConfig.put("app.redis.pool.max-active", "100");
            }
        } else if ("test".equals(activeEnv)) {
            // 测试环境:固定小配置
            enhanceConfig.put("spring.datasource.hikari.maximum-pool-size", "5");
            enhanceConfig.put("app.thread.pool.core-size", "10");
            enhanceConfig.put("spring.cache.redis.time-to-live", "60s");
        } else {
            // 开发环境:极简配置
            enhanceConfig.put("spring.datasource.hikari.maximum-pool-size", "2");
            enhanceConfig.put("app.thread.pool.core-size", "5");
        }

        // 4. 灰度机器特殊配置
        String hostname = System.getenv("HOSTNAME") == null ?
                (System.getenv("COMPUTERNAME") == null ? "unknown" : System.getenv("COMPUTERNAME"))
                : System.getenv("HOSTNAME");
        if (hostname.contains("gray")) {
            enhanceConfig.put("app.log.level", "TRACE");
            enhanceConfig.put("app.monitor.sampling.rate", "1.0"); // 全量采样
            System.out.println("[配置增强] 灰度机器,开启全量监控采样");
        } else {
            enhanceConfig.put("app.monitor.sampling.rate", "0.1"); // 10%采样
        }

        // 5. 注入增强配置(优先级高于配置中心,低于启动参数)
        // 先判断是否存在configCenterProperties,避免报错
        MutablePropertySources propertySources = environment.getPropertySources();
        String targetSource = "configCenterProperties";
        if (propertySources.contains(targetSource)) {
            propertySources.addBefore(targetSource, new MapPropertySource("enhanceConfig", enhanceConfig));
        } else {
            // 如果没有配置中心,添加到最后(保证自定义配置生效)
            propertySources.addLast(new MapPropertySource("enhanceConfig", enhanceConfig));
        }

        // 6. 打印增强结果
        System.out.println("[配置增强] 动态增强配置完成:");
        enhanceConfig.forEach((key, value) ->
                System.out.printf("[配置增强] %s = %s%n", key, value));
    }

    /**
     * 兼容获取物理内存大小(GB)
     * @param osBean 操作系统MXBean
     * @return 物理内存大小(GB),获取失败返回默认值8GB
     */
    private long getTotalPhysicalMemorySize(OperatingSystemMXBean osBean) {
        try {
            // 优先使用Sun/Oracle JDK的实现
            if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
                com.sun.management.OperatingSystemMXBean sunOsBean =
                        (com.sun.management.OperatingSystemMXBean) osBean;
                // 转换为GB
                return sunOsBean.getTotalPhysicalMemorySize() / (1024L * 1024L * 1024L);
            }

            // 其他JDK实现的兼容处理(如OpenJDK)
            // 尝试通过反射调用(防止编译时依赖)
            java.lang.reflect.Method method = osBean.getClass().getMethod("getTotalPhysicalMemorySize");
            if (method != null) {
                Long memorySize = (Long) method.invoke(osBean);
                return memorySize / (1024L * 1024L * 1024L);
            }
        } catch (Exception e) {
            System.out.println("[配置增强] 获取物理内存大小失败,使用默认值8GB:" + e.getMessage());
        }

        // 默认返回8GB
        return 8L;
    }
}

配置加载

ini 复制代码
org.springframework.boot.SpringApplicationRunListener=\
com.example.demo.listener.ConfigEnhanceRunListener

输出

ini 复制代码
[配置增强] 开始根据机器特征动态增强配置
[配置增强] 机器特征:CPU核心数=14,内存=48GB,环境=prod
[配置增强] 动态增强配置完成:
[配置增强] app.redis.pool.max-active = 200
[配置增强] spring.datasource.hikari.maximum-pool-size = 28
[配置增强] app.thread.pool.max-size = 112
[配置增强] spring.cache.redis.time-to-live = 3600s
[配置增强] app.thread.pool.core-size = 56
[配置增强] app.monitor.sampling.rate = 0.1
[配置增强] spring.datasource.hikari.minimum-idle = 14

  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )___ | '_ | '_| | '_ / _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |___, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.8)

2025-12-14T21:59:21.010+08:00  INFO 23462 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 21.0.9 with PID 23462 (/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-14T21:59:21.012+08:00  INFO 23462 --- [           main] com.example.demo.DemoApplication         : The following 1 profile is active: "prod"
2025-12-14T21:59:21.326+08:00  INFO 23462 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-12-14T21:59:21.332+08:00  INFO 23462 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-12-14T21:59:21.332+08:00  INFO 23462 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.49]
2025-12-14T21:59:21.345+08:00  INFO 23462 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-12-14T21:59:21.345+08:00  INFO 23462 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 319 ms
[配置类] 读取到启动ID:null
2025-12-14T21:59:21.478+08:00  INFO 23462 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-12-14T21:59:21.482+08:00  INFO 23462 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.669 seconds (process running for 0.815)

生产价值

  • 实现配置的硬件自适应,充分利用机器资源;
  • 按环境差异化调整运行参数,兼顾性能与资源消耗;
  • 灰度机器特殊配置,便于问题排查和监控;
  • 无需修改配置中心,动态适配不同机器规格。

六、environmentPrepared()EnvironmentPostProcessor 对比

特性 EnvironmentPostProcessor SpringApplicationRunListener.environmentPrepared()
触发时机 Environment 初始化中(配置加载阶段) Environment 初始化完成(配置最终生效后)
核心能力 配置加载、修改、加密解密 环境校验、上下文传递、配置增强、隔离兜底
配置优先级 可修改配置源,影响最终配置 读取最终配置,仅能增强(无法回滚已加载的配置)
典型场景 配置中心拉取、配置解密 环境合法性校验、启动上下文传递

最佳实践

  1. EnvironmentPostProcessor 负责「配置的加载与修改」;
  2. environmentPrepared() 负责「环境的校验与增强」;
  3. 两者配合,实现「配置加载 - 环境校验 - 配置增强」的完整闭环。

七、总结

SpringApplicationRunListener.environmentPrepared() 是 Spring Boot 启动过程中「环境准备阶段的最后一道关卡」,核心价值体现在:

  • 终极校验:对最终生效的环境做合法性兜底校验,拦截非法部署;
  • 上下文传递:实现跨生命周期的启动上下文共享,替代全局变量;
  • 环境隔离:强制绑定环境与配置特征,防止配置逃逸;
  • 配置增强:根据机器 / 环境特征动态调整运行参数,实现配置自适应。

相较于 EnvironmentPostProcessorenvironmentPrepared() 更聚焦于「环境层面的兜底与增强」,是构建生产级高可靠应用的关键扩展点。

📌 关注我,每天 5 分钟,带你从 Java 小白变身编程高手!

👉 点赞 + 关注 + 转发,让更多小伙伴一起进步!

👉 私信 "SpringBoot 钩子源码" 获取完整源码!

相关推荐
i757_w2 小时前
IDEA快捷键被占用
java·ide·intellij-idea
白鸽(二般)2 小时前
Spring 的配置文件没有小绿叶
java·后端·spring
zhangkaixuan4562 小时前
Paimon Action Jar 实现机制分析
java·大数据·flink·paimon·datalake
only-qi2 小时前
深入理解MySQL中的MVCC:多版本并发控制的实现原理
java·数据库·mysql
ZePingPingZe2 小时前
静态代理、JDK和Cglib动态代理、回调
java·开发语言
万粉变现经纪人2 小时前
如何解决 pip install 代理报错 SOCKS5 握手失败 ReadTimeoutError 问题
java·python·pycharm·beautifulsoup·bug·pandas·pip
风月歌2 小时前
2025-2026计算机毕业设计选题指导,java|springboot|ssm项目成品推荐
java·python·小程序·毕业设计·php·源码
heartbeat..2 小时前
Web 状态管理核心技术详解 + JWT 双 Token (Access/Refresh Token) 自动登录
java·网络·jwt·token
Seven972 小时前
剑指offer-57、二叉树的下一个节点
java