Spring配置那些事

一、引言

配置是一個(gè)項(xiàng)目中不那么起眼,但卻有非常重要的東西。在工程項(xiàng)目中,我們一般會(huì)將可修改、易變、不確定的值作為配置項(xiàng),在配置文件/配置中心中設(shè)置。

比方說,不同環(huán)境有不同的數(shù)據(jù)庫地址、不同的線程池大小等,可以通過每個(gè)環(huán)境單獨(dú)配置文件的方式,實(shí)現(xiàn)不修改代碼的情況下修改配置項(xiàng)。

再比方說,我們有一個(gè)功能上線,可能存在兼容性問題,我們需要在開始的時(shí)候開關(guān)打開,執(zhí)行舊的代碼邏輯,待一些操作執(zhí)行結(jié)束之后,再將開關(guān)關(guān)閉,執(zhí)行新的代碼邏輯。那么我們可以把開關(guān)寫到配置里面,通過配置中心修改配置的方式,在不停機(jī)的情況下,熱更新配置,從而實(shí)現(xiàn)開關(guān)的修改。

那么,Spring應(yīng)用是如何管理配置的呢?對于熱更新的一些場景,我們在實(shí)際開發(fā)中需要做哪些事情呢?本文將對這些問題進(jìn)行介紹。

二、Spring配置使用

本章節(jié)將簡單介紹Spring對于配置的使用。

2.1 讀配置

比如,我們在配置文件或者配置中心(如Apollo)中添加了一個(gè)配置,Spring應(yīng)用可以通過以下幾種方式取出配置。

x:
  y:
    z: 1

1. @Value

通過注解@Value+配置占位符,可以實(shí)現(xiàn)配置注入。對于需要默認(rèn)值的情況,可以在配置項(xiàng)(x.y.z)后添加然后跟上默認(rèn)值(1

@Component
public class MyComponent {
    // @Value("${x.y.z}") // 無默認(rèn)值的情況
    @Value("${x.y.z:1}")
    private int z;
}

2. @ConfigurationProperties

為了方便配置管理,也經(jīng)常會(huì)將配置放到單獨(dú)的Properties類中。通過@ConfigurationProperties 可以指定配置項(xiàng)前綴(x.y),這個(gè)前綴后面的所有配置會(huì)反序列化到該類上。

@Data
@ConfigurationProperties("x.y")
public class MyProperties {
    private int z = 1;
}

為了讓這個(gè)配置可以作為Spring bean被使用,一般可以直接在類上添加@Component注解

@Data
@Component
@ConfigurationProperties("x.y")
public class MyProperties {
    private int z = 1;
}

對于一些自動(dòng)配置情況,需要在滿足條件的情況下,才將Properties加載到Spring容器。那么這個(gè)時(shí)候,可以在自動(dòng)配置類上添加配置@EnableConfigurationProperties,在滿足條件的情況下會(huì)將Properties類引入。

@EnableConfigurationProperties({MyProperties.class})
//@ConditionOnXXX("")  //滿足條件的才自動(dòng)裝配
public class SnowflakeAutoConfiguration {
    // ...
}

另外,還有一個(gè)提升我們開發(fā)效率和體驗(yàn)的小技巧。我們在改配置文件的時(shí)候,發(fā)現(xiàn)Spring官方提供的配置,編輯的時(shí)候會(huì)有自動(dòng)提示,但是我們自己的配置沒有自動(dòng)提示。

我們可以pom.xml添加以下依賴。添加依賴之后,在前端編譯的時(shí)候(也就是編譯class文件的時(shí)候),會(huì)自動(dòng)將@ConfigurationProperties的配置類的信息提取成json格式的元數(shù)據(jù),保存在類路徑的META-INF/spring-configuration-metadata.json文件中。這樣IDE就可以通過元數(shù)據(jù)文件實(shí)現(xiàn)配置編輯的自動(dòng)提示。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

spring-configuration-metadata.json內(nèi)容如下,不需要手動(dòng)編寫。

{
  "groups": [
    {
      "name": "x.y",
      "type": "ltd.dujiabao.configtests.config.MyProperties",
      "sourceType": "ltd.dujiabao.configtests.config.MyProperties"
    }
  ],
  "properties": [
    {
      "name": "x.y.z",
      "type": "java.lang.Integer",
      "sourceType": "ltd.dujiabao.configtests.config.MyProperties",
      "defaultValue": 1
    }
  ],
  "hints": []
}

3. EnvironmentAware

通過實(shí)現(xiàn)EnvironmentAware接口,可以獲取Environment的實(shí)現(xiàn)類,從而取出需要的配置。

這種方式的獲取配置比較常見的使用場景是,在生成BeanDefinition階段,需要取出一些配置值,上面提到的兩種方式,bean都還沒生成,沒辦法通過上面提到的方式拿到配置。需要直接拿到專門用于管理應(yīng)用配置的接口Environment,直接取出所需的配置。對于Environment,后續(xù)會(huì)在第三章第一節(jié)詳細(xì)介紹。

getProperty方法指定配置鍵名稱,從而獲取配置。

public class MyImport implements EnvironmentAware {
    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
        String z = environment.getProperty("x.y.z");
    }
}

通過Binder指定配置前綴,將配置前綴后的所有配置都綁定到指定類中。

public class MyImport implements EnvironmentAware {
    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
        MyProperties myProperties = Binder.get(environment).bind("x.y", MyProperties.class).get();
    }
}

2.2 配置的多環(huán)境使用

1. profile

對于不同環(huán)境,可能會(huì)有不同的配置,比如說線程池大小、連接池大小??梢酝ㄟ^配置profile去控制當(dāng)前使用的是哪個(gè)環(huán)境,用哪個(gè)配置。

比如,當(dāng)前有dev、uat環(huán)境。

dev的配置文件為application-dev.yml

x:
  y:
    z: 2

uat的配置文件為application-uat.yml

x:
  y:
    z: 3

在application.yml中,可以選擇profile,從而選擇對應(yīng)的配置。也可以在啟動(dòng)服務(wù)時(shí),通過命令行的方式傳入。當(dāng)spring.profiles.active=uat,會(huì)使用application-uat.yml,當(dāng)spring.profiles.active=dev會(huì)使用application-dev.yml。

spring:
  profiles:
    active: uat
java -Dspring.profiles.active=dev -jar xxx.jar

三、Spring配置原理

第二章中,介紹了Spring配置日常的基本使用。在本章節(jié),將從配置組件、配置注入、配置熱更新三個(gè)方面詳細(xì)介紹Spring配置的原理及使用。

1. 配置組件

