前言
?在17年時(shí),本人在開始接觸Spring Cloud體系構(gòu)建微服務(wù)系統(tǒng)建設(shè)時(shí)夜惭,通過Spring Cloud Config開始認(rèn)識(shí)分布式配置中心姻灶。Spring Cloud Config支持多種數(shù)據(jù)存儲(chǔ)方式,可以使用git诈茧、mysql等作為庫源产喉,與Spring Boot集成成本非常低。但是其也存在如下一些問題:
- config client組件缺乏mvc工程的兼容敢会,特別對低版本Spring項(xiàng)目而言曾沈。
- 缺乏對使用者友好的UI操作界面,同類型產(chǎn)品Apollo在這一點(diǎn)上做更好鸥昏。
- 安全機(jī)制無法滿足微服務(wù)系統(tǒng)的需要塞俱,Spring Security只能提供簡單安全機(jī)制。
- 缺乏高階特性吏垮,比如環(huán)境隔離障涯、灰度發(fā)布支持、回滾機(jī)制等等膳汪。
?在本文中唯蝶,我們將進(jìn)行如下探索,并嘗試勾勒出一款配置中心產(chǎn)品遗嗽。
- 配置中心的必要性
- Spring粘我、Spring Boot中配置的加載機(jī)制。
- 一個(gè)好的配置中心應(yīng)該需要滿足哪些功能痹换。
由于本人負(fù)責(zé)了公司配置中心的建設(shè)征字,對配置中心的建設(shè)有了一些思考,所以想要通過文章進(jìn)行系統(tǒng)的整理娇豫,希望以后可以產(chǎn)出一款開源的配置中心產(chǎn)品柔纵。
一、為什么需要配置中心
?我們先來看一個(gè)標(biāo)準(zhǔn)的Spring Boot工程锤躁,如下所示:
?Spring Boot工程會(huì)默認(rèn)從rescource目錄下加載application.properties文件搁料,如果有application-dev或详,application-sit,application-prod等文件郭计,會(huì)根據(jù)環(huán)境選擇加載目標(biāo)文件霸琴,一個(gè)工程一般會(huì)有4個(gè)配置文件。
?為了SLA昭伸,我們需要將services們進(jìn)行集群梧乘,假定一個(gè)service需要部署在3個(gè)節(jié)點(diǎn)上。對于一個(gè)service而言庐杨,會(huì)有12個(gè)文件分布在不同節(jié)點(diǎn)(虛擬服務(wù)器选调、真實(shí)服務(wù)器、容器pod等)中灵份,當(dāng)我們需要修改一個(gè)配置時(shí)仁堪,我們需要在3個(gè)節(jié)點(diǎn)中進(jìn)行同樣的修改,目前的情況對于系統(tǒng)運(yùn)維和程序員來說還比較好維護(hù)填渠。
?隨著業(yè)務(wù)的發(fā)展弦聂、系統(tǒng)架構(gòu)的升級,從傳統(tǒng)的單體應(yīng)用架構(gòu)逐步進(jìn)行業(yè)務(wù)拆分往微服務(wù)架構(gòu)發(fā)展氛什,配置文件也隨之迅速膨脹并散落在各個(gè)service pod中莺葫,變得越來越多難以管理和維護(hù)。所以在微服務(wù)系統(tǒng)中枪眉,我們需要一個(gè)集中式的配置中心捺檬,需要其將散落在系統(tǒng)各個(gè)角落的配置集中起來,并進(jìn)行可視化的管理和維護(hù)贸铜。
二欺冀、Spring、Spring Boot與配置中心
?由于Spring優(yōu)秀的產(chǎn)品特性萨脑,大部分Java項(xiàng)目的開發(fā)都離不開Spring隐轩,很難將兩種分隔開來。了解了Spring加載properties的機(jī)制渤早,也就了解了配置中心客戶端組件如何實(shí)現(xiàn)配置加載职车。<font color=red>(spring版本:5.2.3.RELEASE)</font>
2.1 Java與Property
?Java中有個(gè)比較重要的Properties(Java.util.Properties),主要用于讀取Java的配置文件鹊杖。在Java中悴灵,其配置文件常為.properties文件,文件的內(nèi)容格式為“key=value”的格式骂蓖,文本注釋使用“#”积瞒。
2.1.1 Properties
?它提供了幾個(gè)主要的方法:
- getProperty (String key):用指定的鍵在此屬性列表中搜索屬性。也就是通過參數(shù) key 登下,得到 key 所對應(yīng)的 value茫孔。
- load (InputStream inStream):從輸入流中讀取屬性列表(鍵和元素對)叮喳。通過對指定的文件(比如說上面的 test.properties 文件)進(jìn)行裝載來獲取該文件中的所有鍵 - 值對。以供 getProperty ( String key) 來搜索缰贝。
- setProperty ( String key, String value) 馍悟,調(diào)用 Hashtable 的方法 put 。他通過調(diào)用基類的put方法來設(shè)置 鍵 - 值對剩晴。
- store (OutputStream out, String comments):以適合使用 load 方法加載到 Properties 表中的格式锣咒,將此 Properties 表中的屬性列表(鍵和元素對)寫入輸出流。與 load 方法相反赞弥,該方法將鍵 - 值對寫入到指定的文件中去毅整。
- clear ():清除所有裝載的 鍵 - 值對。該方法在基類中提供绽左。
2.1.2 action
1悼嫉、獲取JVM系統(tǒng)屬性信息
?JVM都有自己的系統(tǒng)配置文件(system.properties),將其輸出妇菱,如下所示:
/**
* java properties測試
*
* @author 錢丁君-chandler 2020-07-05 15:08
* @version 1.0
* @since 1.8
*/
public class PropertiesTest {
public static void main(String[] args) {
Properties pps = System.getProperties();
pps.list(System.out);
}
}
2承粤、讀取test.properties
?新增一個(gè)test.properties文件暴区,如下所示
vim test.properties
chandler.name=chandler
chandler.age=18
chandler.weight=75kg
?使用Properties將配置項(xiàng)在Java中進(jìn)行輸出闯团,如下所示:
/**
* java properties測試
*
* @author 錢丁君-chandler 2020-07-05 15:08
* @version 1.0
* @since 1.8
*/
public class PropertiesTest {
public static void main(String[] args) throws IOException {
Properties pps = new Properties();
pps.load(new FileInputStream("/Users/qiandingjun/resource/git-repository/test/test.properties"));
pps.list(System.out);
}
}
2.2 Spring與Property
?Spring是一個(gè)非常優(yōu)秀的Java框架,在property方面也做了自己的增強(qiáng)處理仙粱。
2.2.1 PropertyPlaceholderConfigurer
?我們首先來看一下配置加載核心類PropertyPlaceholderConfigurer房交,作用是加載指定文件中的配置信息。源代碼如下所示:
public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport {
伐割、候味、、
/**
* Visit each bean definition in the given bean factory and attempt to replace ${...} property
* placeholders with values from the given properties.
*/
@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
throws BeansException {
StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props);
doProcessProperties(beanFactoryToProcess, valueResolver);
}
隔心、白群、、
}
在Spring中硬霍,使用PropertyPlaceholderConfigurer可以在XML配置文件中加入外部屬性文件帜慢,當(dāng)然也可以指定外部文件的編碼。PropertyPlaceholderConfigurer可以將上下文(配置文 件)中的屬性值放在另一個(gè)單獨(dú)的標(biāo)準(zhǔn)java Properties文件中去唯卖。在XML文件中用${key}替換指定的properties文件中的值粱玲。在MVC工程中,我們可以進(jìn)行如下配置加載額外的配置文件::
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1">
<property name="localOverride" value="true">
<property name="ignoreResourceNotFound" value="true">
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
<value>classpath:config/*.properties</value>
<value>classpath:chandler.properties</value>
</list>
</property>
</bean>
- localOverride:加載的數(shù)據(jù)是否覆蓋spring之前加載的同名屬性
- locations:配置properties文件路徑拜轨,例如"classpath:xxxx"抽减;"file:xxxx"
-
ignoreResourceNotFound:加載多個(gè)properties文件,是否忽略不存在的文件橄碾。
PropertyPlaceholderConfigurer在Spring 5.2之后被設(shè)置為過期卵沉,以后將會(huì)被PropertySourcesPlaceholderConfigurer代替颠锉。
2.2.2 PropertySourcesPlaceholderConfigurer
public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerSupport implements EnvironmentAware {
、偎箫、木柬、
@Nullable
//初始化配置源對象,存儲(chǔ)properties信息的List集合淹办,
private MutablePropertySources propertySources;
@Nullable
//加工之后的配置源對象
private PropertySources appliedPropertySources;
@Nullable
private Environment environment;
眉枕、、怜森、
@Override
//加載所有propertySources
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (this.propertySources == null) {
this.propertySources = new MutablePropertySources();
if (this.environment != null) {
this.propertySources.addLast(
new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
@Override
@Nullable
public String getProperty(String key) {
return this.source.getProperty(key);
}
}
);
}
try {
PropertySource<?> localPropertySource =
new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
if (this.localOverride) {
this.propertySources.addFirst(localPropertySource);
}
else {
this.propertySources.addLast(localPropertySource);
}
}
catch (IOException ex) {
throw new BeanInitializationException("Could not load properties", ex);
}
}
processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
this.appliedPropertySources = this.propertySources;
}
速挑、、副硅、
/**
* Visit each bean definition in the given bean factory and attempt to replace ${...} property
* placeholders with values from the given properties.
*/
//加配置中${...}進(jìn)行替換
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
final ConfigurablePropertyResolver propertyResolver) throws BeansException {
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
propertyResolver.setValueSeparator(this.valueSeparator);
//StringValueResolver的實(shí)現(xiàn)姥宝,使用propertyResolver進(jìn)行賦值
StringValueResolver valueResolver = strVal -> {
String resolved = (this.ignoreUnresolvablePlaceholders ?
propertyResolver.resolvePlaceholders(strVal) :
propertyResolver.resolveRequiredPlaceholders(strVal));
if (this.trimValues) {
resolved = resolved.trim();
}
return (resolved.equals(this.nullValue) ? null : resolved);
};
//將StringValueResolver的實(shí)現(xiàn)添加到beanfactory
doProcessProperties(beanFactoryToProcess, valueResolver);
}
}
?仔細(xì)理解PropertySourcesPlaceholderConfigurer中的代碼,從中我們可以窺見Spring配置模塊的全局恐疲。關(guān)鍵要素如下所示:
- propertySources:MutablePropertySources腊满,存儲(chǔ)初始化配置源對象,存儲(chǔ)properties信息的CopyOnWriteArrayList集合培己。
- appliedPropertySources:存儲(chǔ)加工之后的配置源對象碳蛋,首先將配置中${...}進(jìn)行替換汛蝙,然后將beanFactory中所有bean中被@Value修飾或被@ConfigurationProperties作用的屬性將會(huì)被配置賦值边翼。
- environment:Spring所加載的Environment,包含System.getenv()和System.getProperties()桃煎,然后加載到propertySources中零蓉。
2.2.3 @Value
?Spring Boot啟動(dòng)過程中笤受,有兩個(gè)比較重要的過程,如下:
- 掃描敌蜂、解析容器中的bean注冊到beanFactory上去箩兽,就像是信息登記一樣。
- 實(shí)例化章喉、初始化這些掃描到的bean汗贫。
?@Value的解析就是發(fā)生在第二階段,BeanPostProcessor定義了bean初始化前后用戶可以對bean進(jìn)行操作的接口方法囊陡,它的一個(gè)重要實(shí)現(xiàn)類AutowiredAnnotationBeanPostProcessor芳绩,為bean中的@Autowired和@Value注解的注入功能提供支持。
?@Value解析過程中的主要調(diào)用鏈撞反,如下時(shí)序圖表示:
?這里先簡單介紹一下圖中幾個(gè)類的作用:
- AbstractAutowireCapableBeanFactory:提供bean創(chuàng)建妥色、屬性填充、自動(dòng)裝配遏片、初始化嘹害。支持自動(dòng)裝配構(gòu)造函數(shù)撮竿,屬性按名稱和類型裝配,實(shí)現(xiàn)了AutowiredCapableBeanFactory接口定義的createBean方法笔呀。
- AutowiredAnnotationBeanPostProcessor:裝配bean中使用注解標(biāo)注的成員變量幢踏,setter方法、任意的配置方法许师。比較典型的是@Autowired注解和@Value注解房蝉。
- InjectionMetadata:類的注入元數(shù)據(jù),可能是類的方法或?qū)傩缘任⑶贏utowiedAnnotationBeanPostProcessor類中被使用搭幻。
- AutowiredFieldElement:是AutowiredAnnotationBeanPostProcesso的一個(gè)私有內(nèi)部類,繼承InjectionMetadata.InjectedElement逞盆,描述注解的字段檀蹋。
-
DefaultListBeanFactory:實(shí)現(xiàn)接口ConfigurablelistableBeanFactory、BeanDefinitionRegistry
(bean定義的注冊接口)云芦,并繼承AbstractAutowireCapableBeanFactory俯逾,實(shí)現(xiàn)全部類管理的功能。- resolveDependency函數(shù):處理bean中屬性的依賴舅逸,包含bean依賴和property依賴
- StringValueResolver:一個(gè)定義了處置字符串的接口桌肴,只有一個(gè)接口方法resolveStringValue,可以用來解決占位符字符串堡赔。本文中的主要實(shí)現(xiàn)類在PropertySourcePlaceholderConfigure#processProperties方法中通過lambda表達(dá)式定義的识脆。供ConfigurableBeanFactory類使用设联。
- PropertySourcesPlaceholderConfigurer:PropertySources核心管理類善已,負(fù)責(zé)加載所有的PropertySource,并在processProperties中實(shí)現(xiàn)了StringValueResolver的resolveStringValue离例,通過beanFactory的addEmbeddedValueResolver添加進(jìn)去换团。
- PropertyPlaceholderHelper:工具類,在AbstractPropertyResolver的doResolvePlaceholders中使用宫蛆,用來將value注解中${...}替換為目標(biāo)property的value艘包。
?AbstractAutowireCapableBeanFactory#populateBean方法用于填充bean屬性,這些屬性需要被@Value和@Autowired修飾耀盗。
- @Autowired修飾:從listBean根據(jù)類型或beanName獲取bean想虎,進(jìn)行屬性填充。
- @Value修飾:從Property中通過key獲取value叛拷,并進(jìn)行輸出填充舌厨。
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
、忿薇、裙椭、
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.根據(jù)名稱從beanfactory中查找并填充到屬性中
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.根據(jù)類型從beanfactory中查找并填充到屬性中
if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}
boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);
if (hasInstAwareBpps || needsDepCheck) {
if (pvs == null) {
pvs = mbd.getPropertyValues();
}
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
if (hasInstAwareBpps) {
//遍歷所有前置處理器BeanPostProcessor
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvs == null) {
return;
}
}
}
}
if (needsDepCheck) {
checkDependencies(beanName, mbd, filteredPds, pvs);
}
}
if (pvs != null) {
applyPropertyValues(beanName, mbd, bw, pvs);
}
}
?InjectionMetadata#inject逐個(gè)裝配bean的配置屬性躏哩。
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Collection<InjectedElement> checkedElements = this.checkedElements;
Collection<InjectedElement> elementsToIterate =
(checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
if (logger.isDebugEnabled()) {
logger.debug("Processing injected element of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
}
?PropertyPlaceholderHelper#parseStringValue解析屬性值,從propertySource中獲取property揉燃,并返回value結(jié)果扫尺。
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
StringBuilder result = new StringBuilder(value);
int startIndex = value.indexOf(this.placeholderPrefix);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// 從propertySource中獲取property
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}
return result.toString();
}
2.2.4 PropertySource
?PropertySource的作用是通過key,value的形式存儲(chǔ)property炊汤,如下所示:
?如上所示正驻,可以展示出PropertySource的使用場景,需要注意如下:
- PropertySourcesPlaceholderConfigurer初始化PropertySource抢腐,將會(huì)將所有的PropertySource匯總并加載在一起拨拓,方便使用。
- 不同場景加載不同PropertySource氓栈,比如SystemEnvironmentPropertySource渣磷、RandomValuePropertySource、ServletConfigPropertySource授瘦。
- MapPropertySource作用是加載工程中的各種配置和環(huán)境變量醋界,其子類的實(shí)現(xiàn)也是針對不同場景。
2.3 Spring Boot與Property
?Spring Boot工程相比較于MVC工程提完,多了很多“自動(dòng)化配置”形纺,其中就包括了配置的自動(dòng)化加載,那它是怎么做的呢徒欣?<font color=red>(boot版本:2.2.3.RELEASE)</font>
2.3.1 PropertySourcesPlaceholderConfigurer實(shí)例化
?很簡單逐样,直接查詢PropertySourcesPlaceholderConfigurer在那里被使用到,然后找到其中實(shí)例化的部分打肝。如下所示:
- @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE):說明其加載時(shí)的優(yōu)先級比較高脂新,這樣有利于在其他bean使用配置之前,加載好配置粗梭。
- @Configuration(proxyBeanMethods = false):Java Configuration方式初始化Spring bean争便,不過想讓PropertySourcesPlaceholderConfigurer注冊到Spring容器,還需要配合spring.factories断医。
2.3.2 PropertySources
?Spring Boot在啟動(dòng)時(shí)滞乙,默認(rèn)加載classpath下所有.properties文件和.yml文件,Boot是如何實(shí)現(xiàn)的呢鉴嗤?
?再次查看spring.factories斩启,查詢PropertySource如下所示:
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
PropertiesPropertySourceLoader
?這個(gè)類的作用是加載classpath下的properties和xml類型的配置文件的,它實(shí)現(xiàn)了PropertySourceLoader這個(gè)接口醉锅。
YamlPropertySourceLoader
?這個(gè)類的作用是加載classpath下yml和yaml類型的配置文件兔簇,它實(shí)現(xiàn)了PropertySourceLoader這個(gè)接口。
PropertySourceLoader
public interface PropertySourceLoader {
String[] getFileExtensions();
List<PropertySource<?>> load(String name, Resource resource) throws IOException;
}
- getFileExtensions:加載的文件類型擴(kuò)展。
- load:加載配置文件男韧,返回List<PropertySource<?>>朴摊。
如果我們需要擴(kuò)展新的配置文件類型,比如JSON此虑;我們可以定義一個(gè)CsutomPropertySourceLoader實(shí)現(xiàn)PropertySourceLoader即可甚纲。
2.3.3 Environment
?Environment的源碼如下所示:
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();
String[] getDefaultProfiles();
boolean acceptsProfiles(Profiles profiles);
}
?Environment接口的方法是獲取當(dāng)前激活的Profiles和默認(rèn)的Profiles,得知當(dāng)前激活的切面朦前。由于繼承了PropertyResolver介杆,具備獲取來自spring管理的properties的能力。Environment的實(shí)現(xiàn)類如下所示:
?boot中有3個(gè)實(shí)現(xiàn)類韭寸,如下所示:
- StandardEnvironment:適用于非WEB項(xiàng)目春哨,加載來自SYSTEM的properties和environment。
- StandardServletEnvironment:繼承StandardEnvironment恩伺,適用于WEB項(xiàng)目赴背,還加載了來自SERVLET初始化參數(shù)和JNDI 配置。
- StandardReactiveWebEnvironment:繼承StandardServletEnvironment晶渠,目前沒有做額外的動(dòng)作凰荚。
?Spring Boot會(huì)根據(jù)項(xiàng)目的使用場景,選擇性地加載對應(yīng)的Environment褒脯,如下所示:
StandardServletEnvironment
?StandardServletEnvironment適用于WEB項(xiàng)目便瑟,源碼如下:
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
/** Servlet context init parameters property source name: {@value}. */
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
/** Servlet config init parameters property source name: {@value}. */
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
/** JNDI property source name: {@value}. */
public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
super.customizePropertySources(propertySources);
}
@Override
public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}
}
?如上所示,在Environment初始化時(shí)番川,其所需的PropertySources都會(huì)進(jìn)行一一加載到涂。
如果想要擴(kuò)展更多Property,可以通過自定義Environment方式來根據(jù)需要加載PropertySources颁督;
2.3.4 SpringBoot#run
?我們回歸到Spring Boot啟動(dòng)之初践啄,先來觀察一下run方法:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
//第一步獲取并啟動(dòng)監(jiān)聽器Listener
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//第二步構(gòu)建應(yīng)用環(huán)境Environment與屬性源PropertySource
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//輸出Banner
Banner printedBanner = printBanner(environment);
//第三步初始化上下文Context
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
、适篙、往核、
return context;
}
- 第一步獲取并啟動(dòng)監(jiān)聽器Listeners
- 第二步構(gòu)建應(yīng)用環(huán)境Environment與屬性源PropertySources
- 第三步初始化上下文Context
?配置的加載在上下文初始化之前箫爷,這也是合理的嚷节。配置中心客戶端組件需要在應(yīng)用初始化加載來自服務(wù)端的配置,這就要求我們對屬性源PropertySource加載進(jìn)行擴(kuò)展虎锚。
CustomPropertySourceListener
/**
* 自定義配置中心客戶端組件加載監(jiān)聽器
*
* @author 錢丁君-chandler 2020-07-05 18:11
* @version 1.0
* @since 1.8
*/
@Slf4j
public class CustomPropertySourceListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
log.info("Start read Custom property!");
ConfigurableEnvironment configurableEnvironment = event.getEnvironment();
Properties prop = null;//初始化Properties硫痰,如果配置來源于服務(wù)端,通過請求獲取轉(zhuǎn)化成Properties
configurableEnvironment.getPropertySources().addFirst(new PropertiesPropertySource("chandlerProperty", prop));
log.info("End read Custom property!");
}
}
?如上所示窜护,我們獲取通過接口調(diào)用效斑,從配置中心獲取所有的配置信息,然后將其添加到ConfigurableEnvironment中柱徙,這樣意味著我們的property被加載到spring管理的property源中缓屠。如果想要使用奇昙,可以通過@Value注解非常便利。
spring.factories
?在classp下新建spring.factories文件敌完,添加如下內(nèi)容:
org.springframework.context.ApplicationListener=\
com.chandler.instance.client.example.config.CustomPropertySourceListener
?這樣就可以在spring boot啟動(dòng)時(shí)储耐,被加載并啟動(dòng)生效。
三滨溉、我們需要什么樣的配置中心
3.1 什么是配置
Property:n什湘,性質(zhì)
?Property英文原意表達(dá)的很好,一個(gè)事物的性質(zhì)晦攒,同一個(gè)事物在不同環(huán)境Environment中闽撤,會(huì)有不一樣的表現(xiàn)。如下圖所示:
3.2 配置與環(huán)境
?如此脯颜,同一個(gè)程序體可以根據(jù)Environment不同轉(zhuǎn)化成不同的性質(zhì)的程序體哟旗。Environment不同需要改變的property數(shù)量是不定的,如下所示:
?而且由于程序體數(shù)量是不定的栋操,property數(shù)量會(huì)是一個(gè)笛卡爾積的結(jié)果热幔,如下所示:
?如上圖所示,可以讓一個(gè)程序體變更自身的性質(zhì)在不同環(huán)境中表現(xiàn)出符合預(yù)期的能力讼庇,這些能力可以是不同環(huán)境相同表現(xiàn)绎巨,也可以是不同環(huán)境不同表現(xiàn)。
3.2 配置中心
?首先我們先了解一下市面上有哪些優(yōu)秀的配置中心蠕啄,如下所示:
- spring-cloud-config:spring出品场勤,與spring cloud無縫對接,是我接觸的第一個(gè)開源配置中心框架歼跟。
- disconf:百度的一個(gè)分布式配置中心和媳。
- Apollo:攜程框架部門研發(fā)的開源配置管理中心,具備規(guī)范的權(quán)限哈街、流程治理等特性留瞳,業(yè)界使用廣泛。
3.2.1 配置中心框架比對
功能特性
功能點(diǎn) | 優(yōu)先級 | config | disconf | apollo | 備注 |
---|---|---|---|---|---|
靜態(tài)配置加載 | 高 | 支持 | 支持 | 支持 | |
配置熱更新 | 高 | 支持 | 支持 | 支持 | |
統(tǒng)一管理界面 | 高 | 無 | 支持 | 支持 | |
多環(huán)境支持 | 中 | 無 | 支持 | 支持 | |
多集群支持 | 中 | 無 | 支持 | ||
灰度發(fā)布 | 中 | 無 | 支持 | 無 | |
本地配置緩存 | 中 | 無 | 支持 | 支持 | |
配置生效時(shí)間 | 中 | 刷新生效 | 實(shí)時(shí) | 實(shí)時(shí) | |
用戶權(quán)限管理 | 高 | 無 | 支持 | 支持 | |
授權(quán)骚秦、審核她倘、審批 | 高 | 無 | 無 | 無 | |
配置版本管理 | 高 | Git做版本管理 | 提供發(fā)布?xì)v史和回滾按鈕 | 操作記錄有落數(shù)據(jù)庫,但無查詢接口 | |
資源監(jiān)控 | 中 | 無 | 支持 | 支持 | |
告警通知 | 中 | 無 | 支持作箍,郵件方式告警 | 支持硬梁,郵件方式告警 | |
依賴關(guān)系 | 高 | 無 | 無 | 無 |
技術(shù)路線兼容性
?配置中心需要考慮與現(xiàn)有系統(tǒng)的兼容性,胞得,以及是否引入額外的第三方組件荧止。
功能點(diǎn) | 優(yōu)先級 | config | apollo | disconf | 備注 |
---|---|---|---|---|---|
SpringBoot | 高 | 原生支持 | 支持 | 與spring boot無相關(guān) | |
SpringCloud | 高 | 原生支持 | 支持 | 與spring cloud無相關(guān) | |
客戶端支持 | 低 | JAVA | JAVA、.Net | JAVA | |
業(yè)務(wù)系統(tǒng)侵入性 | 高 | 弱 | 弱 | 弱,支持注解及xml方式 | |
依賴組件 | 高 | Eureka | Eureka | zookeeper |
可用性與易用性
功能點(diǎn) | 優(yōu)先級 | config | disconf | apollo | 備注 |
---|---|---|---|---|---|
單點(diǎn)故障 | 高 | 支持HA部署 | 支持HA部署 | 支持HA部署跃巡,高可用由zookeeper保證 | |
多數(shù)據(jù)中心部署 | 高 | 支持 | 支持 | 支持 | |
配置界面 | 高 | 無 | 統(tǒng)一界面 | 統(tǒng)一界面 |
3.2.2 配置中心應(yīng)用場景
1危号、將配置進(jìn)行集中管理
?一般而言,配置中心需要滿足的第一個(gè)場景素邪,將分散的配置信息集中葱色。由于應(yīng)用數(shù)理越來越多,配置文件成本增長娘香,特別是一個(gè)微服務(wù)多節(jié)點(diǎn)部署時(shí)苍狰,每次變更都需要修改多個(gè)節(jié)點(diǎn),配置維護(hù)成本成本增長烘绽。
?集中管理是配置中心的最基本的場景需求淋昭,能力包含配置數(shù)據(jù)存儲(chǔ)和配置數(shù)據(jù)變更操作,可視化界面的友好性也是很重要的安接。配置管理的實(shí)現(xiàn)并不復(fù)雜翔忽,配置的數(shù)據(jù)結(jié)構(gòu)很重要,需要支持多版本盏檐、集群歇式、微環(huán)境等功能。
2胡野、配置項(xiàng)運(yùn)行時(shí)變更
?動(dòng)態(tài)變更配置值也是很重要的一個(gè)場景需求材失,可以避免應(yīng)用重啟,降低配置變更的成本硫豆。
?配置項(xiàng)運(yùn)行時(shí)變更龙巨,需要考慮如下幾點(diǎn):
- 配置數(shù)據(jù)更新
- 定時(shí)線程定時(shí)從配置中心服務(wù)端獲取配置。
- 長輪詢監(jiān)測配置變更熊响,再從配置中心服務(wù)端拉取配置旨别。
- 配置數(shù)據(jù)存儲(chǔ)
- 將配置數(shù)據(jù)加載到spring管理的PropertySource中
- 將配置數(shù)據(jù)在本地緩存、更新汗茄、使用秸弛。
3、業(yè)務(wù)場景
?大多數(shù)的配置中心都只是保存與業(yè)務(wù)無關(guān)的配置洪碳,業(yè)務(wù)模塊中也需要配置數(shù)據(jù)递览,由于沒有配置中心支持,一般是將業(yè)務(wù)配置數(shù)據(jù)保存在服務(wù)本地的數(shù)據(jù)中偶宫。
?業(yè)務(wù)配置的需求是大量存在的非迹,比如,表單結(jié)構(gòu)是配置纯趋,運(yùn)營策略也是配置,優(yōu)惠方案也是配置。
為什么沒有業(yè)務(wù)配置中心呢吵冒?
- 業(yè)務(wù)配置中心是存在的纯命。存在于數(shù)據(jù)庫中,存在于系統(tǒng)配置中心中痹栖。一些公司也會(huì)開發(fā)業(yè)務(wù)配置中心亿汞,一般與業(yè)務(wù)耦合度高。
- 業(yè)務(wù)配置是復(fù)雜的揪阿,特殊化的疗我;支持業(yè)務(wù)配置會(huì)給配置中心會(huì)帶來復(fù)雜性。
四南捂、小結(jié)
如果需要給我修改意見的發(fā)送郵箱:erghjmncq6643981@163.com
轉(zhuǎn)發(fā)博客吴裤,請注明,謝謝溺健。