spring-cloud-config 源碼解析

spring-cloud-config 源碼解析:

  本文主要針對 spring-cloud-dependencies   Hoxton.SR4版本, spring-cloud-config-server/client的 2.2.2.RELEASE 版本進行源碼的解析。

  對於未接觸過 Config 的小夥伴可以參考 https://www.cnblogs.com/wuzhenzhao/p/10670580.html 進行一些基礎知識的瞭解。

  本文主要從以下幾個點來分析:

  1. @Value 的源碼解析
  2. Environment的初始化
  3. SpringBoot 配置的加載過程
  4. Config Client 配置加載過程
  5. Config Server獲取配置過程
  6. 實現自定義配置中心 PropertySourceLocator (基於拓展org.springframework.cloud.bootstrap.BootstrapConfiguration)

@Value 的源碼解析

  在 spring-cloud-config 配置中心的使用中,我們通常只需要使用 @Value 註解,就能完成屬性的注入。而 @Value 基於 Environment 的機制。我們先來看一下 @Value的注入過程

  以如下代碼爲例:

@Value("${wuzz.name}")
private String name;

  @Value 發生於 依賴注入的階段,對於依賴注入不熟悉的朋友可以先參考:https://www.cnblogs.com/wuzhenzhao/p/10882050.html

   在注入的流程中有個至關重要的方法:AbstractAutowireCapableBeanFactory#populateBean

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
        //........
        PropertyDescriptor[] filteredPds = null;
        if (hasInstAwareBpps) {
            if (pvs == null) {
                pvs = mbd.getPropertyValues();
            }
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
                if (bp instanceof InstantiationAwareBeanPostProcessor) {
                    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
            // @Value  的注入入口,基於 AutowiredAnnotationBeanPostProcessor
                    PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
                    if (pvsToUse == null) {
                        if (filteredPds == null) {
                            filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
                        }
                        pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
                        if (pvsToUse == null) {
                            return;
                        }
                    }
                    pvs = pvsToUse;
                }
            }
        }
        //.......
}

  基於 AutowiredAnnotationBeanPostProcessor#postProcessProperties ,這個東西有點類似於類的後置處理器。

@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
     // 掃描註解元數據
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try { // 注入
            metadata.inject(bean, beanName, pvs);
        }
        catch (BeanCreationException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
}

  調用重載的方法 inject,這裏有很多的實現,基於目前我們注入是一個 屬性,調用到 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject ,最終會進入 DefaultListableBeanFactory#doResolveDependency

@Nullable
    public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
            @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
        InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
        try {
            Object shortcut = descriptor.resolveShortcut(this);
            if (shortcut != null) {
                return shortcut;
            }
        
            Class<?> type = descriptor.getDependencyType();
       // 獲取 @Value 的表達式
            Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
            if (value != null) {
                if (value instanceof String) {
            //處理表達式的值,其實就是從 Eviromengt裏面去獲取
                    String strVal = resolveEmbeddedValue((String) value);
                    BeanDefinition bd = (beanName != null && containsBean(beanName) ?
                            getMergedBeanDefinition(beanName) : null);
                    value = evaluateBeanDefinitionString(strVal, bd);
                }
                TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
                try {
                    return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
                }
                catch (UnsupportedOperationException ex) {
                    // A custom TypeConverter which does not support TypeDescriptor resolution...
            // 轉換類型,進行實際注入
                    return (descriptor.getField() != null ?
                            converter.convertIfNecessary(value, type, descriptor.getField()) :
                            converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
                }
            }
      // ........
        }
        finally {
            ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
        }
    }

   DefaultListableBeanFactory#doResolveDependency 作用是處理 bean中的依賴。由此可見,處理 @Value 註解的時機是在getBean方法中,即SpringApplication#run的最後一步,實例化 bean。當獲取 @Value 註解中的表達式之後,進入了  resolveEmbeddedValue  方法,來替換表達式的值:

public String resolveEmbeddedValue(@Nullable String value) {
        if (value == null) {
            return null;
        }
        String result = value;
      // 遍歷 StringValueResolver
        for (StringValueResolver resolver : this.embeddedValueResolvers) {
            result = resolver.resolveStringValue(result);
            if (result == null) {
                return null;
            }
        }
        return result;
}

  通過代碼邏輯我們看到,對於屬性的解析已經委託給了StringValueResolver 對應的實現類,接下來我們就要分析一下這個 StringValueResolver 是如何初始化的。

  StringValueResolver 的初始化 依賴於 PropertySourcesPlaceholderConfigurer ,來看一下該類類圖:

   很明顯 ,該類實現了 BeanFactoryPostProcessor ,必然會執行 postProcessBeanFactory 方法。

@Override
    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;
}

  然後進入   processProperties :

protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            final ConfigurablePropertyResolver propertyResolver) throws BeansException {
    // 設置佔位符的前綴:"{"
    propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
    // 設置佔位符的後綴:"}"
    propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
    // 設置默認值分隔符:":"
    propertyResolver.setValueSeparator(this.valueSeparator);
    // 生成處理 ${...} 表達式的處理器
        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);
        };
        // 將處理器放入 Spring 容器
        doProcessProperties(beanFactoryToProcess, valueResolver);
}
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            StringValueResolver valueResolver) {

        BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);

        String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
        for (String curName : beanNames) {
            // Check that we're not parsing our own bean definition,
            // to avoid failing on unresolvable placeholders in properties file locations.
            if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
                BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
                try {
                    visitor.visitBeanDefinition(bd);
                }
                catch (Exception ex) {
                    throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
                }
            }
        }

        // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
        beanFactoryToProcess.resolveAliases(valueResolver);

        // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
    // 將 StringValueResolver 存入 BeanFactory 中
    beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}

@Override
public void addEmbeddedValueResolver(StringValueResolver valueResolver) {
        Assert.notNull(valueResolver, "StringValueResolver must not be null");
        this.embeddedValueResolvers.add(valueResolver);
}

  回到 AbstractBeanFactory#resolveEmbeddedValue。這個時候我們能知道 embeddedValueResolvers 裏面就一個實現,即 PropertySourcesPlaceholderConfigurer#processProperties 方法內構造的匿名內部類:

StringValueResolver valueResolver = strVal -> {
        // 默認是 false 走後面的邏輯
            String resolved = (this.ignoreUnresolvablePlaceholders ?
                    propertyResolver.resolvePlaceholders(strVal) :
                    propertyResolver.resolveRequiredPlaceholders(strVal));
            if (this.trimValues) {
                resolved = resolved.trim();
            }
            return (resolved.equals(this.nullValue) ? null : resolved);
};

  然後這裏會走propertyResolver.resolveRequiredPlaceholders(strVal))  ,而這裏的 propertyResolver 即 PropertySourcesPropertyResolver,在 PropertySourcesPlaceholderConfigurer#postProcessBeanFactory 創建出來的。

  由於其爲重寫該方法,所以進入其父類 AbstractPropertyResolver#resolveRequiredPlaceholders:

@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
        if (this.strictHelper == null) {
            this.strictHelper = createPlaceholderHelper(false);
        }
        return doResolvePlaceholders(text, this.strictHelper);
}
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
   return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}

  然後來到了很關鍵的一步  this::getPropertyAsRawString :

@Override
@Nullable
protected String getPropertyAsRawString(String key) {
  return getProperty(key, String.class, false);
}

@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
        if (this.propertySources != null) {
            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;
}

  這裏的  propertySources 也是 PropertySourcesPlaceholderConfigurer#postProcessBeanFactory 創建出來的:

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

  通過 Debug 可以看到相關信息

  然後就會來到 PropertySourcesPlaceholderConfigurer#postProcessBeanFactory 匿名內部類中的 getProperty:

  獲取到了一個 StandardServletEnvironment,然後通過該 Environment  類型進行獲取配置源,這個時候又會進到 PropertySourcesPropertyResolver#getProperty,但是這個時候所看到的 propertySources 屬性則如下圖所示

  這就是所有的本環境下的配置源,然後遍歷這些配置源進行屬性的讀取。因爲我們這裏是直接通過application.properties 配置的 ,所以在 ConfigurationPropertySourcesPropertySource#getProperty 獲取到配置。

  然後回到 DefaultListableBeanFactory#doResolveDependency 會通過  converter.convertIfNecessary 將獲取到的值進行注入到對應的Bean 裏面,完成 @Value 的注入操作. 流程圖如下:

