Spring Cloud Config原碼篇(十)

童話述說我的結局發表於2021-01-02

上篇中說到通過@Value註解獲取配置中心的內容進行注入,要想了解這個就要知道spring Environment原理,關於這原理我看了下網上分析的文章:https://blog.csdn.net/topdeveloperr/article/details/88063828

一、Environment的初始化

首先來看第一部分,就是spring boot需要解析的外部資原始檔的路徑是如何初始化的。在spring boot的啟動流程中,有一個 prepareEnvironment 方法,這個方法就是用來準備Environment這個物件的。找他的入中是從springApplication.run 開始的

 

 

 進入ConfigurableEnvironment類 這個方法主要就是建立和配置spring容器的環境資訊

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners
listeners,ApplicationArguments
applicationArguments) {
  // 根據上下文,建立一個合適的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 webflux,則是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 ,在這個類的構造方法中,會呼叫一個自定義配置檔案的方法,這個是spring中比較常見的實現手法,前面在看ribbon、eureka中都有看到。

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
  public AbstractEnvironment() {
customizePropertySources(this.propertySources);
}
}

customizePropertySources 這個方法被 StandardServletEnvironment 重寫了,所以會呼叫StandardServletEnvironment 中的 customizePropertySources 方法。不難看出,這裡是將幾個不同的配置源封裝成 StubPropertySource 新增到
MutablePropertySources 中,呼叫 addLast 是表示一直往最後的位置新增。SERVLET_CONFIG_PROPERTY_SOURCE_NAME:servlet的配置資訊,也就是在中配置的SERVLET_CONTEXT_PROPERTY_SOURCE_NAME: 這個是servlet初始化的上下文,也就是以前我們在web.xml中配置的 context-param 。JNDI_PROPERTY_SOURCE_NAME: 載入jndi.properties配置資訊。

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

繼續呼叫父類,也就是 StandardEnvironment 類中的 customizePropertySources 方法。SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME: 系統變數,通過System.setProperty設定的變數,預設可以看到 java.version 、 os.name 等。SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME: 系統環境變數,也就是我們配置JAVA_HOME的地方。

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

這裡要明確一點,就是新增PropertySource的目的其實就是要告訴Environment,解析哪些位置的屬性檔案進行載入。而在這個新增過程中,所有的新增都是基於 addLast ,也就是最早新增的PropertySource會放在最前面。 systemEnvironment 是在 systemProperties 前面,這點很重要。因為前面的配置會覆蓋後面的配置,也就是說系統變數中的配置比系統環境變數中的配置優先順序更高

二、MutablePropertySources

在上面的程式碼中可以看到,所有的外部資源配置都是新增到了一個MutablePropertySources物件中,這個物件封裝了屬性資源的集合。而從 MutablePropertySources 命名來說,Mutable是一個可變的意思,也就是意味著它動態的管理了PropertySource的集合。

public class MutablePropertySources implements PropertySources {
private final List<PropertySource<?>> propertySourceList = new
CopyOnWriteArrayList<>();
}

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

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
  private final MutablePropertySources propertySources = new
MutablePropertySources();
private final ConfigurablePropertyResolver propertyResolver =
new PropertySourcesPropertyResolver(this.propertySources);
}

通過下面類圖可以發現AbstractEnvironment 實現了檔案解析器ConfigurablePropertyResolver ,而在上面這段程式碼中我們把 MutablePropertySources 傳遞到PropertySourcesPropertyResolver 中。這樣就可以讓 AbstractEnvironment 具備檔案解析的功能,只是這個功能,委託給了PropertySourcesPropertyResolver來實現。

 

 

 

通過上面的程式碼,spring構造了一個 StandardServletEnvironment 物件並且初始化了一些需要解析的propertySource,現在回退到SpringApplication類的prepareEnvironment方法,我們繼續來看  configureEnvironment 這個方法,這個方法有兩個作用

  • addConversionService 新增型別轉化的服務,我們知道properties檔案中配置的屬性都是String型別的,而轉化為Java物件之後要根據合適的型別進行轉化,而 ConversionService 是一套通用的轉化方案,這裡把這個轉化服務設定到當前的Environment,很顯然,就是為Environment配置解析時提供一個型別轉化的解決方案。
  • configurePropertySources 配置Environment中的propertysources,
  • configureProfiles 配置profiles
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
  if (this.addConversionService) {
    ConversionService conversionService =
ApplicationConversionService.getSharedInstance();
    environment.setConversionService((ConfigurableConversionService)
conversionService);
 }
  configurePropertySources(environment, args);
  configureProfiles(environment, args);
}

configurePropertySources方法中

  • 設定 defaultProperties 屬性來源
  • 設定commandLineProperties來源,如果設定了命令列引數,則會載入SimpleCommandLinePropertySource 作為propertySource