本章節(jié),將介紹Spring配置中重要的幾個(gè)組件,并通過介紹組件,將Spring對于配置管理邏輯進(jìn)行介紹。

1.1 Environment

1.1.1 Environment

在Spring中,配置最重要的組件就是Environment,它集成了Spring應(yīng)用的所有配置。

我們可以簡單看下Environment的源碼。Environment主要包括兩部分,一部分是Profile,另一部分是Property。Profile表示當(dāng)前進(jìn)程激活了哪個(gè)環(huán)境,用了哪個(gè)環(huán)境的配置;Property表示當(dāng)前進(jìn)程的配置項(xiàng)。

方法getActiveProfiles獲取當(dāng)前激活的Profile;getDefaultProfiles獲取默認(rèn)的Profile;acceptsProfiles判斷是否滿足所有Profile。

public interface Environment extends PropertyResolver {

    String[] getActiveProfiles();

    String[] getDefaultProfiles();

    @Deprecated
    boolean acceptsProfiles(String... profiles);

    boolean acceptsProfiles(Profiles profiles);
}

containsProperty判斷是否包含某個(gè)配置項(xiàng);getProperty獲取配置項(xiàng)的值;getRequiredProperty獲取配置項(xiàng)的值,當(dāng)配置項(xiàng)不存在拋出IllegalStateException;resolvePlaceholders、resolveRequiredPlaceholders主要用于處理${..}占位符

public interface PropertyResolver {

    boolean containsProperty(String key);

    @Nullable
    String getProperty(String key);

    String getProperty(String key, String defaultValue);

    @Nullable
    <T> T getProperty(String key, Class<T> targetType);

    <T> T getProperty(String key, Class<T> targetType, T defaultValue);

    String getRequiredProperty(String key) throws IllegalStateException;

    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

    String resolvePlaceholders(String text);

    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

1.1.2 ConfigurableEnvironment

ConfigurableEnvironment,顧名思義提供了可配置的Environment接口,它繼承了Environment。

可通過方法setActiveProfiles、addActiveProfile、setDefaultProfiles 修改激活、默認(rèn)的Profile;通過getPropertySources獲取PropertySource列表,并且對PropertySource列表進(jìn)行修改;通過getSystemProperties、getSystemEnvironment可以獲取一些和系統(tǒng)參數(shù)相關(guān)的map。

public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {

    void setActiveProfiles(String... profiles);

    void addActiveProfile(String profile);

    void setDefaultProfiles(String... profiles);

    MutablePropertySources getPropertySources();

    Map<String, Object> getSystemProperties();

    Map<String, Object> getSystemEnvironment();
}

1.1.3 AbstractEnvironment

接口Environment的默認(rèn)實(shí)現(xiàn)類是AbstractEnvironment,我們簡單分析它的實(shí)現(xiàn)原理。

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    //...
}
1.1.2.1 成員變量

AbstractEnvironment包含兩個(gè)重要的成員:

  1. propertySources:維護(hù)所有配置來源PropertySource的一個(gè)集合類
  2. propertyResolver:用于提供一些讀配置的方法,比如說獲取配置值、通過占位符獲取配置值等,propertySources會(huì)傳入作為配置來源

我們這里引出了一個(gè)很重要的組件PropertySource,可以簡單理解為每一個(gè)配置來源都有一個(gè)PropertySource,將在1.2介紹。

    private final MutablePropertySources propertySources = new MutablePropertySources();

    private final ConfigurablePropertyResolver propertyResolver =
            new PropertySourcesPropertyResolver(this.propertySources);
1.1.2.2 構(gòu)造方法

構(gòu)造方法將成員變量propertySources傳入方法customizePropertySources,為子類提供一個(gè)可以自定義PropertySource并加入到的propertySources方法。

    public AbstractEnvironment() {
        customizePropertySources(this.propertySources);
    }

    protected void customizePropertySources(MutablePropertySources propertySources) {
    }

Spring應(yīng)用默認(rèn)的Environment實(shí)現(xiàn)類StandardEnvironment,它會(huì)繼承AbstractEnvironment,重寫方法customizePropertySources。我們可以看到,它添加了兩個(gè)PropertySource,systemProperties是系統(tǒng)屬性的來源,systemEnvironment是系統(tǒng)環(huán)境變量的來源。

比方說,在啟動(dòng)服務(wù)時(shí)傳入設(shè)置系統(tǒng)屬性property_name,那么這個(gè)系統(tǒng)屬性會(huì)因?yàn)?code>systemProperties被Environment管理,可以直接通過第二章介紹的方式獲取該值。

java -Dproperty_name=value -jar your_application.jar

比方說,在Linux環(huán)境下,設(shè)置了環(huán)境變量VARIABLE_NAME,那么它也會(huì)因?yàn)?code>systemEnvironment被Environment管理,可以直接通過第二章介紹的方式獲取該值。

export VARIABLE_NAME="value"
public class StandardEnvironment extends AbstractEnvironment {

    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(
                new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
        propertySources.addLast(
                new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }
}

    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public Map<String, Object> getSystemProperties() {
        try {
            return (Map) System.getProperties();
        }
        catch (AccessControlException ex) {
            return (Map) new ReadOnlySystemAttributesMap() {
                @Override
                @Nullable
                protected String getSystemAttribute(String attributeName) {
                    try {
                        return System.getProperty(attributeName);
                    }
                    catch (AccessControlException ex) {
                        if (logger.isInfoEnabled()) {
                            logger.info("Caught AccessControlException when accessing system property '" +
                                    attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage());
                        }
                        return null;
                    }
                }
            };
        }
    }

    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public Map<String, Object> getSystemEnvironment() {
        if (suppressGetenvAccess()) {
            return Collections.emptyMap();
        }
        try {
            return (Map) System.getenv();
        }
        catch (AccessControlException ex) {
            return (Map) new ReadOnlySystemAttributesMap() {
                @Override
                @Nullable
                protected String getSystemAttribute(String attributeName) {
                    try {
                        return System.getenv(attributeName);
                    }
                    catch (AccessControlException ex) {
                        if (logger.isInfoEnabled()) {
                            logger.info("Caught AccessControlException when accessing system environment variable '" +
                                    attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage());
                        }
                        return null;
                    }
                }
            };
        }
    }
1.1.2.3 getProperty

通過getProperty取出配置項(xiàng)的值。我們可以看到這個(gè)方法實(shí)際上用的就是propertyResolver。

    private final MutablePropertySources propertySources = new MutablePropertySources();

    private final ConfigurablePropertyResolver propertyResolver =
            new PropertySourcesPropertyResolver(this.propertySources);