Environment的初始化

  上面的 @Value 代碼的注入流程我們大致是瞭解了,但是這一過程有個 StandardServletEnvironment 是怎麼初始化的 ,也就是spring boot需要解析的外部資源文件的路徑是如何初始化的。在spring boot的啓動流程中,有一個 prepareEnvironment 方法,這個方法就是用來準備Environment這個對象的。

  springApplication.run -> prepareEnvironment

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        // Create and configure the environment
    // 根據上下文,創建一個合適的Environment對象
        ConfigurableEnvironment environment = getOrCreateEnvironment();    
        //配置Environment的propertySource、以及profile
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);  
        // 通知監聽器,加載配置文件
        listeners.environmentPrepared(environment);
        bindToSpringApplication(environment);
        if (!this.isCustomEnvironment) {
            environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                    deduceEnvironmentClass());
        }
        ConfigurationPropertySources.attach(environment);
        return environment;
}

  getOrCreateEnvironment 這個方法,就是根據當前的webApplication類型匹配對應的environment,當前默認的應該就是StandardServletEnvironment ,如果是spring webflflux,則是StandardReactiveWebEnvironment .

private ConfigurableEnvironment getOrCreateEnvironment() {
        if (this.environment != null) {
            return this.environment;
        }
        switch (this.webApplicationType) {
        case SERVLET:
            return new StandardServletEnvironment();
        case REACTIVE:
            return new StandardReactiveWebEnvironment();
        default:
            return new StandardEnvironment();
        }
}

  StandardServletEnvironment 是整個spring boot應用運行環境的實現類,後面所有的關於環境相關的配置操作都是基於這個類,它的類圖如下

  StandardServletEnvironment 的初始化過程,會做一些事情,就是配置一些基本的屬性來源。StandardServletEnvironment 會初始化父類 AbstractEnvironment ,在這個類的構造方法中,會調用一個自定義配置文件的方法

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

  customizePropertySources 這個方法被 StandardServletEnvironment 重寫了,所以會調用StandardServletEnvironment 中的 customizePropertySources 方法。相信大家不難看出,這裏是將幾個不同的配置源封裝成 StubPropertySource 添加到MutablePropertySources 中,調用 addLast 是表示一直往最後的位置添加。

  1. SERVLET_CONFIG_PROPERTY_SOURCE_NAME:servlet的配置信息,也就是在中配置的
  2. SERVLET_CONTEXT_PROPERTY_SOURCE_NAME: 這個是servlet初始化的上下文,也就是以前我們在web.xml中配置的 context-param 。
  3. JNDI_PROPERTY_SOURCE_NAME: 加載jndi.properties配置信息。 
  4. SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME: 系統變量,通過System.setProperty設置的變量,默認可以看到 java.version 、 os.name 等。
  5. SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME: 系統環境變量,也就是我們配置JAVA_HOME的地方。
  這裏我們要明確一點,就是添加PropertySource的目的其實就是要告訴Environment,解析哪些位置的屬性文件進行加載。而在這個添加過程中,所有的添加都是基於 addLast ,也就是最早添加的PropertySource會放在最前面。 systemEnvironment 是在 systemProperties 前面,這點很重要。因爲前面的配置會覆蓋後面的配置,也就是說系統變量中的配置比系統環境變量中的配置優先級更高.
  MutablePropertySources  在上面的代碼中可以看到,所有的外部資源配置都是添加到了一個MutablePropertySources對象中,這個對象封裝了屬性資源的集合。而從 MutablePropertySources 命名來說,Mutable是一個可變的意思,也就是意味着它動態的管理了PropertySource的集合。 
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

  接着,我們來看一下它怎麼用的,找到 AbstractEnvironment 這個類,在這裏定義了 MutablePropertySources。並且把這個MutablePropertySources作爲參數傳遞給了 ConfigurablePropertyResolver 配置解析器中,而這個配置解析器是一個 PropertySourcesPropertyResolver 實例。 

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

  我們來看一下這個類關係圖, AbstractEnvironment 實現了文件解析器ConfigurablePropertyResolver ,而在上面這段代碼中我們把 MutablePropertySources 傳遞到PropertySourcesPropertyResolver 中。這樣就可以讓 AbstractEnvironment 具備文件解析的功能,只是這個功能,委託給了PropertySourcesPropertyResolver來實現。 這跟我們上面的分析是一致的.

  通過上面的代碼,spring構造了一個 StandardServletEnvironment 對象並且初始化了一些需要解析的propertySource。我們繼續來看 configureEnvironment 這個方法,這個方法有兩個作用

  • addConversionService 添加類型轉化的服務,我們知道properties文件中配置的屬性都是String類型的,而轉化爲Java對象之後要根據合適的類型進行轉化,而 ConversionService 是一套通用的轉化方案,這裏把這個轉化服務設置到當前的Environment,很顯然,就是爲Environment配置解析時提供一個類型轉化的解決方案。這塊大家有空可以去研究一下,也是一個值的學習的設計。
  • configurePropertySources 配置Environment中的propertysources,在上面五個屬性來源的基礎上又加了兩個configureProfiles :這個方法就比較容易理解,就是配置當前激活的profifiles,將當前的activeProfifiles設置到enviroment中。這樣就能夠使得我們完成不同環境下配置的獲取問題。
    • 設置 defaultProperties 屬性來源
    • 設置commandLineProperties來源,如果設置了命令行參數,則會加載SimpleCommandLinePropertySource 作爲propertySource

   這個defaultProperties是什麼呢?給大家演示一個東西,就是說我們可以設置一個默認的屬性,如果設置過,則需要加載。否則就不需要。

