為什麼 @Value 可以獲取配置中心的值?

Coder小黑發表於2020-11-24

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 構造器

AutowiredAnnotationBeanPostProcessor 中有兩個內部類:AutowiredFieldElementAutowiredMethodElement

當前為 Field 注入,定位到 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject 方法。

AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject

通過 debug 可知,整個呼叫鏈如下:

  • AutowiredFieldElement#inject
    • DefaultListableBeanFactory#resolveDependency
      • DefaultListableBeanFactory#doResolveDependency
        • AbstractBeanFactory#resolveEmbeddedValue

DefaultListableBeanFactory#doResolveDependency

通過上述的 debug 跟蹤發現可以通過呼叫 ConfigurableBeanFactory#resolveEmbeddedValue 方法可以獲取佔位符的值。

ConfigurableBeanFactory#resolveEmbeddedValue

這裡的 resolver 是一個 lambda表示式,繼續 debug 我們可以找到具體的執行方法:

AbstractApplicationContext#finishBeanFactoryInitialization

到此,我們簡單總結下:

  1. @Value 的注入由 AutowiredAnnotationBeanPostProcessor 來提供支援
  2. AutowiredAnnotationBeanPostProcessor 中通過呼叫 ConfigurableBeanFactory#resolveEmbeddedValue 來獲取佔位符具體的值
  3. 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。

StandardEnvironment

也就是說,@Value 預設是可以注入 system properties 和 system environment 的。

PropertySource

StandardEnvironment 繼承了 AbstractEnvironment

AbstractEnvironment 中的屬性配置被存放在 MutablePropertySources 中。同時,屬性佔位符的資料也來自於此。

AbstractEnvironment 成員變數

MutablePropertySources 中存放了多個 PropertySource ,並且這些 PropertySource 是有順序的。

MutablePropertySources#get

PropertySource 是 Spring 對配置屬性源的抽象。

PropertySource

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));
    }
}

總結

  1. Spring 通過 PropertySource 來抽象配置屬性源, PropertySource 允許有多個。MutablePropertySources
  2. 在 Spring 容器啟動的時候,會預設載入 systemEnvironment 和 systemProperties。StandardEnvironment#customizePropertySources
  3. 我們可以通過 @PropertySource 註解或者 MutablePropertySources API 來新增自定義配置屬性源
  4. Environment 是 Spring 對 profiles 和 properties 的抽象,預設實現是 StandardEnvironment
  5. @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);
    }
}

相關文章