Jasypt与Apollo一起使用造成Apollo热更新失效问题分析

文章讲述了在Apollo配置管理系统中,由于Jasypt的加密缓存机制导致配置无法热更新的问题。作者通过远程debug发现,配置变更时Jasypt的缓存未更新,从而阻止了配置的即时生效。文中还探讨了配置为何会从Jasypt类中获取以及如何在Apollo配置变更时刷新Jasypt的缓存,并提出了几种可能的解决方案。
摘要由CSDN通过智能技术生成

背景

近日业务同学反映在Apollo界面更改配置后, 服务中对应变量的值却没有改变
相关配置key定义如下:

@ApolloJsonValue("${apollo.config.map:{}}")
private Map<String, List<String>> apolloConfigMap;

分析

问题确认

通过远程debug服务发现,更改apollo配置后,服务中变量的值确实没有改变。 重启也不行。

尝试本地复现

在本地编写demo,按照如上变量配置方式配置, 多次修改apollo配置后,变量的值都能即时热更新, 本地复现失败

远程debug

  1. 将项目的代码clone到本地,远程debug
  2. 在apollo热更新代码处打断点,具体是: AutoUpdateConfigChangeListener#onChange方法。
public void onChange(ConfigChangeEvent changeEvent) {
  Set<String> keys = changeEvent.changedKeys();
  if (CollectionUtils.isEmpty(keys)) {
    return;
  }
  for (String key : keys) {
    // 1. check whether the changed key is relevant
    Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
    if (targetValues == null || targetValues.isEmpty()) {
      continue;
    }

    // 2. check whether the value is really changed or not (since spring property sources have hierarchies)
    if (!shouldTriggerAutoUpdate(changeEvent, key)) {
      continue;
    }

    // 3. update the value
    for (SpringValue val : targetValues) {
      updateSpringValue(val);
    }
  }
}

这个方法比较简单,循环变更的key, 第一步校验变更的key确实是bean中的属性,第二步校验确实需要热更新bean中属性值,第三步是真正的热更新。
3. 通过调试发现,在第二步时,shouldTriggerAutoUpdate方法返回了false,导致不会进行热更新。
4. 我们来看下shouldTriggerAutoUpdate方法

private boolean shouldTriggerAutoUpdate(ConfigChangeEvent changeEvent, String changedKey) {
  ConfigChange configChange = changeEvent.getChange(changedKey);

  if (configChange.getChangeType() == PropertyChangeType.DELETED) {
    return true;
  }

  return Objects.equals(environment.getProperty(changedKey), configChange.getNewValue());
}

逻辑比较简单,返回false的是最后一句, environment中获取到的属性值与apollo中配置的新值不一样。
5. 为什么会不一样?
经过调试发现 key:apollo.config.map的值最终是从com.ulisesbocchio.jasyptspringboot.caching.CachingDelegateEncryptablePropertySource中获取,而此类中有一个cache, apollo配置变更时,此cache中存的仍是旧配置。此类是jasypt相关包中的类,此包是与加解密相关的。

关键代码如下:

public Object getProperty(String name) {
    // Can be called recursively, so, we cannot use computeIfAbsent.
    if (cache.containsKey(name)) {
        return cache.get(name);
    }
    synchronized (name.intern()) {
        if (!cache.containsKey(name)) {
            Object resolved = getProperty(resolver, filter, delegate, name);
            if (resolved != null) {
                cache.put(name, resolved);
            }
        }
        return cache.get(name);
    }
}

结论

因为Jasypt会封装Apollo的PropertySource类,缓存属性值,导致配置不能热更新

延伸思考

1. 为什么apollo的配置会从jasypt类中获取呢?

我们来看下com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter这个类,这是一个property converter。它的作用即是封装服务中各种的PropertySource, 当服务查询配置的值时,如果配置需要解密的话,可以实现解密。而Apollo也会创建一个PropertySource对象, 也会被jasypt包装,导致配置变更时cache无法更新。

2. 能不能apollo配置变更时更新cache或使cache失效

CachingDelegateEncryptablePropertySource类确实有一个refresh方法,可以清空缓存,下次再查询属性值时,会从真正的PropertySource中获取。而refresh方法是在com.ulisesbocchio.jasyptspringboot.caching.RefreshScopeRefreshedEventListener#onApplicationEvent方法中被调用。可以看出,如果apollo配置变更时发送事件,jasypt的onApplicationEvent应该可以被触发,并清空cache。
经过验证确实可以通过编写一个Apollo配置变更监听器,在监听器中发送ApplicationEvent事件,达到清空Cache的目的。但是经过验证,自己定义的监听器,在AutoUpdateConfigChangeListener#onChange之后执行,还是无法热更新。
Apollo将AutoUpdateConfigChangeListener监听器是放在监听器集合中的第一位的,第一个执行。所以必要要更改的话,需要更改AutoUpdateConfigChangeListener的逻辑,首先发送事件,然后再执行onChange方法中的第二步。 但Apollo将AutoUpdateConfigChangeListener放一位也是有道理的,配置变更先更新配置,再执行其它监听器,因为在其它监听器中也许需要用到热更新后的值。

解决方法

解决方法有三种,需要根据使用的场景不同选择不同的方法

  1. 如果需要用到动态配置,并且动态配置是加密的,就需要修改AutoUpdateConfigChangeListener逻辑,先发送事件。注意新增事件类后,需要配置jasypt.encryptor.refreshed-event-classes,其值为事件类的全限定名称。
  2. 如果需要用到动态配置,但动态配置是不需要加密的,需要修改EncryptablePropertySourceConverter类,使其不包装Apollo相关的PropertySource类。
public void convertPropertySources(MutablePropertySources propSources) {
    propSources.stream()
            .filter(ps -> !(ps instanceof EncryptablePropertySource))
            .filter(ps -> !(ps instanceof CompositePropertySource && ps.getName().startsWith("Apollo")))
            .map(this::makeEncryptable)
            .collect(toList())
            .forEach(ps -> propSources.replace(ps.getName(), ps));
}
  1. 不使用Apollo的热更新,属性值直接调用Apolo的Config获取,也能获取到变更后的值。伪代码如下:
Config apolloConfig = ConfigService.getConfig(<namespace>)
apolloConfig.getProperty()

工作十几年收集了很多有用的工具,有需要的朋友欢迎关注我的公众号免费领取
在这里插入图片描述

喜欢我的文章,欢迎关注我的公众号;分享技术、生活故事,做一个有故事的技术人

Jasypt是一个Java库,用于对敏感数据进行加密和解密,例如密码、API密钥等。Spring框架提供了对Jasypt的支持,可以轻松地将其与Spring集成。 下面是使用Jasypt和Spring实现加密和解密的步骤: 1. 添加Jasypt依赖 在Maven项目中,可以将以下依赖添加到pom.xml文件中: ```xml <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>${jasypt.version}</version> </dependency> ``` 其中,${jasypt.version}为Jasypt的版本号。 2. 配置加密算法 在Spring的配置文件(例如application.yml或application.properties)中,需要指定所使用的加密算法和密钥。例如: ```yaml jasypt: encryptor: algorithm: PBEWithMD5AndDES password: mySecretKey ``` 这里使用了PBEWithMD5AndDES算法,并将密钥设置为mySecretKey。 3. 在代码中使用加密和解密 要在代码中使用加密和解密,可以使用Spring的Environment对象。例如: ```java @Autowired private Environment environment; public void someMethod() { String encryptedPassword = environment.getProperty("my.password.property"); StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); encryptor.setPassword(environment.getProperty("jasypt.encryptor.password")); String password = encryptor.decrypt(encryptedPassword); } ``` 在上面的代码中,我们首先从环境变量中获取加密的密码,然后使用Jasypt进行解密。要使用Jasypt的默认配置,可以使用以下代码: ```java String password = environment.getProperty("my.password.property"); ``` 这将自动使用配置文件中指定的加密算法和密钥进行解密。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值