public static void main(String[] args) {
//        SpringApplication.run(ConfigClientApp.class,args);
//        log.info("服務啓動成功");
        SpringApplication springApplication=new SpringApplication(ConfigClientApp.class); 
        Map<String, Object> pro = new HashMap<>(); 
        pro.put("key", "value"); 
        springApplication.setDefaultProperties(pro); 
        springApplication.run(args);

}

  上述工作完成之後,就是發佈一個environmentPrepared環境準備就緒的通知,具體的時間監聽過程的代碼就不再分析了,我們直接進入到 ConfigFileApplicationListener 這個監聽器,這個監聽器就是用來處理項目配置的。 

SpringBoot 配置的加載過程

  ConfigFileApplicationListener.onApplicationEvent 收到事件之後,會執行如下代碼onApplicationEnvironmentPreparedEvent

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
                // 把自己加進去了,意味着會執行 this.postProcessEnvironment
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }
    }

  最終執行到 ConfigFileApplicationListener.addPropertySources 方法中,這個方法做兩個事情

  1. 添加一個RandomValuePropertySource到Environment的MutablePropertySources中
  2. 加載spring boot中的配置信息,比如application.yml或者application.properties

  這塊的代碼就不繼續深入分析了,小夥伴自己感興趣的深入去看看。load 所做的事情如下:

  1. 獲取默認的配置文件路徑,有4種。private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";
  2. 遍歷所有的路徑,拼裝配置文件名稱。
  3. 再遍歷解析器,選擇yml或者properties解析,將解析結果添加到集合MutablePropertySources當中。

  至此,springBoot中的資源文件加載完畢,解析順序從上到下,所以前面的配置文件會覆蓋後面的配置文件。可以看到 application.properties 的優先級最低,系統變量和環境變量的優先級相對較高。 

  結合上面 @Value 的流程,是不是就串起來了呢? 顯示SpringBoot 啓動的時候將加載相關配置到  Environment ,然後注入的時候去這個裏面拿.

