目錄:
- 前言
- 處理方案
- 簡單例子
前言
有的時候,你可能需要在 Spring 環(huán)境中放入一些配置库快,但這些配置無法寫死在配置文件中摸袁,只能運(yùn)行時放入。那么义屏,這個時候該怎么辦呢靠汁?
Apollo 就是搞配置的,那么自然會遇到這個問題闽铐,他是如何處理的呢蝶怔?
處理方案
首先要知道 Spring 環(huán)境中,一個配置的數(shù)據(jù)結(jié)構(gòu)是什么兄墅?
是抽象類 PropertySource<T>
踢星, 內(nèi)部是個 key value 結(jié)構(gòu)。這個 T 可以是任意類型隙咸,取決于子類的設(shè)計沐悦。
子類可以通過重寫 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());
}
}
可以看到藏否,他的泛型是 Map,getProperty 方法則是從 Map 中獲取充包。
Apollo 就直接利用了這個類副签。
兩個不同的子類,不同的刷新邏輯。我們暫時不關(guān)心他們的不同继薛。
這兩個類都會被 RefreshableConfig 組合修壕,添加到 Spring 的環(huán)境中。
import org.springframework.core.env.ConfigurableEnvironment;
public abstract class RefreshableConfig {
@Autowired
private ConfigurableEnvironment environment; // Spring 環(huán)境
@PostConstruct
public void setup() {
// 省略代碼
for (RefreshablePropertySource propertySource : propertySources) {
propertySource.refresh();
// 注意:成功刷新后遏考,放到 Spring 的環(huán)境中
environment.getPropertySources().addLast(propertySource);
}
// 省略代碼
當(dāng)從 Spring 的環(huán)境中獲取配置的時候慈鸠,具體代碼是下面這樣的:
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
for (PropertySource<?> propertySource : this.propertySources) {
// 注意:這里調(diào)用的就是 propertySource.getProperty 方法,子類剛剛重寫的方法
Object value = propertySource.getProperty(key);
// 省略無關(guān)代碼........
return convertValueIfNecessary(value, targetValueType);
}
return null;
}
Spring 維護(hù)了一個 PropertySource 的集合灌具,這個結(jié)合是有順序的青团,也就是說,排在最前面的優(yōu)先級最高(遍歷從下標(biāo) 0 開始)咖楣。
而用戶可以在 PropertySource 里督笆,維護(hù)一個配置字典(Map),這樣诱贿,就類似 2 維數(shù)組的這樣一個數(shù)據(jù)結(jié)構(gòu)娃肿。
所以,配置是可以重名的珠十,重名時料扰,以最前面的 PropertySource 中的配置為準(zhǔn)。所以焙蹭,Spring 留給了幾個 API:
- addFirst(PropertySource<?> propertySource)
- addLast(PropertySource<?> propertySource)
- addBefore(String relativePropertySourceName, PropertySource<?> propertySource)
- addAfter(String relativePropertySourceName, PropertySource<?> propertySource)
從名字可以看出晒杈,通過這些 API,我們可以將 propertySource 插入到我們指定的地方孔厉。從而可以手動控制配置的優(yōu)先級拯钻。
Spring 中有個現(xiàn)成的 CompositePropertySource 類,內(nèi)部聚合了一個 PropertySource Set 集合撰豺,當(dāng) getProperty(String name) 的時候粪般,就會遍歷這個集合,然后調(diào)用這個 propertySource 的 getProperty(name) 方法郑趁。相當(dāng)于 3 維數(shù)組刊驴。
大概的設(shè)計是這樣:
一個環(huán)境中,有多個 PS(PropertySource 簡稱)寡润,每個 PS 可以直接包含配置,也可以再包裝一層 PS舅柜。
簡單例子
我們這里有個簡單的例子梭纹,需求:
程序里有個配置,但不能寫死在配置文件中致份,只能在程序啟動過程中進(jìn)行配置变抽,然后注入到 Spring 環(huán)境中,讓 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 的值击儡,也在表達(dá)式中給了一個默認(rèn)值 1塔沃。
那么現(xiàn)在打印出來的就是配置文件中的值:100.
但是,這不是我們想要的結(jié)果阳谍,所以需要修改代碼蛀柴。
我們加入一個類:
@Component
class Test implements EnvironmentAware, BeanFactoryPostProcessor {
@Override
public void setEnvironment(Environment environment) {
((ConfigurableEnvironment) environment).getPropertySources()
// 這里是 addFirst,優(yōu)先級高于 application.properties 配置
.addFirst(new PropertySource<String>("timeoutConfig", "12345") {
// 重點
@Override
public Object getProperty(String s) {
if (s.equals("timeout")) {//
return source;// 返回構(gòu)造方法中的 source :12345
}
return null;
}
});
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
// NOP
}
}
運(yùn)行之后,結(jié)果:12345
2018-07-02 15:26:54.315 INFO 43393 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-07-02 15:26:54.327 INFO 43393 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.991 seconds (JVM running for 1.49)
12345
12345
為什么加入了這個類矫夯,就能夠代替配置文件中的屬性呢鸽疾?解釋一下這個類的作用。
我們要做的事情就是在 Spring 的環(huán)境中训貌,插入自定義的 PS 對象制肮,以便容器獲取的時候,能夠通過 getProperty 方法獲取對應(yīng)的配置递沪。
所以豺鼻,我們要拿到 Spring 環(huán)境對象,還需要創(chuàng)建一個 PS 對象区拳,并重寫 getProperty 方法拘领,同時,注意:自己的 PS 配置優(yōu)先級需要高于容器配置文件的優(yōu)先級樱调,保險起見约素,放在第一位。
PS 構(gòu)造方法的第一個參數(shù)沒什么用笆凌,就是一個標(biāo)識符圣猎,第二個參數(shù)就是 source,可以定義為任何類型乞而,String送悔,Map,都可以爪模,我們這里簡單期間欠啤,就是一個 String,直接返回這個值屋灌,如果是 Map洁段,就調(diào)用 Map 的 get 方法。
為什么要實現(xiàn) BeanFactoryPostProcessor 接口呢共郭? 實現(xiàn) BeanFactoryPostProcessor 接口的目的是讓該 Bean 的加載時機(jī)提前祠丝,高于目標(biāo) Bean 的初始化疾呻。否則,目標(biāo) Bean 中的 timeout 屬性都注入結(jié)束了写半,后面的操作就沒有意義了岸蜗。
總結(jié)
說白了,就是不想寫配置文件5Aг馈!
而且也不想改老項目的代碼蟆肆,老項目即使在刪除配置文件的情況下矾睦,依然能夠使用配置中心!
這就需要熟悉 Spring 的配置加載邏輯和屬性獲取邏輯炎功。
現(xiàn)在枚冗,我們知道,只需要拿到 Spirng 的環(huán)境對象蛇损,并向環(huán)境中添加自定義的 PS 對象赁温,重寫 PS 的 getProperty 方法,即可獲取配置(注意優(yōu)先級)淤齐。
還需要注意加載這個配置的 bean 的優(yōu)先級也要很高股囊,通常實現(xiàn) BeanFactoryPostProcessor 接口就足夠了,如果還不夠更啄,就需要做一些特殊操作稚疹。