一、引言
配置是一個(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è)重要的成員:
- propertySources:維護(hù)所有配置來源
PropertySource的一個(gè)集合類 - 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è)接口EnvironmentPostProcessor、SmartApplicationListener、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)聽ApplicationEnvironmentPreparedEvent、ApplicationPreparedEvent這兩個(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.factories將EnvironmentPostProcessor的實(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)行注冊。比如,Apollo的com.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的成員變量:
-
environment:當(dāng)前Spring應(yīng)用的Environment -
placeholdersResolver:用于解析占位符,從Environment中取出值 -
resourceLoader:用于從文件系統(tǒng)中讀取配置文件 -
propertySourceLoaders:包含所有用于將配置文件加載為PropertySource的PropertySourceLoader -
profiles:保存當(dāng)前待處理的激活的Profile,這是一個(gè)隊(duì)列。一開始的時(shí)候,會(huì)有一個(gè)默認(rèn)的Profile,并且在讀入配置文件的時(shí)候,可以增加Profile。循環(huán)從隊(duì)列中取出Profile,直到隊(duì)列為空。 -
processedProfiles:保存所有被處理過的Profile -
activatedProfiles:是否已取出被激活的Profile列表。意思是只會(huì)讀取spring.profiles.active一次,先被讀取的優(yōu)先級高,會(huì)被采納;其他不會(huì)被采納。 -
loaded:map保存每個(gè)Profile的PropertySource -
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á)式即可。
基本邏輯就是:
從現(xiàn)有的
PropertySource初始化profiles隊(duì)列。也就是從環(huán)境變量、系統(tǒng)變量中取出。從
profiles隊(duì)頭取出Profile,然后從文件系統(tǒng)讀入該Profile的配置文件。并且若配置文件中有指定spring.profiles.active,并且之前未激活過,則將這些Profile加入到隊(duì)列中。循環(huán)讀,直到隊(duì)列為空。
因此,下面主要介紹兩個(gè)方法:initializeProfiles、load
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,基本邏輯是:
- 默認(rèn)添加一個(gè)null,后續(xù)會(huì)讀入文件application.yml或者其他application.文件
- 從現(xiàn)有的
PropertySource中讀入激活的Profile,并將其加入到隊(duì)列后 - 若未指定激活的
Profile,則添加一個(gè)叫default的Profile
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)
基本邏輯為:
- 獲取配置文件的的路徑位置,通過配置項(xiàng)
spring.config.location。若沒有則默認(rèn)用這些目錄,classpath:/、classpath:/config/、file:./、file:./config/*/、file:./config/ - 遍歷每個(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)
基本邏輯就是:
- 若傳進(jìn)來的location是文件,遍歷所有PropertySourceLoader,對文件進(jìn)行加載
- 若傳進(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ù)雜,一般來說有用的只有注釋的那兩處。
- 在
Profile不為空時(shí),拼接文件名 prefix + "-" + profile + fileExtension,隨后在文件系統(tǒng)查找并加載文件。 - 在
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,隨后將Document的PropertySource 插入到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è)思路。
-
MyProperties添加初始化方法(比如實(shí)現(xiàn)接口InitializingBean、注解@PostConstruct指定),調(diào)用方法MyPropertiesUsage.init(),觸發(fā)MyPropertiesUsage重新初始化。缺點(diǎn)是不夠優(yōu)雅,沒有做到依賴反轉(zhuǎn),不夠通用。 -
MyProperties添加初始化方法(比如實(shí)現(xiàn)接口InitializingBean、注解@PostConstruct指定),調(diào)用發(fā)布自定義的事件MyPropertiesChangedEvent,MyPropertiesUsage監(jiān)聽事件MyPropertiesChangedEvent,重新執(zhí)行初始化方法。缺點(diǎn)是不夠通用,每次有相似的需求時(shí),都需要進(jìn)行額外的改造。 - 自定義注解
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í)踐。
五、參考資料
- Spring Framework源碼
- Apollo Client源碼
- Spring Environment介紹
- Apollo 源碼解析 —— 客戶端配置 Spring 集成(一)之 XML 配置
- 自定義EnvironmentPostProcessor