從 Spring 的環境到 Spring Cloud 的配置

需求


不知不覺,web 開發已經進入 “微服務”、”分佈式” 的時代,致力於提供通用 Java 開發解決方案的 Spring 自然不甘人後,提出了 Spring Cloud 來擴大 Spring 在微服務方面的影響,也取得了市場的認可,在我們的業務中也有應用。

前些天,我在一個需求中也遇到了 spring cloud 的相關問題。我們在用的是 Spring Cloud 的 config 模塊,它是用來支持分佈式配置的,原來單機配置在使用了 Spring Cloud 之後,可以支持第三方存儲配置和配置的動態修改和重新加載,自己在業務代碼裡實現配置的重新加載,Spring Cloud 將整個流程抽離為框架,並很好的融入到 Spring 原有的配置和 Bean 模塊內。

雖然在解決需求問題時走了些彎路,但也藉此機會瞭解了 Spring Cloud 的一部分,抽空總結一下問題和在查詢問題中瞭解到的知識,分享出來讓再遇到此問題的同學少踩坑吧。

本文基於 Spring 5.0.5、Spring Boot 2.0.1 和 Spring Cloud 2.0.2。

從 Spring 的環境到 Spring Cloud 的配置


背景和問題


我們的服務原來有一批單機的配置,由於同一 key 的配置太長,於是將其配置為數組的形式,並使用 Spring Boot 的 @ConfigurationProperties 和 @Value 註解來解析為 Bean 屬性。

properties 文件配置像:

<code>test.config.elements[0]=value1
test.config.elements[1]=value2
test.config.elements[2]=value3

/<code>

在使用時:

<code>
@ConfigurationProperties(prefix="test.config")
Class Test{
@Value("${#elements}")
private String[] elements;
}
/<code>

這樣,Spring 會對 Test 類自動注入,將數組 [value1,value2,value3] 注入到 elements 屬性內。

而我們使用 Spring Cloud 自動加載配置的姿勢是這樣:

<code>@RefreshScope
class Test{
@Value("${test.config.elements}")
private String[] elements;
}
/<code>

使用 @RefreshScope 註解的類,在環境變量有變動後會自動重新加載,將最新的屬性注入到類屬性內,但它卻不支持數組的自動注入。

而我的目標是能找到一種方式,使其即支持注入數組類型的屬性,又能使用 Spring Cloud 的自動刷新配置的特性。

環境和屬性


無論Spring Cloud 的特性如何優秀,在 Spring 的地盤,還是要入鄉隨俗,和 Spring 的基礎組件打成一片。所以為了瞭解整個流程,我們就要先了解 Spring 的基礎。

Spring 是一個大容器,它不光存儲 Bean 和其中的依賴,還存儲著整個應用內的配置,相對於 BeanFactory 存儲著各種 Bean,Spring 管理環境配置的容器就是 Environment,從 Environment 內,我們能根據 key 獲取所有配置,還能根據不同的場景(Profile,如 dev,test,prod)來切換配置。

但 Spring 管理配置的最小單位並不是屬性,而是 PropertySource (屬性源),我們可以理解 PropertySource 是一個文件,或是某張配置數據表,Spring 在 Environment 內維護一個 PropertySourceList,當我們獲取配置時,Spring 從這些 PropertySource 內查找到對應的值,並使用 ConversionService 將值轉換為對應的類型返回。

Spring Cloud 配置刷新機制


分佈式配置

Spring Cloud 內提供了 PropertySourceLocator 接口來對接 Spring 的 PropertySource 體系,通過 PropertySourceLocator,我們就拿到一個”自定義”的 PropertySource,Spring Cloud 裡還有一個實現 ConfigServicePropertySourceLocator,通過它,我們可以定義一個遠程的 ConfigService,通過公用這個 ConfigService 來實現分佈式的配置服務。

從 ConfigClientProperties 這個配置類我們可以看得出來,它也為遠程配置預設了用戶名密碼等安全控制選項,還有 label 用來區分服務池等配置。

scope 配置刷新

遠程配置有了,接下來就是對變化的監測和基於配置變化的刷新。

Spring Cloud 提供了 ContextRefresher 來幫助我們實現環境的刷新,其主要邏輯在 refreshEnvironment 方法和 scope.refreshAll() 方法,我們分開來看。

我們先來看 spring cloud 支持的 scope.refreshAll 方法。

<code>    public void refreshAll() {
\t\tsuper.destroy();
\t\tthis.context.publishEvent(new RefreshScopeRefreshedEvent());
\t}
/<code>

scope.refreshAll 則更”野蠻”一些,直接銷燬了 scope,併發布了一個 RefreshScopeRefreshedEvent 事件,scope 的銷燬會導致 scope 內(被 RefreshScope 註解)所有的 bean 都會被銷燬。而這些被強制設置為 lazyInit 的 bean 再次創建時,也就完成了新配置的重新加載。

ConfigurationProperties 配置刷新

然後再回過頭來看 refreshEnvironment 方法。

<code>Map<string> before = extract(this.context.getEnvironment().getPropertySources());
\t\taddConfigFilesToEnvironment();
\t\tSet<string> keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();
\t\tthis.context.publishEvent(new EnvironmentChangeEvent(context, keys));
\t\treturn keys;

/<string>/<string>/<code>

它讀取了環境內所有 PropertySource 內的配置後,重新創建了一個 SpringApplication 以刷新配置,再次讀取所有配置項並得到與前面保存的配置項的對比,最後將前後配置差發布了一個 EnvironmentChangeEvent 事件。 而 EnvironmentChangeEvent 的監聽器是由 ConfigurationPropertiesRebinder 實現的,其主要邏輯在 rebind 方法。

<code>\tObject bean = this.applicationContext.getBean(name);
\tif (AopUtils.isAopProxy(bean)) {
\t\tbean = ProxyUtils.getTargetObject(bean);
\t}
\tif (bean != null) {
\t\tthis.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
\t\treturn true;
/<code>

可以看到它的處理邏輯,就是把其內部存儲的 ConfigurationPropertiesBeans 依次執行銷燬邏輯,再執行初始化邏輯實現屬性的重新綁定。

這裡可以知道,Spring Cloud 在進行配置刷新時是考慮過 ConfigurationProperties 的,經過測試,在 ContextRefresher 刷新上下文後,ConfigurationProperties 註解類的屬性是會進行動態刷新的。

測試一次就解決的事情,感覺有些白忙活了。。不過既然查到這裡了,就再往下深入一些。

Bean 的創建與環境


接著我們再來看一下,環境裡的屬性都是怎麼在 Bean 創建時被使用的。

我們知道,Spring 的 Bean 都是在 BeanFactory 內創建的,創建邏輯的入口在 AbstractBeanFactory.doGetBean(name, requiredType, args, false) 方法,而具體實現在 AbstractAutowireCapableBeanFactory.doCreateBean 方法內,在這個方法裡,實現了 Bean 實例的創建、屬性填充、初始化方法調用等邏輯。

在這裡,有一個非常複雜的步驟就是調用全局的 BeanPostProcessor,這個接口是 Spring 為 Bean 創建準備的勾子接口,實現這個接口的類可以對 Bean 創建時的操作進行修改。它是一個非常重要的接口,是我們能干涉 Spring Bean 創建流程的重要入口。

我們要說的是它的一種具體實現 ConfigurationPropertiesBindingPostProcessor,它通過調用鏈 ConfigurationPropertiesBinder.bind() --> Binder.bindObject() --> Binder.findProperty() 方法查找環境內的屬性。

<code>private ConfigurationProperty findProperty(ConfigurationPropertyName name,
\t\t\tContext context) {
\t\tif (name.isEmpty()) {
\t\t\treturn null;
\t\t}
\t\treturn context.streamSources()
\t\t\t\t.map((source) -> source.getConfigurationProperty(name))
\t\t\t\t.filter(Objects::nonNull).findFirst().orElse(null);
\t}
/<code>

找到對應的屬性後,再使用 converter 將屬性轉換為對應的類型注入到 Bean 骨。

<code>    private  Object bindProperty(Bindable target, Context context,
\t\t\tConfigurationProperty property) {
\t\tcontext.setConfigurationProperty(property);
\t\tObject result = property.getValue();
\t\tresult = this.placeholdersResolver.resolvePlaceholders(result);
\t\tresult = context.getConverter().convert(result, target);
\t\treturn result;

\t}
/<code>

一種 trick 方式


由上面可以看到,Spring 是支持 @ConfigurationProperties 屬性的動態修改的,但在查詢流程時,我也找到了一種比較 trick 的方式。

我們先來整理動態屬性注入的關鍵點,再從這些關鍵點裡找可修改點。

  1. PropertySourceLocator 將 PropertySource 從遠程數據源引入,如果這時我們能修改數據源的結果就能達到目的,可是 Spring Cloud 的遠程資源定位器 ConfigServicePropertySourceLocator 和 遠程調用工具 RestTemplate 都是實現類,如果生硬地對其繼承並修改,代碼很不優雅。
  2. Bean 創建時會依次使用 BeanPostProcessor 對上下文進行操作。這時添加一個 BeanPostProcessor,可以手動實現對 Bean 屬性的修改。但這種方式 實現起來很複雜,而且由於每一個 BeanPostProcessor 在所有 Bean 創建時都會調用,可能會有安全問題。
  3. Spring 會在解決類屬性注入時,使用 PropertyResolver 將配置項解析為類屬性指定的類型。這時候添加屬性解析器 PropertyResolver 或類型轉換器 ConversionService 可以插手屬性的操作。但它們都只負責處理一個屬性,由於我的目標是”多個”屬性變成一個屬性,它們也無能為力。

我這裡能想到的方式是借用 Spring 自動注入的能力,把 Environment Bean 注入到某個類中,然後在類的初始化方法裡對 Environment 內的 PropertySource 裡進行修改,也可以達成目的,這裡貼一下偽代碼。

<code>@Component
@RefreshScope // 借用 Spring Cloud 實現此 Bean 的刷新
public class ListSupportPropertyResolver {
@Autowired
ConfigurableEnvironment env; // 將環境注入到 Bean 內是修改環境的重要前提

@PostConstruct
public void init() {
// 將屬性鍵值對從環境內取出
Map<string> properties = extract(env.getPropertySources());

// 解析環境裡的數組,抽取出其中的數組配置
Map<string>> listProperties = collectListProperties(properties)
Map<string> propertiesMap = new HashMap<>(listProperties);

MutablePropertySources propertySources = env.getPropertySources();
// 把數組配置生成一個 PropertySource 並放到環境的 PropertySourceList 內
propertySources.addFirst(new MapPropertySource("modifiedProperties", propertiesMap));
}
}
/<string>/<string>/<string>/<code>

這樣,在創建 Bean 時,就能第一優先級使用我們修改過的 PropertySource 了。

當然了,有了比較”正規”的方式後,我們不必要對 PropertySource 進行修改,畢竟全局修改等於未知風險或埋坑。

小結


查找答案的過程中,我更深刻地理解到 Environment、BeanFactory 這些才是 Spring 的基石,框架提供的各種花式功能都是基於它們實現的,對這些知識的掌握,對於理解它表現出來的高級特性很有幫助,之後再查找框架問題也會更有方向


分享到:


相關文章: