遗留系统中 static PropertyUtils.getProperty() 的一次温和改造
关键词:Spring Boot / Nacos / static / ApplicationContext / 配置治理 / 遗留系统
一、问题背景:一个看似无害的工具类
在很多“有一定历史包袱”的 Java 项目中,都会存在类似下面这样的工具类:
public class PropertyUtils {
private static Properties prop;
static{
if(prop == null){
prop = new Properties();
try{
String env = StringUtils.isEmpty(System.getenv("ENV")) ? "uat" : System.getenv("ENV");
prop.load(PropertyUtils.class.getResourceAsStream("/config-" + env +".properties"));
}catch(Exception e){
e.printStackTrace();
}
}
}
public static String getProperty(String key){
return prop.getProperty(key);
}
}
它通常具有以下特点:
- 使用 static 方法,全项目可直接调用
- 在 static 块中加载配置文件
- 被大量业务代码依赖,几乎无法直接删除或重构
在早期单体应用、properties 文件直读的年代,这种写法虽然不优雅,但足够稳定。
问题出现在:
当系统开始迁移到 Spring Boot + yml + Nacos 之后
二、为什么它会在 Nacos / yml 场景下彻底失效?
1. static 的本质:脱离 Spring 生命周期
static 的初始化时机只有一个:
类第一次被 JVM 加载时
而这个时机:
- 早于 Spring 容器刷新
- 早于 Nacos 配置拉取
- 早于
Environment、@Value、@ConfigurationProperties
换句话说:
PropertyUtils 在 Spring 还没“醒”之前,就已经把配置“读死了”
2. 为什么 ApplicationContextInitializer 也救不了?
很多人第一反应是:
“我在
main方法里加一个ApplicationContextInitializer,提前把配置塞进去不就行了?”
现实是:
ApplicationContextInitializer只作用于 Spring Context 初始化过程- 对已经完成 static 初始化的类无效
static 代码一旦执行,JVM 不会为你“重来一次”。
三、改造目标:不动调用点,解决配置来源问题
现实约束非常明确:
- ❌ 不能把
PropertyUtils.getProperty()全项目替换成@Value - ❌ 不能要求所有调用点注入
Environment - ❌ 不能大规模重构历史代码
目标只有一个:
在 不改变原有调用方式 的前提下,让
getProperty()能正确读取:
- yml
- profile
- Nacos
四、核心思路:static 不是问题,static“自己读文件”才是问题
关键认知转变:
static 方法可以保留,但 static 初始化逻辑必须被移除
换句话说:
- 不让它“主动加载配置”
- 而是让 Spring 在正确的时机把配置“注入进来”
五、最终方案:static 门面 + Spring 注入一次
1. 改造后的 PropertyUtils(关键点)
@Component
public class PropertyUtils implements EnvironmentAware {
private static Environment environment;
@Override
public void setEnvironment(Environment env) {
PropertyUtils.environment = env;
}
public static String getProperty(String key) {
if (environment == null) {
throw new IllegalStateException("Spring Environment not initialized yet");
}
return environment.getProperty(key);
}
}
这个设计解决了什么?
-
✅
getProperty()调用方式完全不变 -
✅ 配置来源完全交给 Spring
Environment -
✅ 自动支持:
- application.yml
- profile
- Nacos
- 启动参数 / 系统变量
六、它为什么一定能生效?
因为:
EnvironmentAware的回调发生在 Context Refresh 早期- Nacos 的
PropertySource已经被挂载到Environment - static 字段只是一个“指针”,而不是初始化逻辑
static 不再决定“读什么”,只决定“怎么拿” 。
七、关于 yml / properties / Nacos 的兼容性说明
这一方案天然支持:
spring:
profiles:
active: uat
custom:
feature:
enabled: true
调用:
PropertyUtils.getProperty("custom.feature.enabled");
Nacos 中同样适用,无需额外适配代码。
八、如果你担心“启动早期被调用”怎么办?
这是一个非常合理的担忧。
但结论是:
如果某段代码在 Spring Context 未初始化前就调用业务配置,本身就是设计问题
保留这个 IllegalStateException,反而能:
- 暴露隐藏的启动时序问题
- 防止“静默读 null 配置”
九、总结:这不是一次配置迁移,而是一次认知升级
这次问题的本质并不是:
- Nacos 配错了
- yml 没生效
- Spring Boot 不稳定
而是:
JVM 类加载模型 × Spring 生命周期 × static 工具类的必然冲突
解决它的关键,不在于“更复杂的初始化技巧”,而在于:
- 把“配置读取权”交还给 Spring
- 把 static 降级为“访问门面”
如果你的系统里,也有成片的 XXXUtils.getProperty(),
那么这条路,大概率你迟早也要走一次。
写在最后:
很多技术债不是“写错了”,而是在当年是最优解。工程师真正的能力,不是避免历史,而是在不推倒重来的前提下,给系统一个向前的出口。