Config Client 配置加載過程

  在Spring Cloud Config中,我們通過@Value註解注入了一個屬性,但是這個屬性不存在於本地配置中,那麼Config是如何將遠程配置信息加載到Environment中的呢?這裏我們需要思考幾個問題
  1. 如何將配置加載到 Environment
  2. 配置變更時,如何控制 Bean 是否需要 create,重新觸發一次 Bean 的初始化,才能將 @Value 註解指定的字段從 Environment 中重新注入。
  3. 配置變更時,如何控制新的配置會更新到 Environment 中,才能保證配置變更時可注入最新的值。
  爲了解決這三個問題,Spring Cloud Config規範中定義了三個核心的接口
  1. PropertySourceLocator:抽象出這個接口,就是讓用戶可定製化的將一些配置加載到Environment。這部分的配置獲取遵循了 Spring Cloud Config 的理念,即希望能從外部儲存介質中來 loacte。
  2. RefreshScope: Spring Cloud 定義這個註解,是擴展了 Spring 原有的 Scope 類型。用來標識當前這個 Bean 是一個refresh 類型的 Scope。其主要作用就是可以控制 Bean 的整個生命週期。
  3. ContextRefresher:抽象出這個 Class,是讓用戶自己按需來刷新上下文(比如當有配置刷新時,希望可以刷新上下文,將最新的配置更新到 Environment,重新創建 Bean 時,就可以從Environment 中注入最新的配置)。 

  從前面的代碼分析過程中我們知道,Environment中所有外部化配置,針對不同類型的配置都會有與之對應的PropertySource,比如(SystemEnvironmentPropertySource、CommandLinePropertySource)。以及PropertySourcesPropertyResolver來進行解析。那Config Client在啓動的時候,必然也會需要從遠程服務器上獲取配置加載到Environment中,這樣才能使得應用程序通過@value進行屬性的注入,而且我們一定可以猜測到的是,這塊的工作一定又和spring中某個機制有關係。 

  在spring boot項目啓動時,有一個prepareContext的方法,它會回調所有實現了ApplicationContextInitializer 的實例,來做一些初始化工作。

public ConfigurableApplicationContext run(String... args) { 
    //省略代碼... 
    prepareContext(context, environment, listeners,applicationArguments, printedBanner); 
    //省略代碼 
    return context; 
}
protected void applyInitializers(ConfigurableApplicationContext context) {
   for (ApplicationContextInitializer initializer : getInitializers()) {
      Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
            ApplicationContextInitializer.class);
      Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
      initializer.initialize(context);
   }
}

  PropertySourceBootstrapConfiguration 實現了 ApplicationContextInitializer 接口,其目的就是在應用程序上下文初始化的時候做一些額外的操作.根據默認的 AnnotationAwareOrderComparator 排序規則對propertySourceLocators數組進行排序獲取運行的環境上下文ConfifigurableEnvironment 遍歷propertySourceLocators時 ,調用 locate 方法,傳入獲取的上下文environment,將source添加到PropertySource的鏈表中,設置source是否爲空的標識標量empty。source不爲空的情況,纔會設置到environment中,返回Environment的可變形式,可進行的操作如addFirst、addLast,移除propertySources中的bootstrapProperties,根據config server覆寫的規則,設置propertySources,處理多個active profiles的配置信息 。

public void initialize(ConfigurableApplicationContext applicationContext) {
        List<PropertySource<?>> composite = new ArrayList<>();
       //對propertySourceLocators數組進行排序,根據默認的AnnotationAwareOrderComparator
        AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
        boolean empty = true;
       //獲取運行的環境上下文 
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        for (PropertySourceLocator locator : this.propertySourceLocators) {
      //回調所有實現PropertySourceLocator接口實例的locate方法,
            Collection<PropertySource<?>> source = locator.locateCollection(environment);
            if (source == null || source.size() == 0) {
                continue;
            }
            List<PropertySource<?>> sourceList = new ArrayList<>();
            for (PropertySource<?> p : source) {
                sourceList.add(new BootstrapPropertySource<>(p));
            }
            logger.info("Located property source: " + sourceList);
            composite.addAll(sourceList);//將source添加到數組 
            empty = false;//表示propertysource不爲空 
        }
      //只有propertysource不爲空的情況,纔會設置到environment中 
        if (!empty) {
            MutablePropertySources propertySources = environment.getPropertySources();
            String logConfig = environment.resolvePlaceholders("${logging.config:}");
            LogFile logFile = LogFile.get(environment);
            for (PropertySource<?> p : environment.getPropertySources()) {
                if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
                    propertySources.remove(p.getName());
                }
            }
            insertPropertySources(propertySources, composite);
            reinitializeLoggingSystem(environment, logConfig, logFile);
            setLogLevels(applicationContext, environment);
            handleIncludedProfiles(environment);
        }
}

  PropertySourceLoader.locateCollection,這個方法會調用子類的locate方法,來獲得一個PropertySource,然後將PropertySource集合返回。接着它會調用 ConfigServicePropertySourceLocator 的locate方法。

  ConfigServicePropertySourceLocator.locate :這個就是Confifig Client的關鍵實現了,它會通過RestTemplate調用一個遠程地址獲得配置信息,getRemoteEnvironment 。然後把這個配置PropertySources,然後將這個信息包裝成一個OriginTrackedMapPropertySource,設置到 Composite 中。 

