Jasypt與Apollo一起使用造成Apollo熱更新失效問題分析

2023-01-11 15:05:05

背景

近日業務同學反映在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));
    }
  3. 不使用Apollo的熱更新,屬性值直接呼叫Apolo的Config獲取,也能獲取到變更後的值。虛擬碼如下:
Config apolloConfig = ConfigService.getConfig(<namespace>)
- apolloConfig.getProperty()