上篇中说到通过@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());
}

Spring Cloud Config原码篇(十)的更多相关文章

  1. SpringCloud教程 | 第七篇: 高可用的分布式配置中心(Spring Cloud Config)(Finchley版本)

    上一篇文章讲述了一个服务如何从配置中心读取文件,配置中心如何从远程git读取配置文件,当服务实例很多时,都从配置中心读取文件,这时可以考虑将配置中心做成一个微服务,将其集群化,从而达到高可用,架构图如 ...

  2. 【SpringCloud】第七篇: 高可用的分布式配置中心(Spring Cloud Config)

    前言: 必需学会SpringBoot基础知识 简介: spring cloud 为开发人员提供了快速构建分布式系统的一些工具,包括配置管理.服务发现.断路器.路由.微代理.事件总线.全局锁.决策竞选. ...

  3. 跟我学SpringCloud | 第七篇:Spring Cloud Config 配置中心高可用和refresh

    SpringCloud系列教程 | 第七篇:Spring Cloud Config 配置中心高可用和refresh Springboot: 2.1.6.RELEASE SpringCloud: Gre ...

  4. 史上最简单的SpringCloud教程 | 第七篇: 高可用的分布式配置中心(Spring Cloud Config)

    上一篇文章讲述了一个服务如何从配置中心读取文件,配置中心如何从远程git读取配置文件,当服务实例很多时,都从配置中心读取文件,这时可以考虑将配置中心做成一个微服务,将其集群化,从而达到高可用,架构图如 ...

  5. 史上最简单的SpringCloud教程 | 第六篇: 分布式配置中心(Spring Cloud Config)

    一.简介 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件. 在Spring Cloud中,有分布式配置中心组件spring cloud confi ...

  6. 第六篇: 分布式配置中心(Spring Cloud Config)

    一.简介 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件. 在Spring Cloud中,有分布式配置中心组件spring cloud confi ...

  7. spring cloud连载第二篇之Spring Cloud Config

    Spring Cloud Config Spring Cloud Config为分布式服务提供了服务侧和客户侧的外部配置支持.通过Spring Cloud Config你可以有一个统一的地方来管理所有 ...

  8. SpringCloud教程 | 第六篇: 分布式配置中心(Spring Cloud Config)(Finchley版本)

    在上一篇文章讲述zuul的时候,已经提到过,使用配置服务来保存各个服务的配置文件.它就是Spring Cloud Config. 一.简介 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管 ...

  9. 【SpringCloud】第六篇: 分布式配置中心(Spring Cloud Config)

    前言: 必需学会SpringBoot基础知识 简介: spring cloud 为开发人员提供了快速构建分布式系统的一些工具,包括配置管理.服务发现.断路器.路由.微代理.事件总线.全局锁.决策竞选. ...

随机推荐

  1. OpenCV-Python图像转换为PyQt图像的变形及花屏无法正常显示问题研究

    ☞ ░ 前往老猿Python博文目录 ░ 一.引言 在<PyQt转换显示Python-OpenCV图像实现图形化界面的视频播放>介绍了实现在OpenCV和PyQt之间转换并传递图像实现在P ...

  2. moviepy音视频剪辑:使用fl_time进行时间特效处理报错OSError: Error in file xxxx, Accessing time

    ☞ ░ 前往老猿Python博文目录 ░ 老猿在使用moviepy音视频剪辑的fl_time进行时间特效处理时,系统报错: OSError: Error in file F:\video\WinBas ...

  3. PyQt(Python+Qt)学习随笔:QToolBox工具箱currentItem对应的index、text、name、icon、ToolTip属性

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 在Designer中,toolBox主要有如下属性: 可以看到,toolBox的属性主要是与当前项相 ...

  4. 第15.19节 PyQt(Python+Qt)入门学习:自定义信号与槽连接

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.引言 本文利用中介绍了PyQt中的信号和槽机制,除了使用PyQt组件的已有信号外,PyQt和Qt ...

  5. JQuery获取父,子,兄弟节点

    jQuery.parent(expr) // 查找父节点,可以传入expr进行过滤,比如$("span").parent()或者$("span").parent ...

  6. SPFA算法优化

    前言 \(SPFA\) 通常在稀疏图中运行效率高于 \(Dijkstra\) ,但是也容易被卡. 普通的 \(SPFA\) 时间复杂度为 \(O(km)\) ,其中 \(k\) 是一条边松弛其端点点的 ...

  7. 【题解】P1852 跳跳棋

    link 题意 跳跳棋是在一条数轴上进行的.棋子只能摆在整点上.每个点不能摆超过一个棋子.棋盘上有3颗棋子,分别在 \(a,b,c\) 这三个位置.我们要通过最少的跳动把他们的位置移动成 \(x,y, ...

  8. NOIP2017 D1T3 逛公园

    发现 \(K\) 很小,不妨设置一个 \(O(NK)\) 的 \(DP\). 发现可行的最短路必须满足是 \(d <= dis <= d + K\). 由逆向思维,则是从某点出发,可以消耗 ...

  9. SpringBoot添加多数据源mysql和oracle

    项目结构 多数据源配置文件 MultiDataSourceConfig.java SqlSessionTemplate1.java SqlSessionTemplate2.java package c ...

  10. JAVA死锁的检测流程

    步骤一. 查询检测的进程 1.首先查看系统资源占用信息,TOP看一下 发现正在运行的JAVA项目CPU占用率很高,百分之360左右了,那么问题一定出在这个程序中 2 .也可以通过名称查询进程pid 步 ...