    @Override
    @Nullable
    public String getProperty(String key) {
        return this.propertyResolver.getProperty(key);
    }

我們通過源碼可以找到propertyResolver獲取配置的位置,簡單來說就是遍歷所有PropertySource,第一個(gè)找到值的就直接返回。因此PropertySource的順序還有一個(gè)優(yōu)先級問題,排前面的優(yōu)先使用。

    @Nullable
    protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
        if (this.propertySources != null) {
      // 遍歷所有PropertySource
            for (PropertySource<?> propertySource : this.propertySources) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Searching for key '" + key + "' in PropertySource '" +
                            propertySource.getName() + "'");
                }
                Object value = propertySource.getProperty(key);
                if (value != null) {
                    if (resolveNestedPlaceholders && value instanceof String) {
                        value = resolveNestedPlaceholders((String) value);
                    }
                    logKeyFound(key, propertySource, value);
                    return convertValueIfNecessary(value, targetValueType);
                }
            }
        }
        if (logger.isTraceEnabled()) {
            logger.trace("Could not find key '" + key + "' in any property source");
        }
        return null;
    }
1.1.2.4 getActiveProfiles

顧名思義,方法就是用來獲取當(dāng)前被激活的Profile

從方法中可以看到,獲取激活的Profile的基本邏輯就是,在沒有初始化的情況下,從配置項(xiàng)spring.profiles.active中獲取,隨后保存到成員變量activeProfiles中;之后可以直接從activeProfiles獲取。

    public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";
    private final Set<String> activeProfiles = new LinkedHashSet<>();

    public String[] getActiveProfiles() {
        return StringUtils.toStringArray(doGetActiveProfiles());
    }

    protected Set<String> doGetActiveProfiles() {
        synchronized (this.activeProfiles) {
            if (this.activeProfiles.isEmpty()) {
                String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
                if (StringUtils.hasText(profiles)) {
                    setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
                            StringUtils.trimAllWhitespace(profiles)));
                }
            }
            return this.activeProfiles;
        }
    }

1.2 PropertySource

簡單來說就是對配置來源的抽象,也就是說每一種配置來源都有一個(gè)PropertySource。比如說,配置文件的配置來源是OriginTrackedMapPropertySource,Apollo的配置來源是ConfigPropertySource的對象。而如果我們想自定義配置來源,也可以通過繼承PropertySource來實(shí)現(xiàn)。

1.2.1 PropertySource

首先介紹一下抽象類PropertySource。成員主要由幾部分組成,name配置來源的名稱,source來源的實(shí)體。最重要的方法getProperty是抽象方法,由子類實(shí)現(xiàn)查詢配置的邏輯。

public abstract class PropertySource<T> {

    protected final Log logger = LogFactory.getLog(getClass());

    protected final String name;

    protected final T source;

    public PropertySource(String name, T source) {
        Assert.hasText(name, "Property source name must contain at least one character");
        Assert.notNull(source, "Property source must not be null");
        this.name = name;
        this.source = source;
    }

    @SuppressWarnings("unchecked")
    public PropertySource(String name) {
        this(name, (T) new Object());
    }

    public String getName() {
        return this.name;
    }

    public T getSource() {
        return this.source;
    }

    public boolean containsProperty(String name) {
        return (getProperty(name) != null);
    }

    @Nullable
    public abstract Object getProperty(String name);

    public static PropertySource<?> named(String name) {
        return new ComparisonPropertySource(name);
    }
}

1.3 ConfigFileApplicationListener

接下來,我們將介紹ConfigFileApplicationListener,通過它可以了解到配置文件是如何變成PropertySource的,并且可以了解到如何自定義PropertySource,自定義的PropertySource如何被發(fā)現(xiàn)并使用。

我們可以看到,ConfigFileApplicationListener實(shí)現(xiàn)了三個(gè)接口EnvironmentPostProcessorSmartApplicationListener、Ordered。

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
    //..
}

1.3.1 Ordered

簡單來說Ordered是用來表示多個(gè)同類組件之間順序,后續(xù)在處理所有EnvironmentPostProcessor時(shí)會(huì)用到這個(gè)順序。

    public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
    private int order = DEFAULT_ORDER;
    
    @Override
    public int getOrder() {
        return this.order;
    }

1.3.2 SmartApplicationListener

SmartApplicationListener簡單來說就是可以同時(shí)監(jiān)聽多種應(yīng)用事件ApplicationEvent,ConfigFileApplicationListener會(huì)監(jiān)聽ApplicationEnvironmentPreparedEventApplicationPreparedEvent這兩個(gè)事件,針對這兩個(gè)事件,分別會(huì)執(zhí)行onApplicationEnvironmentPreparedEvent、onApplicationPreparedEvent這兩個(gè)方法。

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
                || ApplicationPreparedEvent.class.isAssignableFrom(eventType);
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent(event);
        }
    }

在Spring應(yīng)用啟動(dòng)的前期,會(huì)創(chuàng)建并準(zhǔn)備一個(gè)應(yīng)用的Environment,完成準(zhǔn)備之后會(huì)發(fā)布一個(gè)ApplicationEnvironmentPreparedEvent事件。這個(gè)事件會(huì)觸發(fā)執(zhí)行

ConfigFileApplicationListener的方法onApplicationEnvironmentPreparedEvent,對一系列PropertySource進(jìn)行加載并注冊到Environment中。

我們可以看到,這個(gè)方法做的事情主要是將所有EnvironmentPostProcessor加載進(jìn)來,隨后按照設(shè)定的順序逐一執(zhí)行。

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        // 加載所有EnvironmentPostProcessor
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        // 把當(dāng)前對象也加入到處理器列表中
        postProcessors.add(this);
        // 根據(jù)Ordered設(shè)置的順序進(jìn)行排序
        AnnotationAwareOrderComparator.sort(postProcessors);
        // EnvironmentPostProcessor逐一執(zhí)行
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }
    }

    // 通過Spring Factory的機(jī)制加載所有EnvironmentPostProcessor
    List<EnvironmentPostProcessor> loadPostProcessors() {
        return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
    }

我們通過方法loadPostProcessors可以看出,Spring Boot為開發(fā)者提供了擴(kuò)展接口。開發(fā)者可以自定義EnvironmentPostProcessor,然后在META-INF/spring.factories中將該自定義類進(jìn)行注冊。SpringFactoriesLoader會(huì)通過掃描每個(gè)jar包類路徑的文件META-INF/spring.factoriesEnvironmentPostProcessor的實(shí)現(xiàn)類找出,然后將它們進(jìn)行實(shí)例化。