Config Server獲取配置過程

  服務器端去遠程倉庫加載配置的流程就比較簡單了,核心接口是: EnvironmentRepository ,提供了配置讀取的功能。我們先從請求入口開始看

  EnvironmentController :Spring Cloud Config Server提供了EnvironmentController,這樣通過在瀏覽器訪問即可從git中獲取配置信息

  在這個controller中,提供了很多的映射,最終會調用的是 getEnvironment 。

public Environment getEnvironment(String name, String profiles, String label,
            boolean includeOrigin) {
        name = Environment.normalize(name);
        label = Environment.normalize(label);
        Environment environment = this.repository.findOne(name, profiles, label,
                includeOrigin);
        if (!this.acceptEmpty
                && (environment == null || environment.getPropertySources().isEmpty())) {
            throw new EnvironmentNotFoundException("Profile Not found");
        }
        return environment;
}

  this.repository.findOne ,調用某個repository存儲組件來獲得環境配置信息進行返回。repository是一個 EnvironmentRepository 對象,它有很多實現,其中就包含RedisEnvironmentRepository 、 JdbcEnvironmentRepository 等。默認實現是MultipleJGitEnvironmentRepository ,表示多個不同地址的git數據源。

  MultipleJGitEnvironmentRepository.findOne :MultipleJGitEnvironmentRepository 代理遍歷每個 JGitEnvironmentRepository,JGitEnvironmentRepository 下使用 NativeEnvironmentRepository 代理讀取本地文件。

public Environment findOne(String application, String profile, String label,
            boolean includeOrigin) {
    
       //遍歷所有Git源
for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {
            if (repository.matches(application, profile, label)) {
                for (JGitEnvironmentRepository candidate : getRepositories(repository,
                        application, profile, label)) {
                    try {
                        if (label == null) {
                            label = candidate.getDefaultLabel();
                        }
                        Environment source = candidate.findOne(application, profile,
                                label, includeOrigin);
                        if (source != null) {
                            return source;
                        }
                    }
                    catch (Exception e) {
                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug(
                                    "Cannot load configuration from " + candidate.getUri()
                                            + ", cause: (" + e.getClass().getSimpleName()
                                            + ") " + e.getMessage(),
                                    e);
                        }
                        continue;
                    }
                }
            }
        }
        JGitEnvironmentRepository candidate = getRepository(this, application, profile,
                label);
        if (label == null) {
            label = candidate.getDefaultLabel();
        }
        if (candidate == this) {
            return super.findOne(application, profile, label, includeOrigin);
        }
        return candidate.findOne(application, profile, label, includeOrigin);
}

  AbstractScmEnvironmentRepository.findOne :調用抽象類的findOne方法,主要有兩個核心邏輯

  1. 調用getLocations從GIT遠程倉庫同步到本地
  2. 使用 NativeEnvironmentRepository 委託來讀取本地文件內容 
@Override
    public synchronized Environment findOne(String application, String profile,
            String label, boolean includeOrigin) {
        NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(), new NativeEnvironmentProperties());
        Locations locations = getLocations(application, profile, label);
        delegate.setSearchLocations(locations.getLocations());
        Environment result = delegate.findOne(application, profile, "", includeOrigin);
        result.setVersion(locations.getVersion());
        result.setLabel(label);
        return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(),getUri());
}

   然後調用 getLocations 如下:

@Override
public synchronized Locations getLocations(String application, String profile,
            String label) {
        if (label == null) {
            label = this.defaultLabel;
        }
        String version = refresh(label);
        return new Locations(application, profile, label, version,
                getSearchLocations(getWorkingDirectory(), application, profile, label));
}
// 調用 Git 相關API 進行獲取相關配置
public String refresh(String label) {
        Git git = null;
        try {
            git = createGitClient();
            if (shouldPull(git)) {
                FetchResult fetchStatus = fetch(git, label);
                if (this.deleteUntrackedBranches && fetchStatus != null) {
                    deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(),
                            git);
                }
                // checkout after fetch so we can get any new branches, tags, ect.
                checkout(git, label);
                tryMerge(git, label);
            }
            else {
                // nothing to update so just checkout and merge.
                // Merge because remote branch could have been updated before
                checkout(git, label);
                tryMerge(git, label);
            }
            // always return what is currently HEAD as the version
            return git.getRepository().findRef("HEAD").getObjectId().getName();
        }
  // ........
}

  總體來說,spring-cloud-config 的相關代碼流程並不複雜,主要是我們需要熟悉 @Value   的原理 、Environment 的原理。這樣看起來就很輕鬆了。

