在上一篇中,我们深入剖析了配置加载阶段的核心扩展点 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 初始化完成(配置最终生效后) |
| 核心能力 | 配置加载、修改、加密解密 | 环境校验、上下文传递、配置增强、隔离兜底 |
| 配置优先级 | 可修改配置源,影响最终配置 | 读取最终配置,仅能增强(无法回滚已加载的配置) |
| 典型场景 | 配置中心拉取、配置解密 | 环境合法性校验、启动上下文传递 |
✅ 最佳实践:
EnvironmentPostProcessor负责「配置的加载与修改」;environmentPrepared()负责「环境的校验与增强」;- 两者配合,实现「配置加载 - 环境校验 - 配置增强」的完整闭环。
七、总结
SpringApplicationRunListener.environmentPrepared() 是 Spring Boot 启动过程中「环境准备阶段的最后一道关卡」,核心价值体现在:
- 终极校验:对最终生效的环境做合法性兜底校验,拦截非法部署;
- 上下文传递:实现跨生命周期的启动上下文共享,替代全局变量;
- 环境隔离:强制绑定环境与配置特征,防止配置逃逸;
- 配置增强:根据机器 / 环境特征动态调整运行参数,实现配置自适应。
相较于 EnvironmentPostProcessor,environmentPrepared() 更聚焦于「环境层面的兜底与增强」,是构建生产级高可靠应用的关键扩展点。
📌 关注我,每天 5 分钟,带你从 Java 小白变身编程高手!
👉 点赞 + 关注 + 转发,让更多小伙伴一起进步!
👉 私信 "SpringBoot 钩子源码" 获取完整源码!