認(rèn)識(shí)配置中心

前言

?在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集成成本非常低。但是其也存在如下一些問題:

  1. config client組件缺乏mvc工程的兼容敢会,特別對低版本Spring項(xiàng)目而言曾沈。
  2. 缺乏對使用者友好的UI操作界面,同類型產(chǎn)品Apollo在這一點(diǎn)上做更好鸥昏。
  3. 安全機(jī)制無法滿足微服務(wù)系統(tǒng)的需要塞俱,Spring Security只能提供簡單安全機(jī)制。
  4. 缺乏高階特性吏垮,比如環(huán)境隔離障涯、灰度發(fā)布支持、回滾機(jī)制等等膳汪。

?在本文中唯蝶,我們將進(jìn)行如下探索,并嘗試勾勒出一款配置中心產(chǎn)品遗嗽。

  1. 配置中心的必要性
  2. Spring粘我、Spring Boot中配置的加載機(jī)制。
  3. 一個(gè)好的配置中心應(yīng)該需要滿足哪些功能痹换。

由于本人負(fù)責(zé)了公司配置中心的建設(shè)征字,對配置中心的建設(shè)有了一些思考,所以想要通過文章進(jìn)行系統(tǒng)的整理娇豫,希望以后可以產(chǎn)出一款開源的配置中心產(chǎn)品柔纵。

一、為什么需要配置中心

?我們先來看一個(gè)標(biāo)準(zhǔn)的Spring Boot工程锤躁,如下所示:

boot工程加載配置.png

?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ù)填渠。

為什么需要配置中心.png



?隨著業(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
properties.png

?它提供了幾個(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);
    }
}
system properties.png
2承粤、讀取test.properties

?新增一個(gè)test.properties文件暴区,如下所示

vim test.properties
chandler.name=chandler
chandler.age=18
chandler.weight=75kg
test.properties.png

?使用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);
    }
}
test.properties輸出.png

2.2 Spring與Property

?Spring是一個(gè)非常優(yōu)秀的Java框架,在property方面也做了自己的增強(qiáng)處理仙粱。

2.2.1 PropertyPlaceholderConfigurer

?我們首先來看一下配置加載核心類PropertyPlaceholderConfigurer房交,作用是加載指定文件中的配置信息。源代碼如下所示:

propertyplaceholderconfigurer.png

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文件,是否忽略不存在的文件橄碾。
    PropertiesLoaderSupport.png

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)鍵要素如下所示:

  1. propertySources:MutablePropertySources腊满,存儲(chǔ)初始化配置源對象,存儲(chǔ)properties信息的CopyOnWriteArrayList集合培己。
  2. appliedPropertySources:存儲(chǔ)加工之后的配置源對象碳蛋,首先將配置中${...}進(jìn)行替換汛蝙,然后將beanFactory中所有bean中被@Value修飾或被@ConfigurationProperties作用的屬性將會(huì)被配置賦值边翼。
  3. environment:Spring所加載的Environment,包含System.getenv()和System.getProperties()桃煎,然后加載到propertySources中零蓉。
spring配置模塊-propertysourcesplaceholderconfigurer.png
2.2.3 @Value

?Spring Boot啟動(dòng)過程中笤受,有兩個(gè)比較重要的過程,如下:

  1. 掃描敌蜂、解析容器中的bean注冊到beanFactory上去箩兽,就像是信息登記一樣。
  2. 實(shí)例化章喉、初始化這些掃描到的bean汗贫。

?@Value的解析就是發(fā)生在第二階段,BeanPostProcessor定義了bean初始化前后用戶可以對bean進(jìn)行操作的接口方法囊陡,它的一個(gè)重要實(shí)現(xiàn)類AutowiredAnnotationBeanPostProcessor芳绩,為bean中的@Autowired和@Value注解的注入功能提供支持。

?@Value解析過程中的主要調(diào)用鏈撞反,如下時(shí)序圖表示:

value注解處理調(diào)用鏈路.png