protected void configurePropertySources(ConfigurableEnvironment environment,
String[] args) {
  MutablePropertySources sources = environment.getPropertySources();
  if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
    sources.addLast(new MapPropertySource("defaultProperties",
this.defaultProperties));
 }
  if (this.addCommandLineProperties && args.length > 0) {
    String name =
CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
    if (sources.contains(name)) {
      PropertySource<?> source = sources.get(name);
      CompositePropertySource composite = new
CompositePropertySource(name);
      composite.addPropertySource(
        new
SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
      composite.addPropertySource(source);
      sources.replace(name, composite);
   }
    else {
      sources.addFirst(new SimpleCommandLinePropertySource(args));
   }
 }
}

到目前為止,還是在初始化外部化配置的資料來源。接著進入configureProfiles方法,這個方法就比較容易理解,就是配置當前啟用的profiles,將當前的activeProfiles設定到enviroment中。這樣就能夠使得我們完成不同環境下配置的獲取問題。

protected void configureProfiles(ConfigurableEnvironment environment, String[]
args) {
  Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
  profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
  environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

經過上面的操作spring的配置資訊都已載入完成,但有一個很重要的配置還沒有載入,那就是springboot的配置資訊,現在回退到SpringApplication類的prepareEnvironment類

 

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        // Create and configure the environment
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);
//springboot的釋出事件 listeners.environmentPrepared(environment); bindToSpringApplication(environment);
if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; }
    void environmentPrepared(ConfigurableEnvironment environment) {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.environmentPrepared(environment);
        }
    }

選擇EventPublishingRunListener類的environmentPrepared,進入事件的監聽

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        this.initialMulticaster
                .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
    }

選擇multicastEvent,進入SimpleApplicationEventMulticaster類的multicastEvent方法,這個方法是多緯度的監聽

    @Override
    public void multicastEvent(ApplicationEvent event) {
        multicastEvent(event, resolveDefaultEventType(event));
    }
    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        Executor executor = getTaskExecutor();
//得到結果集
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) { if (executor != null) {
//反射呼叫 executor.execute(()
-> invokeListener(listener, event)); } else { invokeListener(listener, event); } } }

上面事件有反射呼叫就一定會有一個監聽,如果有興趣可以Debugger會發現這個getApplicationListeners的事件監聽中有一個叫ConfigFileApplicationListener,這個監聽器就是用來處理專案配置的,進入ConfigFileApplicationListener類會看到一個onApplicationEvent方法

 

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
//環境的準備事件 onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)
event); } if (event instanceof ApplicationPreparedEvent) {
//Appliaction的準備事件 onApplicationPreparedEvent(
event); } }

如果有人在我之前說的要debugger的地方debugger的話會發現,現在釋出的事件是一個ApplicationEnvironmentPreparedEvent事件,進入onApplicationEnvironmentPreparedEvent事件中

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }
    }

 

 

 最終執行到 ConfigFileApplicationListener.addPropertySources 方法中

 

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

這個方法做兩個事情

  • 新增一個RandomValuePropertySource到Environment的MutablePropertySources中
  • 載入spring boot中的配置資訊,比如application.yml或者application.properties
    protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        new Loader(environment, resourceLoader).load();
    }

進入Load類load方法這個方法比較複雜,總的來說,就是載入所有可能的profiles首先我們來看,這裡實際上是呼叫了 FilteredPropertySource.apply 方法。然後傳遞了一個lambda表示式到apply方法中。

  • 其中apply方法的主要邏輯是,判斷如果當前如果有預設配置,則將預設配置員增加一個FilteredPropertySource 。
  • 執行匿名內部類
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<>();
                  initializeProfiles();
                  while (!this.profiles.isEmpty()) {
                    Profile profile = this.profiles.poll();
                    if (isDefaultProfile(profile)) {
                     
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);
                });
}

 

下面這個lambda表示式的主要邏輯是

  • 呼叫initializeProfiles初始化預設的Profile,沒有設定的話就用預設,初始化之後儲存到 privateDeque<Profile> profiles; 中,它是一個LIFO佇列。 因為 profiles 採用了 LIFO 佇列,後進先出。所以會先載入profile為null的配置檔案 ,也就是匹配 application.properties、application.yml 。
  • 如果profiles不為空,則迴圈遍歷每一個profiles,呼叫 load方法進行載入。


 

(defaultProperties) -> {
  // 未處理的資料集合
  this.profiles = new LinkedList<>();
  // 已處理的資料集合
  this.processedProfiles = new LinkedList<>();
  this.activatedProfiles = false;
  this.loaded = new LinkedHashMap<>();
  //載入存在已經啟用的 profiles
  initializeProfiles();
  while (!this.profiles.isEmpty()) {//遍歷所有profiles
    Profile profile = this.profiles.poll();
    if (isDefaultProfile(profile)) {
      addProfileToEnvironment(profile.getName());
   }
    // 確定搜尋範圍,獲取對應的配置檔名,並使用相應載入器載入
    load(profile, this::getPositiveProfileFilter,
      addToLoaded(MutablePropertySources::addLast, false));
    // 將處理完的 profile新增到 processedProfiles列表當中,表示已經處理完成
    this.processedProfiles.add(profile);
 }
  load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
  addLoadedPropertySources();
  applyActiveProfiles(defaultProperties);// 更新 activeProfiles列表

點選上面的 initializeProfiles,進入Load類initializeProfiles方法,該方法的作用是載入存在已經啟用的 profiles

 

private void initializeProfiles() {
  // The default profile for these purposes is represented as null. We add it
  // first so that it is processed first and has lowest priority.
  this.profiles.add(null);
  Binder binder = Binder.get(this.environment);
  //判斷當前環境是否配置 spring.profiles.active屬性
  Set<Profile> activatedViaProperty = getProfiles(binder,
ACTIVE_PROFILES_PROPERTY);
  //判斷當前環境是否配置 spring.profiles.include屬性
  Set<Profile> includedViaProperty = getProfiles(binder,
INCLUDE_PROFILES_PROPERTY);
  //如果沒有特別指定的話,就是 application.properties 和 application-
default.properties配置
  List<Profile> otherActiveProfiles =
getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
  this.profiles.addAll(otherActiveProfiles);
  // Any pre-existing active profiles set via property sources (e.g.
  // System properties) take precedence over those added in config files.
  this.profiles.addAll(includedViaProperty);
  addActiveProfiles(activatedViaProperty);
  // 如果 profiles集仍然為null,即沒有指定,就會建立預設的profile
  if (this.profiles.size() == 1) { // only has null profile
    for (String defaultProfileName : this.environment.getDefaultProfiles())
{
      Profile defaultProfile = new Profile(defaultProfileName, true);
      this.profiles.add(defaultProfile);
   }
 }
}

這個看明白後返回上一層點load進入Load類的load方法,繼續跟進load方法,通過 getSearchLoacations 進行搜尋,並且進行迭代。

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

getSearchLocations的主要功能,就是獲取需要遍歷的目標路徑,預設情況下,會去DEFAULT_SEARCH_LOCATIONS中查詢,也就是

private Set<String> getSearchLocations() {
  Set<String> locations =
getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
  if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
    locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY));
 }
  else {
    locations.addAll(
      asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
 }
  return locations;
}

拿到路徑地址之後,再拼接對應路徑,選擇合適的yml或者properties解析器進行解析。

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

整理流程如下:
1)獲取預設的配置檔案路徑,有4種。

2)遍歷所有的路徑,拼裝配置檔名稱。

3)再遍歷解析器,選擇yml或者properties解析,將解析結果新增到集合MutablePropertySources當中。

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

三、config中的environment

在Spring Cloud Config中,通過@Value註解注入了一個屬性,但是這個屬性不存在於本地配置中,那麼Config是如何將遠端配置資訊載入到Environment中的呢?這裡需要思考幾個問題

  • 如何將配置載入到 Environment
  • 配置變更時,如何控制 Bean 是否需要 create,重新觸發一次 Bean 的初始化,才能將 @Value 註解指定的欄位從 Environment 中重新注入。
  • 配置變更時,如何控制新的配置會更新到 Environment 中,才能保證配置變更時可注入最新的值。

為了解決這三個問題,Spring Cloud Config規範中定義了三個核心的介面

  • PropertySourceLocator:抽象出這個介面,就是讓使用者可定製化的將一些配置載入到Environment。這部分的配置獲取遵循了 Spring Cloud Config 的理念,即希望能從外部儲存介質中來 loacte。
  • RefreshScope: Spring Cloud 定義這個註解,是擴充套件了 Spring 原有的 Scope 型別。用來標識當前這個 Bean 是一個refresh 型別的 Scope。其主要作用就是可以控制 Bean 的整個生命週期。
  • ContextRefresher:抽象出這個 Class,是讓使用者自己按需來重新整理上下文(比如當有配置重新整理時,希望可以重新整理上下文,將最新的配置更新到 Environment,重新建立 Bean 時,就可以從Environment 中注入最新的配置)。

下面就來了解下Environment是如何在啟動過程中從遠端伺服器上載入配置的

3.1、Config Client 配置載入過程

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

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

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            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); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }

 

 前面說過prepareContext的方法,它會回撥所有實現了ApplicationContextInitializer 的例項然而PropertySourceBootstrapConfiguration 實現了 ApplicationContextInitializer 介面,其目的就是在應用程式上下文初始化的時候做一些額外的操作.根據預設的 AnnotationAwareOrderComparator 排序規則對propertySourceLocators陣列進行排序,獲取執行的環境上下文ConfigurableEnvironment,遍歷propertySourceLocators時

  • 呼叫 locate 方法,傳入獲取的上下文environment
  • 將source新增到PropertySource的連結串列中
  • 設定source是否為空的標識標量empty

