项目中实现了多种Jasypt加密器:
@Getter
@AllArgsConstructor
public enum JasyptEnum {
DEFAULT_JASYPT(-1, "默认加解密器:未指定加解密器"),
SUWELL_JASYPT(1, "xxx标品加解密器"),
WPSC_JASYPT(2, "xxxx加解密器"),
}
配置项包括:
jasypt.jasyptType
: 加密器类型jasypt.keyId
: 密钥IDjasypt.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配置更新后不会生效
}
}
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配置的问题:
# bootstrap.yml 中的配置
spring:
cloud:
nacos:
config:
shared-configs:
- dataId: plss-config.yml
group: COMMON
refresh: true # 已开启自动刷新
接着我们检查了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也不会重新创建。
通过源码分析,我们发现了关键问题:
在配置发布成功后,添加一行关键代码:
@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()
的工作机制:
为了让更多配置类能够动态刷新,我们在项目中大量使用了@RefreshScope注解:
@Component
@RefreshScope // 配置更新时会重新创建这个bean
public class CustomProperties {
@Value("${custom.config.value}")
private String configValue;
// getter/setter...
}
项目中共有142个类使用了@RefreshScope注解,涵盖:
我们通过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": "配置刷新成功"
}
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在应用启动时初始化
@Bean
public StringEncryptor stringEncryptor(ConfigurableEnvironment env) {
// 此时读取的是启动时的Environment配置
String jasyptType = env.getProperty("jasypt.jasyptType");
// 如果后续配置更新,但Environment未刷新
// 这个bean仍然持有旧的配置信息
return createEncryptor(jasyptType);
}
// ✅ 正确做法
boolean success = configService.publishConfig(dataId, group, content, type);
if (success) {
contextRefresher.refreshEnvironment(); // 必须添加
}
// ❌ 错误做法
configService.publishConfig(dataId, group, content, type);
// 缺少刷新步骤
// ✅ 需要动态刷新的配置类
@Component
@RefreshScope
public class DynamicConfig {
@Value("${dynamic.property}")
private String property;
}
// ❌ 不要在性能敏感的bean上使用
@Service
@RefreshScope // 不推荐:每次刷新都会重新创建,影响性能
public class HighFrequencyService {
// 高频调用的服务
}
@EventListener
public void handleRefreshEvent(RefreshEvent event) {
log.info("配置刷新事件: {}", event.getEventDesc());
// 可以添加监控和告警逻辑
}
public void refreshConfigSafely() {
try {
boolean success = publishConfig();
if (success) {
contextRefresher.refreshEnvironment();
log.info("配置刷新成功");
}
} catch (Exception e) {
log.error("配置刷新失败", e);
// 可以考虑重试机制
}
}
这个问题看似简单,实际上涉及了Spring Cloud配置管理的核心机制。关键点在于理解: