怎麼把自己的配置放到 Spring 環境中
Apollo 5 — 教你怎麼把自己的配置放到 Spring 環境中
參考URL: https://www.jianshu.com/p/cd6824f0672d
有的時候,你可能需要在 Spring 環境中放入一些配置,但這些配置無法寫死在配置文件中,只能運行時放入。那麼,這個時候該怎麼辦呢?
Apollo 就是搞配置的,那麼自然會遇到這個問題,他是如何處理的呢?
首先要知道 Spring 環境中,一個配置的數據結構是什麼?
是抽象類 PropertySource, 內部是個 key value 結構。這個 T 可以是任意類型,取決於子類的設計。
子類可以通過重寫 getProperty 抽象方法獲取配置。
Spring 自身的 org.springframework.core.env.MapPropertySource 就重寫了這個方法。
public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> {
public MapPropertySource(String name, Map<String, Object> source) {
super(name, source);
}
@Override
public Object getProperty(String name) {
return this.source.get(name);
}
@Override
public boolean containsProperty(String name) {
return this.source.containsKey(name);
}
@Override
public String[] getPropertyNames() {
return StringUtils.toStringArray(this.source.keySet());
}
}
Apollo 就直接利用了這個類。
兩個不同的子類,不同的刷新邏輯。我們暫時不關心他們的不同。
這兩個類都會被 RefreshableConfig 組合,添加到 Spring 的環境中。
import org.springframework.core.env.ConfigurableEnvironment;
public abstract class RefreshableConfig {
@Autowired
private ConfigurableEnvironment environment; // Spring 環境
@PostConstruct
public void setup() {
// 省略代碼
for (RefreshablePropertySource propertySource : propertySources) {
propertySource.refresh();
// 注意:成功刷新後,放到 Spring 的環境中
environment.getPropertySources().addLast(propertySource);
}
// 省略代碼
當從 Spring 的環境中獲取配置的時候,具體代碼是下面這樣的:
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
for (PropertySource<?> propertySource : this.propertySources) {
// 注意:這裏調用的就是 propertySource.getProperty 方法,子類剛剛重寫的方法
Object value = propertySource.getProperty(key);
// 省略無關代碼........
return convertValueIfNecessary(value, targetValueType);
}
return null;
}
Spring 維護了一個 PropertySource 的集合,這個結合是有順序的,也就是說,排在最前面的優先級最高(遍歷從下標 0 開始)。
而用戶可以在 PropertySource 裏,維護一個配置字典(Map),這樣,就類似 2 維數組的這樣一個數據結構。
所以,配置是可以重名的,重名時,以最前面的 PropertySource 中的配置爲準。所以,Spring 留給了幾個 API:
addFirst(PropertySource<?> propertySource)
addLast(PropertySource<?> propertySource)
addBefore(String relativePropertySourceName, PropertySource<?> propertySource)
addAfter(String relativePropertySourceName, PropertySource<?> propertySource)
從名字可以看出,通過這些 API,我們可以將 propertySource 插入到我們指定的地方。從而可以手動控制配置的優先級。
Spring 中有個現成的 CompositePropertySource 類,內部聚合了一個 PropertySource Set 集合,當 getProperty(String name) 的時候,就會遍歷這個集合,然後調用這個 propertySource 的 getProperty(name) 方法。相當於 3 維數組。
一個環境中,有多個 PS(PropertySource 簡稱),每個 PS 可以直接包含配置,也可以再包裝一層 PS。
簡單例子
我們這裏有個簡單的例子,需求:
程序裏有個配置,但不能寫死在配置文件中,只能在程序啓動過程中進行配置,然後注入到 Spring 環境中,讓 Spring 在之後的 IOC 中,可以正常的使用這些配置。
@SpringBootApplication
public class DemoApplication {
@Value("${timeout:1}")
String timeout;
public static void main(String[] args) throws InterruptedException {
ApplicationContext c = SpringApplication.run(DemoApplication.class, args);
for (; ; ) {
Thread.sleep(1000);
System.out.println(c.getBean(DemoApplication.class).timeout);
}
}
}
application.properties 配置文件
timeout=100
上面的代碼中,我們在 bean 中定義了一個屬性 timeout, 並在本地配置文件中寫入了一個 100 的值,也在表達式中給了一個默認值 1。
那麼現在打印出來的就是配置文件中的值:100.
但是,這不是我們想要的結果,所以需要修改代碼。
我們加入一個類:
@Component
class Test implements EnvironmentAware, BeanFactoryPostProcessor {
@Override
public void setEnvironment(Environment environment) {
((ConfigurableEnvironment) environment).getPropertySources()
// 這裏是 addFirst,優先級高於 application.properties 配置
.addFirst(new PropertySource<String>("timeoutConfig", "12345") {
// 重點
@Override
public Object getProperty(String s) {
if (s.equals("timeout")) {//
return source;// 返回構造方法中的 source :12345
}
return null;
}
});
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
// NOP
}
}
運行之後,結果:12345
爲什麼加入了這個類,就能夠代替配置文件中的屬性呢?解釋一下這個類的作用。
我們要做的事情就是在 Spring 的環境中,插入自定義的 PS 對象,以便容器獲取的時候,能夠通過 getProperty 方法獲取對應的配置。
所以,我們要拿到 Spring 環境對象,還需要創建一個 PS 對象,並重寫 getProperty 方法,同時,注意:自己的 PS 配置優先級需要高於容器配置文件的優先級,保險起見,放在第一位。
PS 構造方法的第一個參數沒什麼用,就是一個標識符,第二個參數就是 source,可以定義爲任何類型,String,Map,都可以,我們這裏簡單期間,就是一個 String,直接返回這個值,如果是 Map,就調用 Map 的 get 方法。
爲什麼要實現 BeanFactoryPostProcessor 接口呢? 實現 BeanFactoryPostProcessor 接口的目的是讓該 Bean 的加載時機提前,高於目標 Bean 的初始化。否則,目標 Bean 中的 timeout 屬性都注入結束了,後面的操作就沒有意義了。
總結:需要拿到 Spirng 的環境對象,並向環境中添加自定義的 PS 對象,重寫 PS 的 getProperty 方法,即可獲取配置(注意優先級)。還需要注意加載這個配置的 bean 的優先級也要很高,通常實BeanFactoryPostProcessor 接口就足夠了。