source不為空的情況,才會設定到environment中

  • 返回Environment的可變形式,可進行的操作如addFirst、addLast
  • 移除propertySources中的bootstrapProperties
  • 根據config server覆寫的規則,設定propertySources
  • 處理多個active profiles的配置資訊
@Override
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);
 }
}

選擇locateCollection進入PropertySourceLoader類的locateCollection方法;這個方法會呼叫子類的locate方法,來獲得一個PropertySource,然後將PropertySource集合返回。接著它會呼叫 ConfigServicePropertySourceLocator 的locate方法。

static Collection<PropertySource<?>> locateCollection(PropertySourceLocator
locator,
                           Environment environment) {
  PropertySource<?> propertySource = locator.locate(environment);
  if (propertySource == null) {
    return Collections.emptyList();
 }
  if (CompositePropertySource.class.isInstance(propertySource)) {
    Collection<PropertySource<?>> sources = ((CompositePropertySource)
propertySource)
     .getPropertySources();
    List<PropertySource<?>> filteredSources = new ArrayList<>();
    for (PropertySource<?> p : sources) {
      if (p != null) {
        filteredSources.add(p);
     }
   }
    return filteredSources;
 }
  else {
    return Arrays.asList(propertySource);
 }
}

進入ConfigServicePropertySourceLocator類的locate方法這個就是Config Client的關鍵實現了,它會通過RestTemplate呼叫一個遠端地址獲得配置資訊,getRemoteEnvironment 。然後把這個配置PropertySources,然後將這個資訊包裝成一個OriginTrackedMapPropertySource,設定到 Composite 中。

public org.springframework.core.env.PropertySource<?> locate(
  org.springframework.core.env.Environment environment) {
  ConfigClientProperties properties =
this.defaultProperties.override(environment);
  CompositePropertySource composite = new
OriginTrackedCompositePropertySource(
    "configService");
  RestTemplate restTemplate = this.restTemplate == null
    ? getSecureRestTemplate(properties) : this.restTemplate;
  Exception error = null;
  String errorBody = null;
  try {
    String[] labels = new String[] { "" };
    if (StringUtils.hasText(properties.getLabel())) {
      labels = StringUtils
       .commaDelimitedListToStringArray(properties.getLabel());
   }
    String state = ConfigClientStateHolder.getState();
    // Try all the labels until one works
    for (String label : labels) {
      Environment result = getRemoteEnvironment(restTemplate, properties,
                           label.trim(), state);
if (result != null) {
        log(result);
        // result.getPropertySources() can be null if using xml
        if (result.getPropertySources() != null) {
          for (PropertySource source : result.getPropertySources()) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map =
translateOrigins(source.getName(),
                                 (Map<String,
Object>) source.getSource());
            composite.addPropertySource(
              new OriginTrackedMapPropertySource(source.getName(),
                               map));
         }
       }
        if (StringUtils.hasText(result.getState())
          || StringUtils.hasText(result.getVersion())) {
          HashMap<String, Object> map = new HashMap<>();
          putValue(map, "config.client.state", result.getState());
          putValue(map, "config.client.version", result.getVersion());
          composite.addFirstPropertySource(
            new MapPropertySource("configClient", map));
       }
        return composite;
     }
   }
    errorBody = String.format("None of labels %s found",
Arrays.toString(labels));
 }
  catch (HttpServerErrorException e) {
    error = e;
    if (MediaType.APPLICATION_JSON
     .includes(e.getResponseHeaders().getContentType())) {
      errorBody = e.getResponseBodyAsString();
   }
 }
  catch (Exception e) {
    error = e;
 }
  if (properties.isFailFast()) {
    throw new IllegalStateException(
      "Could not locate PropertySource and the fail fast property is set,
failing"
      + (errorBody == null ? "" : ": " + errorBody),
      error);
 }
  logger.warn("Could not locate PropertySource: "
        + (error != null ? error.getMessage() : errorBody));
  return null;
}

四、Config Server獲取配置過程

伺服器端去遠端倉庫載入配置的流程就比較簡單了,核心介面是: EnvironmentRepository ,提供了配置讀取的功能。先從請求入口開始看;pring 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類中 代理遍歷每個 JGitEnvironmentRepository,JGitEnvironmentRepository 下使用 NativeEnvironmentRepository 代理讀取本地檔案。

@Override
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方法,主要有兩個核心邏輯

  • 呼叫getLocations從GIT遠端倉庫同步到本地
  • 使用 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());
}

 

 

 



 










 

 

相關文章