因此,如果我們想自定義配置來源PropertySource,可以先實(shí)現(xiàn)EnvironmentPostProcessor,EnvironmentPostProcessor中將PropertySource加入到Environment中,然后將這個(gè)類寫到文件META-INF/spring.factories中

org.springframework.boot.env.EnvironmentPostProcessor=ltd.dujiabao.configtests.config.CustomEnvironmentPostProcessor

1.3.3 EnvironmentPostProcessor

在Spring應(yīng)用生成Environment之后,會(huì)通過調(diào)用EnvironmentPostProcessor,對Environment進(jìn)行進(jìn)一步增強(qiáng)。也就是說,如果我們想添加自定義的PropertySource,可以通過實(shí)現(xiàn)這個(gè)接口,然后通過spring.factories進(jìn)行注冊。比如,Apollocom.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer。

ConfigFileApplicationListener自身就是EnvironmentPostProcessor的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)方法會(huì)將向Environment添加若干個(gè)PropertySource,包括基于配置文件的PropertySource。下面我們將詳細(xì)介紹這個(gè)過程。

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
    }

    protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        // 添加RandomValuePropertySource
        RandomValuePropertySource.addToEnvironment(environment);
        // 加載
        new Loader(environment, resourceLoader).load();
    }

首先,將RandomValuePropertySource添加到Environment,簡單來說就是我們?nèi)∨渲玫臅r(shí)候可以通過配置項(xiàng)random.int、random.long、random.uuid取出隨機(jī)值,比較簡單,不再贅述。

之后,通過內(nèi)部類Loader進(jìn)行加載。也就是說,加載PropertySource的核心邏輯在Loader

1.3.4 ConfigFileApplicationListener.Loader

1.3.4.1 成員變量&構(gòu)造方法

我們先來看下Loader的成員變量:

  1. environment:當(dāng)前Spring應(yīng)用的Environment
  2. placeholdersResolver:用于解析占位符,從Environment中取出值
  3. resourceLoader:用于從文件系統(tǒng)中讀取配置文件
  4. propertySourceLoaders:包含所有用于將配置文件加載為PropertySourcePropertySourceLoader
  5. profiles:保存當(dāng)前待處理的激活的Profile,這是一個(gè)隊(duì)列。一開始的時(shí)候,會(huì)有一個(gè)默認(rèn)的Profile,并且在讀入配置文件的時(shí)候,可以增加Profile。循環(huán)從隊(duì)列中取出Profile,直到隊(duì)列為空。
  6. processedProfiles:保存所有被處理過的Profile
  7. activatedProfiles:是否已取出被激活的Profile列表。意思是只會(huì)讀取spring.profiles.active一次,先被讀取的優(yōu)先級高,會(huì)被采納;其他不會(huì)被采納。
  8. loaded:map保存每個(gè)Profile的PropertySource
  9. loadDocumentsCache:緩存讀入的文件,避免需要每次都從文件系統(tǒng)中讀入

從構(gòu)造方法中,我們可以看出PropertySourceLoader也提供了可擴(kuò)展的spi。構(gòu)造方法中,通過SpringFactoriesLoader查出所有PropertySourceLoader。我們可以通過實(shí)現(xiàn)PropertySourceLoader,自定義解析配置文件的方法。

    private class Loader {
        private final Log logger = ConfigFileApplicationListener.this.logger;
        // 當(dāng)前Spring應(yīng)用的`Environment`
        private final ConfigurableEnvironment environment;
        // 用于解析占位符,從`Environment`中取出值
        private final PropertySourcesPlaceholdersResolver placeholdersResolver;
        // 用于從文件系統(tǒng)中讀取配置文件
        private final ResourceLoader resourceLoader;
        // 包含所有用于將配置文件加載為PropertySource的PropertySourceLoader
        private final List<PropertySourceLoader> propertySourceLoaders;
        // 保存當(dāng)前待處理的激活的`Profile`,這是一個(gè)隊(duì)列。一開始的時(shí)候,會(huì)有一個(gè)默認(rèn)的`Profile`,并且在讀入配置文件的時(shí)候,可以增加Profile。循環(huán)從隊(duì)列中取出`Profile`,直到隊(duì)列為空。
        private Deque<Profile> profiles;
        // 保存所有被處理過的`Profile`
        private List<Profile> processedProfiles;
        // 是否已取出被激活的`Profile`列表。意思是只會(huì)讀取`spring.profiles.active`一次,先被讀取的優(yōu)先級高,會(huì)被采納;其他不會(huì)被采納。
        private boolean activatedProfiles;
        // map保存每個(gè)Profile的`PropertySource`
        private Map<Profile, MutablePropertySources> loaded;
        // 緩存讀入的文件,避免需要每次都從文件系統(tǒng)中讀入
        private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();
    
        Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
            this.environment = environment;
      // 傳入environment,構(gòu)造PropertySourcesPlaceholdersResolver
            this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
      // 創(chuàng)建資源加載器
            this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
      // 從Spring Loader中取出配置加載器列表
            this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                    getClass().getClassLoader());
        }
  }
1.3.4.2 Loader#load

接下來介紹加載配置的方法。FilteredPropertySource.apply 里面實(shí)際上沒做什么,我們就直接忽略。我們直接看最后的lambda表達(dá)式即可。

基本邏輯就是:

  1. 從現(xiàn)有的PropertySource初始化profiles隊(duì)列。也就是從環(huán)境變量、系統(tǒng)變量中取出。

  2. profiles隊(duì)頭取出Profile,然后從文件系統(tǒng)讀入該Profile的配置文件。并且若配置文件中有指定spring.profiles.active,并且之前未激活過,則將這些Profile加入到隊(duì)列中。循環(huán)讀,直到隊(duì)列為空。

因此,下面主要介紹兩個(gè)方法:initializeProfilesload


        void load() {
            FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
                    (defaultProperties) -> {
                        this.profiles = new LinkedList<>();
                        this.processedProfiles = new LinkedList<>();
                        this.activatedProfiles = false;
                        this.loaded = new LinkedHashMap<>();
            // 初始化Profile列表
                        initializeProfiles();
            // 取出當(dāng)前Profile,掃描配置文件
                        while (!this.profiles.isEmpty()) {
                            Profile profile = this.profiles.poll();
                            if (isDefaultProfile(profile)) {
                // 將非默認(rèn)Profile加入到Environment
                                addProfileToEnvironment(profile.getName());
                            }
              // 加載配置文件
                            load(profile, this::getPositiveProfileFilter,
                                    addToLoaded(MutablePropertySources::addLast, false));
                            this.processedProfiles.add(profile);
                        }
                        load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
                        addLoadedPropertySources();
                        applyActiveProfiles(defaultProperties);
                    });
        }