?這里先簡單介紹一下圖中幾個(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.png



?如上所示正驻,可以展示出PropertySource的使用場景,需要注意如下:

  1. PropertySourcesPlaceholderConfigurer初始化PropertySource抢腐,將會(huì)將所有的PropertySource匯總并加載在一起拨拓,方便使用。
  2. 不同場景加載不同PropertySource氓栈,比如SystemEnvironmentPropertySource渣磷、RandomValuePropertySource、ServletConfigPropertySource授瘦。
  3. MapPropertySource作用是加載工程中的各種配置和環(huán)境變量醋界,其子類的實(shí)現(xiàn)也是針對不同場景。
MapPropertySource.png

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í)例化的部分打肝。如下所示:

PropertyPlaceholderConfigurer實(shí)例化.png

  • @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
PropertiesPropertySourceLoader.png

?這個(gè)類的作用是加載classpath下的properties和xml類型的配置文件的,它實(shí)現(xiàn)了PropertySourceLoader這個(gè)接口醉锅。

YamlPropertySourceLoader
YamlPropertySourceLoader.png

?這個(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)類如下所示:

Environment實(shí)現(xiàn)類.png



?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褒脯,如下所示:

spring boot environment.png

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)。如下圖所示:

程序體的性質(zhì).png

3.2 配置與環(huán)境

?如此脯颜,同一個(gè)程序體可以根據(jù)Environment不同轉(zhuǎn)化成不同的性質(zhì)的程序體哟旗。Environment不同需要改變的property數(shù)量是不定的,如下所示:


property數(shù)量是不定.png

?而且由于程序體數(shù)量是不定的栋操,property數(shù)量會(huì)是一個(gè)笛卡爾積的結(jié)果热幔,如下所示:


笛卡爾積的結(jié)果.png

配置與環(huán)境.png

?如上圖所示,可以讓一個(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):

  1. 配置數(shù)據(jù)更新
    1. 定時(shí)線程定時(shí)從配置中心服務(wù)端獲取配置。
    2. 長輪詢監(jiān)測配置變更熊响,再從配置中心服務(wù)端拉取配置旨别。
  2. 配置數(shù)據(jù)存儲(chǔ)
    1. 將配置數(shù)據(jù)加載到spring管理的PropertySource中
    2. 將配置數(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ù)配置中心呢吵冒?

  1. 業(yè)務(wù)配置中心是存在的纯命。存在于數(shù)據(jù)庫中,存在于系統(tǒng)配置中心中痹栖。一些公司也會(huì)開發(fā)業(yè)務(wù)配置中心亿汞,一般與業(yè)務(wù)耦合度高。
  2. 業(yè)務(wù)配置是復(fù)雜的揪阿,特殊化的疗我;支持業(yè)務(wù)配置會(huì)給配置中心會(huì)帶來復(fù)雜性。

四南捂、小結(jié)

如果需要給我修改意見的發(fā)送郵箱:erghjmncq6643981@163.com

轉(zhuǎn)發(fā)博客吴裤,請注明,謝謝溺健。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末麦牺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鞭缭,更是在濱河造成了極大的恐慌剖膳,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岭辣,死亡現(xiàn)場離奇詭異吱晒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沦童,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門枕荞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人搞动,你說我怎么就攤上這事躏精。” “怎么了鹦肿?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵矗烛,是天一觀的道長。 經(jīng)常有香客問我箩溃,道長瞭吃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任涣旨,我火速辦了婚禮歪架,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘霹陡。我一直安慰自己和蚪,他們只是感情好止状,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著攒霹,像睡著了一般怯疤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上催束,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天集峦,我揣著相機(jī)與錄音,去河邊找鬼抠刺。 笑死塔淤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的速妖。 我是一名探鬼主播高蜂,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼买优!你這毒婦竟也來了妨马?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤杀赢,失蹤者是張志新(化名)和其女友劉穎烘跺,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脂崔,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡滤淳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砌左。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脖咐。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖汇歹,靈堂內(nèi)的尸體忽然破棺而出屁擅,到底是詐尸還是另有隱情,我是刑警寧澤产弹,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布派歌,位于F島的核電站,受9級特大地震影響痰哨,放射性物質(zhì)發(fā)生泄漏胶果。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一斤斧、第九天 我趴在偏房一處隱蔽的房頂上張望早抠。 院中可真熱鬧,春花似錦撬讽、人聲如沸蕊连。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咪奖。三九已至盗忱,卻和暖如春酱床,著一層夾襖步出監(jiān)牢的瞬間羊赵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工扇谣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昧捷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓罐寨,卻偏偏與公主長得像靡挥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鸯绿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容