Spring Cloud + Nacos + Jasypt 配置刷新踩坑记:一个排查了好久的问题

9 min
AI 总结
$
|

Spring Cloud + Nacos + Jasypt 配置刷新踩坑记:一个排查了好久的问题

Jasypt配置体系

项目中实现了多种Jasypt加密器:

@Getter
@AllArgsConstructor
public enum JasyptEnum {
    DEFAULT_JASYPT(-1, "默认加解密器:未指定加解密器"),
    SUWELL_JASYPT(1, "xxx标品加解密器"), 
    WPSC_JASYPT(2, "xxxx加解密器"),
}

配置项包括:

  • jasypt.jasyptType: 加密器类型
  • jasypt.keyId: 密钥ID
  • jasypt.appKeyPath: 应用密钥路径
  • jasypt.secretPath: 密钥文件路径

问题现象

典型场景

我们有一个业务配置组件 BusinessConfigComponent,负责将数据库中的配置推送到Nacos:

@Component
public class BusinessConfigComponent {
    
    @Resource
    private NacosConfigManager nacosConfigManager;
    
    @Resource 
    private SysConfigService sysConfigService;
    
    public void initBusinessConfig(Map<String, Object> extraConfig, boolean dbForbidden) {
        // 1. 读取数据库配置
        Map<String, Object> map = mapDbConfig(dbForbidden);
        
        // 2. 获取Nacos现有配置
        ConfigService configService = nacosConfigManager.getConfigService();
        String config = configService.getConfig("xxxx.yml", "COMMON", 5000);
        
        // 3. 合并配置
        Map<String, Object> existParams = Maps.newHashMap();
        if (StringUtils.isNotEmpty(config)) {
            existParams = new Yaml().load(config);
        }
        existParams.putAll(map);
        existParams.putAll(extraConfig);
        
        // 4. 发布到Nacos
        String yamlString = new Yaml().dump(existParams);
        boolean success = configService.publishConfig("xxxx.yml", "COMMON", yamlString, "yaml");
        
        // 问题就在这里!!!
        // 如果没有下面这行代码,Jasypt配置更新后不会生效
    }
}

问题表现

  1. 配置发布成功: Nacos控制台显示配置已更新
  2. 应用无感知: 应用中的Jasypt相关配置仍然是旧值
  3. 加密失败: 新的密钥配置无法正常工作
  4. 重启生效: 只有重启应用才能获取最新配置

错误日志示例

2024-11-15 10:30:25.123 WARN  - 加载的jasypt类型为空或者-1:null,使用默认加解密器DefaultLazyEncryptor...
2024-11-15 10:30:25.124 ERROR - Jasypt配置加载失败: jasypt.keyId为空
2024-11-15 10:30:25.125 ERROR - Redis连接失败: 无法解密密码配置

排查过程

第一阶段:怀疑Nacos配置

最初我们怀疑是Nacos配置的问题:

  1. 检查配置格式: 确认YAML格式正确
  2. 验证配置推送: 通过Nacos控制台确认配置已更新
  3. 检查配置监听: 确认应用已注册配置监听器
# bootstrap.yml 中的配置
spring:
  cloud:
    nacos:
      config:
        shared-configs:
          - dataId: plss-config.yml
            group: COMMON
            refresh: true  # 已开启自动刷新

第二阶段:怀疑Jasypt初始化

接着我们检查了Jasypt的初始化逻辑:

@Bean(name = ENCRYPTOR_BEAN_NAME)
public StringEncryptor stringEncryptor(final EnvCopy envCopy, final BeanFactory bf) {
    try {
        ConfigurableEnvironment configurableEnvironment = envCopy.get();
        String jasyptType = configurableEnvironment.getProperty("jasypt.jasyptType");
        String keyId = configurableEnvironment.getProperty("jasypt.keyId");
        
        // 这里获取到的仍然是旧配置!
        if (StringUtils.isBlank(jasyptType) || "-1".equals(jasyptType)) {
            log.warn("加载的jasypt类型为空或者-1:{},使用默认加解密器...", jasyptType);
            return new DefaultLazyEncryptor(envCopy.get(), customEncryptorBeanName, isCustom, bf);
        }
        // ...
    }
}

发现问题:Jasypt的 StringEncryptor bean在应用启动时就已经创建,即使Nacos配置更新了,这个bean也不会重新创建。

第三阶段:深入Spring Cloud配置刷新机制

通过源码分析,我们发现了关键问题:

  1. Nacos配置更新: 只是更新了配置中心的数据
  2. 本地Environment未更新: Spring的Environment对象没有感知到配置变化
  3. Bean未重新创建: 已创建的bean(如StringEncryptor)不会自动重新初始化

解决方案

核心代码

在配置发布成功后,添加一行关键代码:

@Component
public class BusinessConfigComponent {
    
    @Resource
    private ContextRefresher contextRefresher;  // 关键依赖
    