實現自定義配置中心 PropertySourceLocator (基於拓展org.springframework.cloud.bootstrap.BootstrapConfiguration)

  看過了 Spring-Cloud-Config 相關源碼,我們怎麼基於同樣的機制來實現我們的配置中心呢? 只需要以下幾個步驟,以JSON 爲例:

  1. 編寫類JsonPropertySourceLocator實現 org.springframework.cloud.bootstrap.config.PropertySourceLocator 接口,實現 locate 方法
  2. 編寫類繼承 org.springframework.core.env.EnumerablePropertySource 
  3. 新建 resource/META-INF/spring.factories 文件拓展 org.springframework.cloud.bootstrap.BootstrapConfiguration
  4. 新建配置源文件  json

1.編寫類JsonPropertySourceLocator實現 org.springframework.cloud.bootstrap.config.PropertySourceLocator 接口,實現 locate 方法

@Component
public class JsonPropertySourceLocator implements PropertySourceLocator{

    private final static String DEFAULT_LOCATION="classpath:gupao.json";

    private final ResourceLoader resourceLoader=new DefaultResourceLoader(getClass().getClassLoader());

    @Override
    public PropertySource<?> locate(Environment environment) {
        WuzzDefineJsonPropertySource jsonPropertySource=
                new WuzzDefineJsonPropertySource("jsonPropertyConfig",mapPropertySource());
        return jsonPropertySource;
    }

    private Map<String,Object> mapPropertySource(){
        //訪問遠程配置?http接口。
        Resource resource=this.resourceLoader.getResource(DEFAULT_LOCATION);
        if(resource==null){
            return null;
        }
        Map<String,Object> result=new HashMap<>();
        JsonParser jsonParser= JsonParserFactory.getJsonParser();
        Map<String,Object> fileMap=jsonParser.parseMap(readFile(resource));
        processorMap("",result,fileMap);
        return result;
    }

    private void processorMap(String prefix,Map<String,Object> result,Map<String,Object> fileMap){
        if(prefix.length()>0){
            prefix+=".";
        }
        for (Map.Entry<String,Object> entrySet:fileMap.entrySet()){
            if(entrySet.getValue() instanceof Map){
                processorMap(prefix+entrySet.getKey(),result,(Map<String,Object>)entrySet.getValue());
            }else{
                result.put(prefix+entrySet.getKey(),entrySet.getValue());
            }
        }
    }

    /**
     * JSON格式
     * @param resource
     * @return
     */
    private String readFile(Resource resource){
        FileInputStream fileInputStream=null;
        try{
            fileInputStream=new FileInputStream(resource.getFile());
            byte[] readByte=new byte[(int)resource.getFile().length()]; //TODO 錯誤演示
            fileInputStream.read(readByte);
            return new String(readByte,"UTF-8");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(fileInputStream!=null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

2.編寫類繼承 org.springframework.core.env.EnumerablePropertySource 

public class WuzzDefineJsonPropertySource extends EnumerablePropertySource<Map<String, Object>> {

    public WuzzDefineJsonPropertySource(String name, Map<String, Object> source) {
        super(name, source);
    }

    @Override
    public String[] getPropertyNames() {
        return StringUtils.toStringArray(this.source.keySet());
    }

    @Override
    public Object getProperty(String name) {
        return this.source.get(name);
    }
}

3.新建 resource/META-INF/spring.factories 文件拓展 org.springframework.cloud.bootstrap.BootstrapConfiguration

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  com.wuzz.demo.sourceloader.JsonPropertySourceLocator

4.新建配置源文件  json

{
  "custom":{
    "property":{
      "hello": "hello Wuzz"
    }
  }
}

5.測試類

@Value("${custom.property.hello}")
    private String config;

@GetMapping("/config")
public String config(){
    return config;
}

  啓動的時候可以斷點進去看看是否被加載:

  訪問配置好的路徑 :

  更多的信息請參考官網

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章