遗留系统中 static PropertyUtils.getProperty() 的一次温和改造

5 阅读3分钟

遗留系统中 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
    • 启动参数 / 系统变量

六、它为什么一定能生效?

因为:

  1. EnvironmentAware 的回调发生在 Context Refresh 早期
  2. Nacos 的 PropertySource 已经被挂载到 Environment
  3. 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()

那么这条路,大概率你迟早也要走一次。


写在最后:

很多技术债不是“写错了”,而是在当年是最优解。工程师真正的能力,不是避免历史,而是在不推倒重来的前提下,给系统一个向前的出口