    public void initBusinessConfig(Map<String, Object> extraConfig, boolean dbForbidden) {
        // ... 配置合并和发布逻辑
        
        boolean success = configService.publishConfig("plss-config.yml", "COMMON", yamlString, "yaml");
        
        // 🔥 解决方案:发布成功后刷新本地环境
        if (success) {
            contextRefresher.refreshEnvironment();  // 就是这一行!
        }
    }
}

原理解析

ContextRefresher.refreshEnvironment() 的工作机制:

  1. 发送RefreshEvent: 向Spring事件总线发送刷新事件
  2. 重新加载配置源: 从所有配置源(包括Nacos)重新拉取配置
  3. 更新Environment: 将新配置更新到Spring的Environment中
  4. 刷新@RefreshScope Bean: 重新创建所有标注了@RefreshScope的bean

配合@RefreshScope注解

为了让更多配置类能够动态刷新,我们在项目中大量使用了@RefreshScope注解:

@Component
@RefreshScope  // 配置更新时会重新创建这个bean
public class CustomProperties {
    
    @Value("${custom.config.value}")
    private String configValue;
    
    // getter/setter...
}

项目中共有142个类使用了@RefreshScope注解,涵盖:

  • 数据库连接配置
  • Redis配置
  • 加密配置
  • 业务配置参数
  • 第三方服务配置

效果验证

验证方式

我们通过JasyptController提供的接口来验证配置是否生效:

@RestController
@RequestMapping("/jasypt")
public class JasyptController {
    
    @Resource
    private StringEncryptor jasyptStringEncryptor;
    
    @Value("${spring.data.redis.password}")
    private String redisPassword;
    
    @PostMapping("/verify")
    public Object verify() {
        JSONObject result = new JSONObject();
        
        // 验证Redis连接(使用加密密码)
        String redisKey = "test_key";
        redisService.set(redisKey, redisPassword);
        result.put("redis", redisService.get(redisKey, String.class));
        
        return result;
    }
}

验证结果

修复前:

{
  "error": "Redis连接失败: 密码解密错误"
}

修复后:

{
  "redis": "连接成功",
  "jasyptType": "2",
  "status": "配置刷新成功"
}

深层原理分析

Spring Cloud配置刷新生命周期

graph TD
    A[配置发布到Nacos] --> B[Nacos通知客户端]
    B --> C[客户端接收配置变更]
    C --> D{是否调用refreshEnvironment?}
    D -->|否| E[配置停留在PropertySource层]
    D -->|是| F[发送RefreshEvent事件]
    F --> G[重新加载所有配置源]
    G --> H[更新Environment对象]
    H --> I[销毁@RefreshScope Bean]
    I --> J[重新创建Bean并注入新配置]
    E --> K[应用仍使用旧配置]
    J --> L[应用使用新配置]

Jasypt初始化时机

// Jasypt在应用启动时初始化
@Bean
public StringEncryptor stringEncryptor(ConfigurableEnvironment env) {
    // 此时读取的是启动时的Environment配置
    String jasyptType = env.getProperty("jasypt.jasyptType");
    
    // 如果后续配置更新,但Environment未刷新
    // 这个bean仍然持有旧的配置信息
    return createEncryptor(jasyptType);
}

为什么需要主动刷新

  1. Nacos客户端: 只负责拉取最新配置到本地缓存
  2. PropertySource: 配置存储在PropertySource中,但不会自动触发Environment更新
  3. Bean生命周期: 已创建的单例bean不会因为配置变更而重新创建
  4. 手动触发: 需要通过ContextRefresher手动触发刷新流程

最佳实践建议

1. 配置发布后必须刷新

// ✅ 正确做法
boolean success = configService.publishConfig(dataId, group, content, type);
if (success) {
    contextRefresher.refreshEnvironment();  // 必须添加
}

// ❌ 错误做法  
configService.publishConfig(dataId, group, content, type);
// 缺少刷新步骤

2. 合理使用@RefreshScope

// ✅ 需要动态刷新的配置类
@Component
@RefreshScope
public class DynamicConfig {
    @Value("${dynamic.property}")
    private String property;
}

// ❌ 不要在性能敏感的bean上使用
@Service
@RefreshScope  // 不推荐:每次刷新都会重新创建,影响性能
public class HighFrequencyService {
    // 高频调用的服务
}

3. 监控配置刷新

@EventListener
public void handleRefreshEvent(RefreshEvent event) {
    log.info("配置刷新事件: {}", event.getEventDesc());
    // 可以添加监控和告警逻辑
}

4. 异常处理

public void refreshConfigSafely() {
    try {
        boolean success = publishConfig();
        if (success) {
            contextRefresher.refreshEnvironment();
            log.info("配置刷新成功");
        }
    } catch (Exception e) {
        log.error("配置刷新失败", e);
        // 可以考虑重试机制
    }
}

总结

这个问题看似简单,实际上涉及了Spring Cloud配置管理的核心机制。关键点在于理解:

  1. 配置更新 ≠ 配置生效: Nacos配置更新只是第一步
  2. Environment刷新: 需要主动触发Environment的刷新
  3. Bean生命周期: @RefreshScope确保bean能够重新创建
  4. 时机很重要: 必须在配置发布成功后立即刷新

参考资料