1.3.4.3 Loader#initializeProfiles

初始化成員變量profiles,基本邏輯是:

  1. 默認(rèn)添加一個(gè)null,后續(xù)會(huì)讀入文件application.yml或者其他application.文件
  2. 從現(xiàn)有的PropertySource中讀入激活的Profile,并將其加入到隊(duì)列后
  3. 若未指定激活的Profile,則添加一個(gè)叫defaultProfile
private void initializeProfiles() {
        // 默認(rèn)添加一個(gè)null,后續(xù)會(huì)讀入文件application.yml或者其他application.文件
            this.profiles.add(null);
            Binder binder = Binder.get(this.environment);
        // 從現(xiàn)有的PropertySource中讀入spring.profiles.active
            Set<Profile> activatedViaProperty = getProfiles(binder, ACTIVE_PROFILES_PROPERTY);
        // 從現(xiàn)有的PropertySource中讀入spring.profiles.include
            Set<Profile> includedViaProperty = getProfiles(binder, INCLUDE_PROFILES_PROPERTY);
        // 從environment中讀入其他active的Profile,可能是硬編碼指定的
            List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
            this.profiles.addAll(otherActiveProfiles);
            this.profiles.addAll(includedViaProperty);
        // 添加激活的Profile
            addActiveProfiles(activatedViaProperty);
        // 若沒有指定,那添加一個(gè)default的Profile,后續(xù)會(huì)讀入文件application-default.yml或者其他application-default.文件
            if (this.profiles.size() == 1) {
                for (String defaultProfileName : this.environment.getDefaultProfiles()) {
                    Profile defaultProfile = new Profile(defaultProfileName, true);
                    this.profiles.add(defaultProfile);
                }
            }
        }

void addActiveProfiles(Set<Profile> profiles) {
            if (profiles.isEmpty()) {
                return;
            }
        // 只允許添加一次激活的Profile
            if (this.activatedProfiles) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Profiles already activated, '" + profiles + "' will not be applied");
                }
                return;
            }
        // 添加激活的Profile
            this.profiles.addAll(profiles);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Activated activeProfiles " + StringUtils.collectionToCommaDelimitedString(profiles));
            }
        // 設(shè)置標(biāo)識(shí)位
            this.activatedProfiles = true;
        // 刪除默認(rèn)的profile default
            removeUnprocessedDefaultProfiles();
        }
1.3.4.4 Loader#load(Profile, DocumentFilterFactory, DocumentConsumer)

