hello,大家好,我是小黑,好久不見~~
這是關於配置中心的系列文章,應該會分多篇釋出,內容大致包括:
1、Spring 是如何實現 @Value 注入的
2、一個簡易版配置中心的關鍵技術
3、開源主流配置中心相關技術
@Value 注入過程
從一個最簡單的程式開始:
@Configuration
@PropertySource("classpath:application.properties")
public class ValueAnnotationDemo {
@Value("${username}")
private String username;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueAnnotationDemo.class);
System.out.println(context.getBean(ValueAnnotationDemo.class).username);
context.close();
}
}
application.properties 檔案內容:
username=coder-xiao-hei
由 AutowiredAnnotationBeanPostProcessor
負責來處理 @Value
,此外該類還負責處理 @Autowired
和 @Inject
。
在 AutowiredAnnotationBeanPostProcessor
中有兩個內部類:AutowiredFieldElement
和 AutowiredMethodElement
。
當前為 Field 注入,定位到 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject
方法。
通過 debug 可知,整個呼叫鏈如下:
AutowiredFieldElement#inject
DefaultListableBeanFactory#resolveDependency
DefaultListableBeanFactory#doResolveDependency
AbstractBeanFactory#resolveEmbeddedValue
通過上述的 debug 跟蹤發現可以通過呼叫 ConfigurableBeanFactory#resolveEmbeddedValue
方法可以獲取佔位符的值。
這裡的 resolver
是一個 lambda表示式,繼續 debug 我們可以找到具體的執行方法:
到此,我們簡單總結下:
@Value
的注入由AutowiredAnnotationBeanPostProcessor
來提供支援- 在
AutowiredAnnotationBeanPostProcessor
中通過呼叫ConfigurableBeanFactory#resolveEmbeddedValue
來獲取佔位符具體的值 ConfigurableBeanFactory#resolveEmbeddedValue
其實是委託給了ConfigurableEnvironment
來實現
Spring Environment
Environment 概述
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-environment
The Environment interface is an abstraction integrated in the container that models two key aspects of the application environment: profiles and properties.
A profile is a named, logical group of bean definitions to be registered with the container only if the given profile is active. Beans may be assigned to a profile whether defined in XML or with annotations. The role of the Environment object with relation to profiles is in determining which profiles (if any) are currently active, and which profiles (if any) should be active by default.
Properties play an important role in almost all applications and may originate from a variety of sources: properties files, JVM system properties, system environment variables, JNDI, servlet context parameters, ad-hoc Properties objects, Map objects, and so on. The role of the Environment object with relation to properties is to provide the user with a convenient service interface for configuring property sources and resolving properties from them.
Environment 是對 profiles 和 properties 的抽象:
- 實現了對屬性配置的統一儲存,同時 properties 允許有多個來源
- 通過 Environment profiles 來實現條件化裝配 Bean
現在我們主要來關注 Environment 對 properties 的支援。
StandardEnvironment
下面,我們就來具體看一下 AbstractApplicationContext#finishBeanFactoryInitialization
中的這個 lambda 表示式。
strVal -> getEnvironment().resolvePlaceholders(strVal)
首先,通過 AbstractApplicationContext#getEnvironment
獲取到了 ConfigurableEnvironment
的例項物件,這裡建立的其實是 StandardEnvironment
例項物件。
在 StandardEnvironment
中,預設新增了兩個自定義的屬性源,分別是:systemEnvironment 和 systemProperties。
也就是說,@Value
預設是可以注入 system properties 和 system environment 的。
PropertySource
StandardEnvironment
繼承了 AbstractEnvironment
。
在 AbstractEnvironment
中的屬性配置被存放在 MutablePropertySources
中。同時,屬性佔位符的資料也來自於此。
MutablePropertySources
中存放了多個 PropertySource
,並且這些 PropertySource
是有順序的。
PropertySource
是 Spring 對配置屬性源的抽象。
name 表示當前屬性源的名稱。source 存放了當前的屬性。
讀者可以自行檢視一下最簡單的基於 Map
的實現:MapPropertySource
。
配置屬性源
有兩種方式可以進行屬性源配置:使用 @PropertySource
註解,或者通過 MutablePropertySources
的 API。例如:
@Configuration
@PropertySource("classpath:application.properties")
public class ValueAnnotationDemo {
@Value("${username}")
private String username;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueAnnotationDemo.class);
Map<String, Object> map = new HashMap<>();
map.put("my.name", "coder小黑");
context.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("coder-xiaohei-test", map));
}
}
總結
- Spring 通過
PropertySource
來抽象配置屬性源,PropertySource
允許有多個。MutablePropertySources
- 在 Spring 容器啟動的時候,會預設載入 systemEnvironment 和 systemProperties。
StandardEnvironment#customizePropertySources
- 我們可以通過
@PropertySource
註解或者MutablePropertySources API
來新增自定義配置屬性源 Environment
是 Spring 對 profiles 和 properties 的抽象,預設實現是StandardEnvironment
@Value
的注入由AutowiredAnnotationBeanPostProcessor
來提供支援,資料來源來自於PropertySource
public class Demo {
@Value("${os.name}") // 來自 system properties
private String osName;
@Value("${user.name}") // 通過 MutablePropertySources API 來註冊
private String username;
@Value("${os.version}") // 測試先後順序
private String osVersion;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Demo.class);
ConfigurableEnvironment environment = context.getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("user.name", "xiaohei");
source.put("os.version", "version-for-xiaohei");
// 新增自定義 MapPropertySource,且放在第一位
propertySources.addFirst(new MapPropertySource("coder-xiao-hei-test", source));
// 啟動容器
context.refresh();
Demo bean = context.getBean(Demo.class);
// Mac OS X
System.out.println(bean.osName);
// xiaohei
System.out.println(bean.username);
// version-for-xiaohei
System.out.println(bean.osVersion);
// Mac OS X
System.out.println(System.getProperty("os.name"));
// 10.15.7
System.out.println(System.getProperty("os.version"));
// xiaohei
System.out.println(environment.getProperty("user.name"));
//xiaohei
System.out.println(environment.resolvePlaceholders("${user.name}"));
context.close();
}
}
簡易版配置中心
@Value 支援配置中心資料來源
@Value
的值都來源於 PropertySource
,而我們可以通過 API 的方式來向 Spring Environment 中新增自定義的 PropertySource
。
在此處,我們選擇通過監聽 ApplicationEnvironmentPreparedEvent
事件來實現。
@Slf4j
public class CentralConfigPropertySourceListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
private final CentralConfig centralConfig = new CentralConfig();
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
centralConfig.loadCentralConfig();
event.getEnvironment().getPropertySources().addFirst(new CentralConfigPropertySource(centralConfig));
}
static class CentralConfig {
private volatile Map<String, Object> config = new HashMap<>();
private void loadCentralConfig() {
// 模擬從配置中心獲取資料
config.put("coder.name", "xiaohei");
config.put("coder.language", "java");
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模擬配置更新
config.put("coder.language", "java222");
System.out.println("update 'coder.language' success");
}).start();
}
}
static class CentralConfigPropertySource extends EnumerablePropertySource<CentralConfig> {
private static final String PROPERTY_SOURCE_NAME = "centralConfigPropertySource";
public CentralConfigPropertySource(CentralConfig source) {
super(PROPERTY_SOURCE_NAME, source);
}
@Override
@Nullable
public Object getProperty(String name) {
return this.source.config.get(name);
}
@Override
public boolean containsProperty(String name) {
return this.source.config.containsKey(name);
}
@Override
public String[] getPropertyNames() {
return StringUtils.toStringArray(this.source.config.keySet());
}
}
}
通過 META-INF/spring.factories
檔案來註冊:
org.springframework.context.ApplicationListener=com.example.config.CentralConfigPropertySourceListener
實時釋出更新配置
一般來說有兩種方案:
-
客戶端拉模式:客戶端長輪詢服務端,如果服務端資料發生修改,則立即返回給客戶端
-
服務端推模式:釋出更新配置之後,由配置中心主動通知各客戶端
- 在這裡我們選用服務端推模式來進行實現。在叢集部署環境下,一旦某個配置中心服務感知到了配置項的變化,就會通過 redis 的 pub/sub 來通知客戶端和其他的配置中心服務節點
- 輕量級實現方案,程式碼簡單,但強依賴 redis,pub/sub 可以會有丟失
自定義註解支援動態更新配置
Spring 的 @Value
注入是在 Bean 初始化階段執行的。在程式執行過程當中,配置項發生了變更, @Value
並不會重新注入。
我們可以通過增強 @Value
或者自定義新的註解來支援動態更新配置。這裡小黑選擇的是第二種方案,自定義新的註解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigValue {
String value();
}
@Component
public class ConfigValueAnnotationBeanPostProcessor implements BeanPostProcessor, EnvironmentAware {
private static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
new PropertyPlaceholderHelper(
SystemPropertyUtils.PLACEHOLDER_PREFIX,
SystemPropertyUtils.PLACEHOLDER_SUFFIX,
SystemPropertyUtils.VALUE_SEPARATOR,
false);
private MultiValueMap<String, ConfigValueHolder> keyHolder = new LinkedMultiValueMap<>();
private Environment environment;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
ReflectionUtils.doWithFields(bean.getClass(),
field -> {
ConfigValue annotation = AnnotationUtils.findAnnotation(field, ConfigValue.class);
if (annotation == null) {
return;
}
String value = environment.resolvePlaceholders(annotation.value());
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, bean, value);
String key = PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(annotation.value(), placeholderName -> placeholderName);
ConfigValueHolder configValueHolder = new ConfigValueHolder(bean, beanName, field, key);
keyHolder.add(key, configValueHolder);
});
return bean;
}
/**
* 當配置發生了修改
*
* @param key 配置項
*/
public void update(String key) {
List<ConfigValueHolder> configValueHolders = keyHolder.get(key);
if (CollectionUtils.isEmpty(configValueHolders)) {
return;
}
String property = environment.getProperty(key);
configValueHolders.forEach(holder -> ReflectionUtils.setField(holder.field, holder.bean, property));
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@AllArgsConstructor
static class ConfigValueHolder {
final Object bean;
final String beanName;
final Field field;
final String key;
}
}
主測試程式碼:
@SpringBootApplication
public class ConfigApplication {
@Value("${coder.name}")
String coderName;
@ConfigValue("${coder.language}")
String language;
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext context = SpringApplication.run(ConfigApplication.class, args);
ConfigApplication bean = context.getBean(ConfigApplication.class);
// xiaohei
System.out.println(bean.coderName);
// java
System.out.println(bean.language);
ConfigValueAnnotationBeanPostProcessor processor = context.getBean(ConfigValueAnnotationBeanPostProcessor.class);
// 模擬配置發生了更新
TimeUnit.SECONDS.sleep(10);
processor.update("coder.language");
// java222
System.out.println(bean.language);
}
}