基本邏輯為:

  1. 獲取配置文件的的路徑位置,通過配置項(xiàng)spring.config.location。若沒有則默認(rèn)用這些目錄,classpath:/、classpath:/config/file:./、file:./config/*/file:./config/
  2. 遍歷每個(gè)路徑,在每個(gè)路徑下搜索配置文件。配置文件的文件名從配置項(xiàng)spring.config.name獲取。若沒有則默認(rèn)用,application
        private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
      // 遍歷所有配置文件的路徑,加載配置文件
            getSearchLocations().forEach((location) -> {
                boolean isDirectory = location.endsWith("/");
        // 獲取配置文件名前綴
                Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
        // 加載
                names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
            });
        }

        private Set<String> getSearchLocations() {
      // 獲取額外的配置文件路徑,spring.config.additional-location
            Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
      // 獲取配置文件文件路徑,spring.config.location,如果沒有指定,則用默認(rèn)值
            if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
                locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY));
            }
            else {
                locations.addAll(
            // 默認(rèn)從這些路徑搜索文件classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/
                        asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
            }
            return locations;
        }

        private Set<String> getSearchNames() {
      // 獲取配置文件前綴名,spring.config.name
            if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
                String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
                Set<String> names = asResolvedSet(property, null);
                names.forEach(this::assertValidConfigName);
                return names;
            }
      // 若沒有設(shè)置,默認(rèn)為application
            return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
        }
1.3.4.5 Loader#load(String, Profile, DocumentFilterFactory, DocumentConsumer)

基本邏輯就是:

  1. 若傳進(jìn)來的location是文件,遍歷所有PropertySourceLoader,對文件進(jìn)行加載
  2. 若傳進(jìn)來的location是文件夾,遍歷所有PropertySourceLoader,對所有可能的文件進(jìn)行嘗試加載
        private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
                DocumentConsumer consumer) {
      // 當(dāng)傳進(jìn)來的location是文件,不是文件夾,name為空,直接進(jìn)入下面的加載邏輯
            if (!StringUtils.hasText(name)) {
        // 遍歷所有PropertySourceLoader,只有支持文件后綴的能加載
                for (PropertySourceLoader loader : this.propertySourceLoaders) {
                    if (canLoadFileExtension(loader, location)) {
                        load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
                        return;
                    }
                }
                throw new IllegalStateException("File extension of config file location '" + location
                        + "' is not known to any PropertySourceLoader. If the location is meant to reference "
                        + "a directory, it must end in '/'");
            }
      // 當(dāng)傳進(jìn)來的location是文件夾
            Set<String> processed = new HashSet<>();
      // 遍歷所有PropertySourceLoader,獲取該加載器支持的文件后綴,然后拼接成路徑,對文件進(jìn)行加載
            for (PropertySourceLoader loader : this.propertySourceLoaders) {
                for (String fileExtension : loader.getFileExtensions()) {
                    if (processed.add(fileExtension)) {
                        loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
                                consumer);
                    }
                }
            }
        }
1.3.4.6 Loader#loadForFileExtension

這個(gè)方法的邏輯比較復(fù)雜,一般來說有用的只有注釋的那兩處。

  1. Profile不為空時(shí),拼接文件名 prefix + "-" + profile + fileExtension,隨后在文件系統(tǒng)查找并加載文件。
  2. Profile為空時(shí),拼接文件名 prefix + fileExtension,隨后在文件系統(tǒng)查找并加載文件。
        private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
                Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
            DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
            if (profile != null) {
                String profileSpecificFile = prefix + "-" + profile + fileExtension;
        // 在Profile不為null時(shí),一般會(huì)通過這個(gè)方法加載配置文件
                load(loader, profileSpecificFile, profile, defaultFilter, consumer);
                load(loader, profileSpecificFile, profile, profileFilter, consumer);
                for (Profile processedProfile : this.processedProfiles) {
                    if (processedProfile != null) {
                        String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
                        load(loader, previouslyLoaded, profile, profileFilter, consumer);
                    }
                }
            }
            // 在在Profile為null時(shí),一般會(huì)通過這個(gè)方法加載配置文件
            load(loader, prefix + fileExtension, profile, profileFilter, consumer);
        }
1.3.4.7 Loader#load(PropertySourceLoader, String, Profile, DocumentFilter, DocumentConsumer)

基本邏輯就是將文件讀進(jìn)Document,隨后將DocumentPropertySource 插入到loaded中,這樣就完成了從配置文件到PropertySource的轉(zhuǎn)換

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
                DocumentConsumer consumer) {
        // 通過路徑查找資源
            Resource[] resources = getResources(location);
            for (Resource resource : resources) {
                try {
          // 文件不存在,直接返回
                    if (resource == null || !resource.exists()) {
                        if (this.logger.isTraceEnabled()) {
                            StringBuilder description = getDescription("Skipped missing config ", location, resource,
                                    profile);
                            this.logger.trace(description);
                        }
                        continue;
                    }
          // 文件后綴為空,直接返回
                    if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
                        if (this.logger.isTraceEnabled()) {
                            StringBuilder description = getDescription("Skipped empty config extension ", location,
                                    resource, profile);
                            this.logger.trace(description);
                        }
                        continue;
                    }
          // 包含一些隱藏的元素,不重要。。
                    if (resource.isFile() && isPatternLocation(location) && hasHiddenPathElement(resource)) {
                        if (this.logger.isTraceEnabled()) {
                            StringBuilder description = getDescription("Skipped location with hidden path element ",
                                    location, resource, profile);
                            this.logger.trace(description);
                        }
                        continue;
                    }
          // 將文件加載為Document列表
                    String name = "applicationConfig: [" + getLocationName(location, resource) + "]";
                    List<Document> documents = loadDocuments(loader, name, resource);
                    if (CollectionUtils.isEmpty(documents)) {
                        if (this.logger.isTraceEnabled()) {
                            StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
                                    profile);
                            this.logger.trace(description);
                        }
                        continue;
                    }
                    List<Document> loaded = new ArrayList<>();
          // 一般我們不會(huì)配filter,認(rèn)為考慮滿足的情況就好了
                    for (Document document : documents) {
                        if (filter.match(document)) {
                            addActiveProfiles(document.getActiveProfiles());
                            addIncludedProfiles(document.getIncludeProfiles());
                            loaded.add(document);
                        }
                    }
                    Collections.reverse(loaded);
          // 將文檔轉(zhuǎn)換為
                    if (!loaded.isEmpty()) {
                        loaded.forEach((document) -> consumer.accept(profile, document));
                        if (this.logger.isDebugEnabled()) {
                            StringBuilder description = getDescription("Loaded config file ", location, resource,
                                    profile);
                            this.logger.debug(description);
                        }
                    }
                }
                catch (Exception ex) {
                    StringBuilder description = getDescription("Failed to load property source from ", location,
                            resource, profile);
                    throw new IllegalStateException(description.toString(), ex);
                }
            }
        }

        private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
                boolean checkForExisting) {
            return (profile, document) -> {
                if (checkForExisting) {
                    for (MutablePropertySources merged : this.loaded.values()) {
                        if (merged.contains(document.getPropertySource().getName())) {
                            return;
                        }
                    }
                }
        // 將文檔的PropertySource加入到loaded里面
                MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
                        (k) -> new MutablePropertySources());
                addMethod.accept(merged, document.getPropertySource());
            };
        }
1.3.4.8 Loader#addLoadedPropertySources

1.3.4.2 在完成加載之后,會(huì)將加載成功的所有PropertySource加入到Environment

        private void addLoadedPropertySources() {
            MutablePropertySources destination = this.environment.getPropertySources();
            List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
            Collections.reverse(loaded);
            String lastAdded = null;
            Set<String> added = new HashSet<>();
      // 遍歷所有被load的PropertySource
            for (MutablePropertySources sources : loaded) {
                for (PropertySource<?> source : sources) {
                    if (added.add(source.getName())) {
            // 將其加入到environment中
                        addLoadedPropertySource(destination, lastAdded, source);
                        lastAdded = source.getName();
                    }
                }
            }
        }

        private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded,
                PropertySource<?> source) {
            if (lastAdded == null) {
                if (destination.contains(DEFAULT_PROPERTIES)) {
                    destination.addBefore(DEFAULT_PROPERTIES, source);
                }
                else {
                    destination.addLast(source);
                }
            }
            else {
                destination.addAfter(lastAdded, source);
            }
        }

至此,終于介紹完Spring Boot加載配置文件至Environment的邏輯。

2. 配置注入

本小節(jié)主要介紹@Value、@ConfigurationProperties是如何從Environment中拿到配置的。

2.1 @Value 原理

簡單來說,就是在構(gòu)建bean的時(shí)候,在處理自動(dòng)注入時(shí),解析@Value的占位符之后,從所有PropertySource中找到配置值。

詳見https://juejin.cn/post/7043315611744600094

[圖片上傳失敗...(image-ff845d-1718545618069)]

[圖片上傳失敗...(image-6a309f-1718545618069)]

[圖片上傳失敗...(image-836d56-1718545618069)]

[圖片上傳失敗...(image-d01ed6-1718545618069)]

[圖片上傳失敗...(image-3d2d32-1718545618069)]

2.2 @ConfigurationProperties

簡單來說,在創(chuàng)建標(biāo)注了@ConfigurationProperties的bean之后,會(huì)遍歷所有BeanPostProcessor執(zhí)行postProcessBeforeInitialization方法。BeanPostProcessor有一個(gè)實(shí)現(xiàn)類ConfigurationPropertiesBindingPostProcessor專門負(fù)責(zé)將配置值綁定到bean上。

綁定的邏輯也就是從PropertySource中取出配置值,隨后設(shè)置到bean的字段上。詳見org.springframework.boot.context.properties.bind.Binder

[圖片上傳失敗...(image-a8e9ca-1718545618069)]

[圖片上傳失敗...(image-4136ef-1718545618069)]

[圖片上傳失敗...(image-89f1fe-1718545618069)]

[圖片上傳失敗...(image-aaaff2-1718545618069)]

[圖片上傳失敗...(image-92092b-1718545618069)]

[圖片上傳失敗...(image-d57184-1718545618069)]

[圖片上傳失敗...(image-41490c-1718545618069)]

[圖片上傳失敗...(image-9bf82c-1718545618069)]

[圖片上傳失敗...(image-a29d2e-1718545618069)]

[圖片上傳失敗...(image-8792ab-1718545618069)]

org.springframework.boot.context.properties.bind.Binder#findProperty我們可以看出實(shí)際上就是從ConfigurationPropertySource中取出配置值。

[圖片上傳失敗...(image-c7b9e-1718545618069)]

四、配置熱更新的實(shí)踐

考慮到Apollo是比較常見的配置中心,我們將以Apollo為例介紹如何實(shí)現(xiàn)熱更新的Spring應(yīng)用的配置的。

1. @Value

apollo-client 默認(rèn)支持熱更新 @Value的字段值,無需額外配置或開發(fā)。

原理可見 com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener

Apollo 上更新配置之后,AutoUpdateConfigChangeListener會(huì)收到消息,隨后從消息中拿出被修改的key,重新查詢最新的值,通過反射對字段值進(jìn)行重新設(shè)置。

public class AutoUpdateConfigChangeListener implements ConfigChangeListener{
  @Override
  public void onChange(ConfigChangeEvent changeEvent) {
    // 獲取所有修改的key
    Set<String> keys = changeEvent.changedKeys();
    if (CollectionUtils.isEmpty(keys)) {
      return;
    }
    for (String key : keys) {
      // 查出key對應(yīng)的SpringValue,SpringValue存儲(chǔ)
      Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
      if (targetValues == null || targetValues.isEmpty()) {
        continue;
      }

      // 通過反射更新值
      for (SpringValue val : targetValues) {
        updateSpringValue(val);
      }
    }
  }
  
  private void updateSpringValue(SpringValue springValue) {
    try {
      // 查出最新的值,若有需要對值進(jìn)行轉(zhuǎn)換
      Object value = resolvePropertyValue(springValue);
      // 通過反射更新
      springValue.update(value);

      logger.info("Auto update apollo changed value successfully, new value: {}, {}", value,
          springValue);
    } catch (Throwable ex) {
      logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex);
    }
  }
  
    private Object resolvePropertyValue(SpringValue springValue) {
    // value will never be null, as @Value and @ApolloJsonValue will not allow that
    Object value = placeholderHelper
        .resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder());

    if (springValue.isJson()) {
      value = parseJsonValue((String)value, springValue.getGenericType());
    } else {
      if (springValue.isField()) {
        // org.springframework.beans.TypeConverter#convertIfNecessary(java.lang.Object, java.lang.Class, java.lang.reflect.Field) is available from Spring 3.2.0+
        if (typeConverterHasConvertIfNecessaryWithFieldParameter) {
          value = this.typeConverter
              .convertIfNecessary(value, springValue.getTargetType(), springValue.getField());
        } else {
          value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType());
        }
      } else {
        value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType(),
            springValue.getMethodParameter());
      }
    }

    return value;
  }
}
public class SpringValue {
 public void update(Object newVal) throws IllegalAccessException, InvocationTargetException {
    if (isField()) {
      injectField(newVal);
    } else {
      injectMethod(newVal);
    }
  }

  private void injectField(Object newVal) throws IllegalAccessException {
    Object bean = beanRef.get();
    if (bean == null) {
      return;
    }
    boolean accessible = field.isAccessible();
    field.setAccessible(true);
    field.set(bean, newVal);
    field.setAccessible(accessible);
  }

  private void injectMethod(Object newVal)
      throws InvocationTargetException, IllegalAccessException {
    Object bean = beanRef.get();
    if (bean == null) {
      return;
    }
    methodParameter.getMethod().invoke(bean, newVal);
  }
}

2. @ConfigurationProperties

@ConfigurationProperties默認(rèn)是不能自動(dòng)更新的,但是我們從上一小節(jié)可以看出,當(dāng)Apollo配置更新的時(shí)候,會(huì)通知監(jiān)聽器ConfigChangeListener。我們可以通過自定義一個(gè)ConfigChangeListener,在出現(xiàn)配置更新的時(shí)候,觸發(fā)@ConfigurationProperties bean的自動(dòng)更新。

首先引入依賴,用于發(fā)布EnvironmentChangeEvent,以及發(fā)布EnvironmentChangeEvent之后自動(dòng)更新@ConfigurationProperties的bean。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-context</artifactId>
</dependency>

之后,實(shí)現(xiàn)一個(gè)ConfigChangeListener,監(jiān)聽配置變更,發(fā)布事件EnvironmentChangeEvent,至此就可以實(shí)現(xiàn)ConfigurationProperties bean的熱更新。

@Configuration
public class ConfigurationPropertiesLiveRefresher implements ConfigChangeListener, ApplicationRunner {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Value("#{'${apollo.bootstrap.namespaces:}'.split(',')}")
    private List<String> namespaces;

    private static final Logger log = LoggerFactory.getLogger(ConfigurationPropertiesLiveRefresher.class);

    @Override
    public void run(ApplicationArguments args) {
        // 啟動(dòng)時(shí),注冊監(jiān)聽器,將當(dāng)前類注冊進(jìn)去
        for (String namespace : namespaces) {
            ConfigService.getConfig(namespace).addChangeListener(this);
            log.info("Successfully added config change listener to namespace {}", namespace);
        }
    }

    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
        // 當(dāng)存在配置更新時(shí),發(fā)布一個(gè)EnvironmentChangeEvent事件
        publisher.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        log.info("Successfully changed config change event {}", changeEvent.changedKeys());
    }
}

我們可以通過源碼分析一下EnvironmentChangeEvent觸發(fā)更新的原理。

當(dāng)發(fā)布事件EnvironmentChangeEvent之后,監(jiān)聽器ConfigurationPropertiesRebinder監(jiān)聽到事件之后,會(huì)觸發(fā)bean到重新綁定。這樣就實(shí)現(xiàn)了ConfigurationProperties bean的重新綁定。重新綁定里面會(huì)調(diào)用到方法initializeBean,這個(gè)方法又會(huì)走到剛剛2.2小節(jié)提到的配置綁定邏輯。

@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
        implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
  // 所有ConfigurationPropertie的bean的容器
  private ConfigurationPropertiesBeans beans;
 
    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        if (this.applicationContext.equals(event.getSource())
                || event.getKeys().equals(event.getSource())) {
      // 重新綁定
            rebind();
        }
    } 

  
    @ManagedOperation
    public void rebind() {
        this.errors.clear();
    // 遍歷所有ConfigurationPropertie的bean,進(jìn)行重新綁定
        for (String name : this.beans.getBeanNames()) {
            rebind(name);
        }
    }

    @ManagedOperation
    public boolean rebind(String name) {
        if (!this.beans.getBeanNames().contains(name)) {
            return false;
        }
        if (this.applicationContext != null) {
            try {
                Object bean = this.applicationContext.getBean(name);
                if (AopUtils.isAopProxy(bean)) {
                    bean = ProxyUtils.getTargetObject(bean);
                }
                if (bean != null) {
          // 對bean執(zhí)行銷毀方法
                    this.applicationContext.getAutowireCapableBeanFactory()
                            .destroyBean(bean);
          // 對bean重新初始化
                    this.applicationContext.getAutowireCapableBeanFactory()
                            .initializeBean(bean, name);
                    return true;
                }
            }
            catch (RuntimeException e) {
                this.errors.put(name, e);
                throw e;
            }
            catch (Exception e) {
                this.errors.put(name, e);
                throw new IllegalStateException("Cannot rebind to " + name, e);
            }
        }
        return false;
    }
}
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
        implements AutowireCapableBeanFactory {

    @Override
    public Object initializeBean(Object existingBean, String beanName) {
        return initializeBean(beanName, existingBean, null);
    }

  protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
        if (System.getSecurityManager() != null) {
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                invokeAwareMethods(beanName, bean);
                return null;
            }, getAccessControlContext());
        }
        else {
            invokeAwareMethods(beanName, bean);
        }

        Object wrappedBean = bean;
        if (mbd == null || !mbd.isSynthetic()) {
      // 這里??!又重新進(jìn)入這個(gè)方法,對bean的值進(jìn)行重新綁定!
            wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
        }

        try {
            invokeInitMethods(beanName, wrappedBean, mbd);
        }
        catch (Throwable ex) {
            throw new BeanCreationException(
                    (mbd != null ? mbd.getResourceDescription() : null),
                    beanName, "Invocation of init method failed", ex);
        }
        if (mbd == null || !mbd.isSynthetic()) {
            wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
        }

        return wrappedBean;
    }
}

3. 依賴@ConfigurationProperties的bean更新

但是,還有另一個(gè)問題。有些bean的字段值是根據(jù)ConfigurationProperties bean的配置值而生成的。當(dāng)Configuration bean的配置值更新之后,使用這個(gè)配置值的bean的字段也需要更新。

比如說MyPropertiesUsage依賴MyProperties的配置值z,生成自身的字段值myValue。

@Component
@ConfigurationProperties("x.y")
@Data
public class MyProperties {
    private String z = "";
}
@Data
@Component
public class MyPropertiesUsage {
    @Autowired
    private MyProperties myProperties;

    private String myValue;

    @PostConstruct
    public void init() {
        myValue = "my-" + myProperties.getZ();
    }
}

為了在更新MyProperties之后,觸發(fā)MyPropertiesUsage的更新,主要有幾個(gè)思路。

  1. MyProperties 添加初始化方法(比如實(shí)現(xiàn)接口InitializingBean、注解@PostConstruct指定),調(diào)用方法MyPropertiesUsage.init(),觸發(fā)MyPropertiesUsage重新初始化。缺點(diǎn)是不夠優(yōu)雅,沒有做到依賴反轉(zhuǎn),不夠通用。
  2. MyProperties 添加初始化方法(比如實(shí)現(xiàn)接口InitializingBean、注解@PostConstruct指定),調(diào)用發(fā)布自定義的事件MyPropertiesChangedEvent,MyPropertiesUsage監(jiān)聽事件MyPropertiesChangedEvent,重新執(zhí)行初始化方法。缺點(diǎn)是不夠通用,每次有相似的需求時(shí),都需要進(jìn)行額外的改造。
  3. 自定義注解RefreshAfterConfigurationPropertiesChanged,標(biāo)注在需要在配置變化時(shí)更新的bean上。當(dāng)監(jiān)聽到配置發(fā)生變化時(shí),自動(dòng)將所有標(biāo)注了該注解的bean重新初始化。

第三個(gè)思路比較通用,并且開發(fā)成本也比較低。我們可以代碼實(shí)現(xiàn):

自定義注解RefreshAfterConfigurationPropertiesChanged

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshAfterConfigurationPropertiesChanged {
}

MyPropertiesUsage添加注解RefreshAfterConfigurationPropertiesChanged

@Data
@Component
@RefreshAfterConfigurationPropertiesChanged
public class MyPropertiesUsage {

    @Autowired
    private MyProperties myProperties;

    private String myValue;

    @PostConstruct
    public void init() {
        myValue = "my-" + myProperties.getZ();
    }
}

修改ConfigurationPropertiesLiveRefresher,添加方法refreshBeansDependsOnConfigurationProperties,在監(jiān)聽到配置變更事件,并且配置已重新綁定之后,對標(biāo)注了ConfigurationPropertiesLiveRefresher對bean進(jìn)行重新初始化。

@Configuration
public class ConfigurationPropertiesLiveRefresher implements ConfigChangeListener, ApplicationRunner, ApplicationContextAware {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Value("#{'${apollo.bootstrap.namespaces:}'.split(',')}")
    private List<String> namespaces;

    @Autowired
    private ApplicationContext applicationContext;

    private static final Logger log = LoggerFactory.getLogger(ConfigurationPropertiesLiveRefresher.class);

    @Override
    public void run(ApplicationArguments args) {
        for (String namespace : namespaces) {
            ConfigService.getConfig(namespace).addChangeListener(this);
            log.info("Successfully added config change listener to namespace {}", namespace);
        }
    }

    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
        publisher.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        // 新增方法,刷新bean
        refreshBeansDependsOnConfigurationProperties();
        log.info("Successfully changed config change event {}", changeEvent.changedKeys());
    }

    private void refreshBeansDependsOnConfigurationProperties() {
      // 從容器中拿到所有標(biāo)注了RefreshAfterConfigurationPropertiesChanged的bean
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(RefreshAfterConfigurationPropertiesChanged.class);
      // 對所有bean先進(jìn)行銷毀,再對bean進(jìn)行初始化
        for (Map.Entry<String, Object> entry : beans.entrySet()) {
            this.applicationContext.getAutowireCapableBeanFactory()
                    .destroyBean(entry.getValue());
            this.applicationContext.getAutowireCapableBeanFactory()
                    .initializeBean(entry.getValue(), entry.getKey());
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

四、總結(jié)

本文在第二章中介紹了Spring配置的基本使用、第三章中介紹了Spring配置原理、第四章中介紹了日常開發(fā)中配置熱更新的一些實(shí)踐。

五、參考資料

  1. Spring Framework源碼
  2. Apollo Client源碼
  3. Spring Environment介紹
  4. Apollo 源碼解析 —— 客戶端配置 Spring 集成(一)之 XML 配置
  5. 自定義EnvironmentPostProcessor
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容