一、基本原理

我们知道SpringMVC Web容器启动的时候会读取配置文件来进行Envirement的初始化设值,再根据Envirement中的信息去进行Web容器的初始化启动。spring_cloud_config就是对这里的拓展,其统一管理这些配置文件,然后其他的服务在启动的时候通过http请求去config_server读取本服务的配置环境。在这里,config_server就根据请求的相关信息如服务名称&环境信息&分支去读取其管理的配置文件,再将配置文件中的信息封装为一个Envirement返回给调用方。这就是config_server主要处理的逻辑,下面我们就来具体分析这个否则。

二、远程调用介绍

1、调用方

1)、PropertySourceBootstrapConfiguration

调用方在去config_server读取Envirement的发起方法是PropertySourceBootstrapConfiguration

@Configuration@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)public class PropertySourceBootstrapConfiguration implements  ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {复制代码

可以看到其是实现的ApplicationContextInitializer接口。下面我们来看其的方法调用:

@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {
   CompositePropertySource composite = new CompositePropertySource(
         BOOTSTRAP_PROPERTY_SOURCE_NAME);
   AnnotationAwareOrderComparator.sort(this.propertySourceLocators);   boolean empty = true;
   ConfigurableEnvironment environment = applicationContext.getEnvironment();   for (PropertySourceLocator locator : this.propertySourceLocators) {
      PropertySource<?> source = null;
      source = locator.locate(environment);      if (source == null) {         continue;
      }
      ......
   }   if (!empty) {
      MutablePropertySources propertySources = environment.getPropertySources();
      ......
      insertPropertySources(propertySources, composite);
      ......
   }
}复制代码

这里的逻辑主要是有两个:通过PropertySourceLocator去远程获取对应的PropertySource(其是从远程获取的Envirement提取的),然后再通过insertPropertySources方法将其添加到本服务的Envirement中MutablePropertySources propertySources = environment.getPropertySources()。

private void insertPropertySources(MutablePropertySources propertySources,
      CompositePropertySource composite) {
   MutablePropertySources incoming = new MutablePropertySources();
   incoming.addFirst(composite);
   PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
   Binder.get(environment(incoming)).bind("spring.cloud.config", Bindable.ofInstance(remoteProperties));   if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
         && remoteProperties.isOverrideSystemProperties())) {
      propertySources.addFirst(composite);      return;
   }   if (remoteProperties.isOverrideNone()) {
      propertySources.addLast(composite);      return;
   }   if (propertySources
         .contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {      if (!remoteProperties.isOverrideSystemProperties()) {
         propertySources.addAfter(
               StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
               composite);
      }      else {
         propertySources.addBefore(
               StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
               composite);
      }
   }   else {
      propertySources.addLast(composite);
   }
}复制代码

可以看到这里还有决定是添加在addLast或addBefore。

2)、ConfigServicePropertySourceLocator

@Override@Retryable(interceptor = "configServerRetryInterceptor")public org.springframework.core.env.PropertySource<?> locate(
      org.springframework.core.env.Environment environment) {
      ......      // Try all the labels until one works  for (String label : labels) {
         Environment result = getRemoteEnvironment(restTemplate, properties,
               label.trim(), state);         if (result != null) {
              ......               for (PropertySource source : result.getPropertySources()) {                  @SuppressWarnings("unchecked")  Map<String, Object> map = (Map<String, Object>) source
                        .getSource();
                  composite.addPropertySource(new MapPropertySource(source.getName(), map));
               }
            }
            ......return composite;
         }
      }
   }
      ......
}复制代码

上面的this.propertySourceLocators其中就是ConfigServicePropertySourceLocator,这里就是通过getRemoteEnvironment方法去获取config_server中的信息,在获取其的result.getPropertySources()信息。

private Environment getRemoteEnvironment(RestTemplate restTemplate,
      ConfigClientProperties properties, String label, String state) {
   String path = "/{name}/{profile}";
   String name = properties.getName();
   String profile = properties.getProfile();
   String token = properties.getToken();   int noOfUrls = properties.getUri().length;   if (noOfUrls > 1) {
      logger.info("Multiple Config Server Urls found listed.");
   }
   Object[] args = new String[] { name, profile };   if (StringUtils.hasText(label)) {      if (label.contains("/")) {
         label = label.replace("/", "(_)");
      }
      args = new String[] { name, profile, label };
      path = path + "/{label}";
   }
   ResponseEntity<Environment> response = null;   for (int i = 0; i < noOfUrls; i++) {
      Credentials credentials = properties.getCredentials(i);
      String uri = credentials.getUri();
      String username = credentials.getUsername();
      String password = credentials.getPassword();
      logger.info("Fetching config from server at : " + uri);      try {
         HttpHeaders headers = new HttpHeaders();
         addAuthorizationToken(properties, headers, username, password);         if (StringUtils.hasText(token)) {
            headers.add(TOKEN_HEADER, token);
         }         if (StringUtils.hasText(state) && properties.isSendState()) {
            headers.add(STATE_HEADER, state);
         }         final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
         response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
               Environment.class, args);
         ......
      Environment result = response.getBody();      return result;
   }   return null;
}复制代码

这里就是远程调用config_server的方法。

2、被调用方(config_server)

1)、配置文件配置的信息

spring:
  cloud:
    config:
      #对应分支
#      label: master
      profile: dev
      name: config-client复制代码

这里的spring.cloud.config.name - 对应的应用名称、spring.cloud.config.profile` - 对应的环境名称、spring.cloud.config.label` - 对应的分支名称(注释了)。

1)、EnvironmentController

@RestController@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")public class EnvironmentController {复制代码

config_server就是用这个类去接收调用方获取Envirement请求的。这个类是有很多接收的方法:

@RequestMapping("/{name}/{profiles:.*[^-].*}")public Environment defaultLabel(@PathVariable String name,      @PathVariable String profiles) {   return labelled(name, profiles, null);
}@RequestMapping("/{name}/{profiles}/{label:.*}")public Environment labelled(@PathVariable String name, @PathVariable String profiles,      @PathVariable String label) {
   ......   return environment;
}@RequestMapping("/{name}-{profiles}.properties")public ResponseEntity<String> properties(@PathVariable String name,      @PathVariable String profiles,      @RequestParam(defaultValue = "true") boolean resolvePlaceholders)  throws IOException {   return labelledProperties(name, profiles, null, resolvePlaceholders);
}@RequestMapping("/{label}/{name}-{profiles}.properties")public ResponseEntity<String> labelledProperties(@PathVariable String name,      @PathVariable String profiles, @PathVariable String label,      @RequestParam(defaultValue = "true") boolean resolvePlaceholders)  throws IOException {
   ......   return getSuccess(propertiesString);
}@RequestMapping("{name}-{profiles}.json")public ResponseEntity<String> jsonProperties(@PathVariable String name,      @PathVariable String profiles,      @RequestParam(defaultValue = "true") boolean resolvePlaceholders)  throws Exception {   return labelledJsonProperties(name, profiles, null, resolvePlaceholders);
}@RequestMapping("/{label}/{name}-{profiles}.json")public ResponseEntity<String> labelledJsonProperties(@PathVariable String name,      @PathVariable String profiles, @PathVariable String label,      @RequestParam(defaultValue = "true") boolean resolvePlaceholders)  throws Exception {
   ......   return getSuccess(json, MediaType.APPLICATION_JSON);
}@RequestMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" })public ResponseEntity<String> yaml(@PathVariable String name,      @PathVariable String profiles,      @RequestParam(defaultValue = "true") boolean resolvePlaceholders)  throws Exception {   return labelledYaml(name, profiles, null, resolvePlaceholders);
}@RequestMapping({ "/{label}/{name}-{profiles}.yml",
      "/{label}/{name}-{profiles}.yaml" })public ResponseEntity<String> labelledYaml(@PathVariable String name,      @PathVariable String profiles, @PathVariable String label,      @RequestParam(defaultValue = "true") boolean resolvePlaceholders)  throws Exception {
   validateProfiles(profiles);
   ......   return getSuccess(yaml);
}复制代码

我们本次的请求是会进入第一个@RequestMapping("/{name}/{profiles:.*[^-].*}"),其调用的是第二个方法,这个方法就是我们本次的主要分析内容了。

三、config_server读取解析返回Envirement

下面我们就来具体分析下labelled(@PathVariable String name, @PathVariable String profiles,@PathVariable String label)方法。

1、labelled(...)方法  (EnvironmentController)

@RequestMapping("/{name}/{profiles}/{label:.*}")public Environment labelled(@PathVariable String name, @PathVariable String profiles,      @PathVariable String label) {   if (name != null && name.contains("(_)")) {      // "(_)" is uncommon in a git repo name, but "/" cannot be matched  // by Spring MVC  name = name.replace("(_)", "/");
   }   if (label != null && label.contains("(_)")) {      // "(_)" is uncommon in a git branch name, but "/" cannot be matched  // by Spring MVC  label = label.replace("(_)", "/");
   }
   Environment environment = this.repository.findOne(name, profiles, label);   if(!acceptEmpty && (environment == null || environment.getPropertySources().isEmpty())){       throw new EnvironmentNotFoundException("Profile Not found");
   }   return environment;
}复制代码

1)、(_)替换符处理

可以看到这里有一种操作将(_)转换为/,这里其实就一种转换,因为如果将/放在请求上是代表的另一个请求,所以这里是有一个替换,这个替换时请求获取的时候可内容请求本身还有一层路径:

@Testpublic void applicationPlaceholderWithSlashForBinary() throws Exception {   this.environmentRepository.setSearchLocations("classpath:/test/{application}");   byte[] resource = this.controller.binary("dev(_)spam", "bar", "", "foo.txt");
   assertEquals("foo: dev_bar/spam", new String(resource));
}复制代码

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig

例如这里其表示要获取的地址是dev/spam,可以看到也可以在搜索地址classpath:/test/{application}中写{application}这种替换符。

2)、this.repository.findOne(......)

private EnvironmentRepository repository;复制代码
Environment environment = this.repository.findOne(name, profiles, label);复制代码

这个就是环境仓库接口:

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig_02

这里我们可以看到JGitEnvironmentRepository这个是用来处理Git仓库的,而我们本次是读取本地文件,其用的是NativeEnvironmentRepository。不过这里是用来代理模式:

public class EnvironmentEncryptorEnvironmentRepository implements EnvironmentRepository {复制代码
@Overridepublic Environment findOne(String name, String profiles, String label) {
   Environment environment = this.delegate.findOne(name, profiles, label);   if (this.environmentEncryptor != null) {
      environment = this.environmentEncryptor.decrypt(environment);
   }   if (!this.overrides.isEmpty()) {
      environment.addFirst(new PropertySource("overrides", this.overrides));
   }   return environment;
}复制代码
public class CompositeEnvironmentRepository implements EnvironmentRepository {   protected List<EnvironmentRepository> environmentRepositories;复制代码
@Overridepublic Environment findOne(String application, String profile, String label) {
   Environment env = new Environment(application, new String[]{profile}, label, null, null);   if(environmentRepositories.size() == 1) {
      Environment envRepo = environmentRepositories.get(0).findOne(application, profile, label);
      env.addAll(envRepo.getPropertySources());
      env.setVersion(envRepo.getVersion());
      env.setState(envRepo.getState());
   } else {      for (EnvironmentRepository repo : environmentRepositories) {
         env.addAll(repo.findOne(application, profile, label).getPropertySources());
      }
   }   return env;
}复制代码

然后NativeEnvironmentRepository是放在environmentRepositories里,所以其是在CompositeEnvironmentRepository中再去调用NativeEnvironmentRepository。

三、NativeEnvironmentRepository

1、基本结构

public class NativeEnvironmentRepository
      implements EnvironmentRepository, SearchPathLocator, Ordered {复制代码

2、变量

private String defaultLabel;/**
 * Locations to search for configuration files. Defaults to the same as a Spring Boot
 * app so [classpath:/,classpath:/config/,file:./,file:./config/].
 */private String[] searchLocations;/**
 * Flag to determine how to handle exceptions during decryption (default false).
 */private boolean failOnError;/**
 * Flag to determine whether label locations should be added.
 */private boolean addLabelLocations;/**
 * Version string to be reported for native repository
 */private String version;private static final String[] DEFAULT_LOCATIONS = new String[] { "classpath:/",      "classpath:/config/", "file:./", "file:./config/" };private ConfigurableEnvironment environment;复制代码

defaultLabel:默认的label

searchLocations:表示进行文件获取的时候的搜索路径

addLabelLocations:表示lable的值是否也参加进对应的路径

DEFAULT_LOCATIONS:表示如果searchLocations没有设置值的时候是默认在这些路径下去搜索对应的文件

3、构造方法

public NativeEnvironmentRepository(ConfigurableEnvironment environment, NativeEnvironmentProperties properties) {   this.environment = environment;   this.addLabelLocations = properties.getAddLabelLocations();   this.defaultLabel = properties.getDefaultLabel();   this.failOnError = properties.getFailOnError();   this.order = properties.getOrder();   this.searchLocations = properties.getSearchLocations();   this.version = properties.getVersion();
}复制代码

可以看到其主要是从NativeEnvironmentProperties中读取对应的信息进行赋值

4、NativeEnvironmentProperties

1)、结构

@ConfigurationProperties("spring.cloud.config.server.native")public class NativeEnvironmentProperties implements EnvironmentRepositoryProperties {复制代码

所以你可以通过再配置文件中设置这些来给NativeEnvironmentRepository的属性进行赋值。

2)、变量

private Boolean failOnError = false;/**
 * Flag to determine whether label locations should be added.
 */private Boolean addLabelLocations = true;private String defaultLabel = "master";/**
 * Locations to search for configuration files. Defaults to the same as a Spring Boot
 * app so [classpath:/,classpath:/config/,file:./,file:./config/].
 */private String[] searchLocations = new String[0];/**
 * Version string to be reported for native repository
 */private String version;private int order = Ordered.LOWEST_PRECEDENCE;复制代码

可以看到其的addLabelLocations是默认为true,defaultLabel默认为master分支

5、findOne(......)方法

@Overridepublic Environment findOne(String config, String profile, String label) {
   SpringApplicationBuilder builder = new SpringApplicationBuilder(
         PropertyPlaceholderAutoConfiguration.class);
   ConfigurableEnvironment environment = getEnvironment(profile);
   builder.environment(environment);
   builder.web(WebApplicationType.NONE).bannerMode(Mode.OFF);   if (!logger.isDebugEnabled()) {      // Make the mini-application startup less verbose  builder.logStartupInfo(false);
   }
   String[] args = getArgs(config, profile, label);   // Explicitly set the listeners (to exclude logging listener which would change
   // log levels in the caller)
   builder.application()
         .setListeners(Arrays.asList(new ConfigFileApplicationListener()));
   ConfigurableApplicationContext context = builder.run(args);
   environment.getPropertySources().remove("profiles");   try {      return clean(new PassthruEnvironmentRepository(environment).findOne(config,
            profile, label));
   }   finally {
      context.close();
   }
}复制代码

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig_03

这个方法就是获取Environment的具体逻辑处理方法。这里首先是创建SpringApplicationBuilder对象。再进行对应的环境赋值,之后再进行Web容器的运行builder.run(args),其是这里的运行主要是为了去触发ConfigFileApplicationListener监听器的逻辑,在这个ConfigFileApplicationListener中会完成对应文件的读取及构建对应的Envirement。下面我们就来看下这个具体的过程:

1)、getEnvironment(profile)

private ConfigurableEnvironment getEnvironment(String profile) {
   ConfigurableEnvironment environment = new StandardEnvironment();
   Map<String, Object> map = new HashMap<>();
   map.put("spring.profiles.active", profile);
   map.put("spring.main.web-application-type", "none");
   environment.getPropertySources().addFirst(new MapPropertySource("profiles", map));   return environment;
}复制代码

创建一个基本的Envirement StandardEnvironment 对象,然后就是将要获取profile(哪种环境)与key spring.profiles.active构建一个map,然后再以此创建一个对应的MapPropertySource添加到Environment中。

2)、getArgs(String application, String profile, String label)

private String[] getArgs(String application, String profile, String label) {
   List<String> list = new ArrayList<String>();
   String config = application;   if (!config.startsWith("application")) {
      config = "application," + config;
   }
   list.add("--spring.config.name=" + config);
   list.add("--spring.cloud.bootstrap.enabled=false");
   list.add("--encrypt.failOnError=" + this.failOnError);
   list.add("--spring.config.location=" + StringUtils.arrayToCommaDelimitedString(
         getLocations(application, profile, label).getLocations()));   return list.toArray(new String[0]);
}复制代码

这里首先我们需要注意,config = "application," + config,也就是说,SpringCloudConfig不单是会去读取传入的应用名称对应的文件,也会默认读取application.yml或application.properties文件。

然后就是--spring.config.location=(读取文件的时候是在那些路径下读取)的设置:

@Overridepublic Locations getLocations(String application, String profile, String label) {
   String[] locations = this.searchLocations;   if (this.searchLocations == null || this.searchLocations.length == 0) {
      locations = DEFAULT_LOCATIONS;
   }
   Collection<String> output = new LinkedHashSet<String>();   if (label == null) {
      label = defaultLabel;
   }   for (String location : locations) {
      String[] profiles = new String[] { profile };      if (profile != null) {
         profiles = StringUtils.commaDelimitedListToStringArray(profile);
      }
      String[] apps = new String[] { application };      if (application != null) {
         apps = StringUtils.commaDelimitedListToStringArray(application);
      }      for (String prof : profiles) {         for (String app : apps) {
            String value = location;if (application != null) {
               value = value.replace("{application}", app);
            }if (prof != null) {
               value = value.replace("{profile}", prof);
            }if (label != null) {
               value = value.replace("{label}", label);
            }if (!value.endsWith("/")) {
               value = value + "/";
            }if (isDirectory(value)) {
               output.add(value);
            }
         }
      }
   }   if (this.addLabelLocations) {      for (String location : locations) {         if (StringUtils.hasText(label)) {
            String labelled = location + label.trim() + "/";if (isDirectory(labelled)) {
               output.add(labelled);
            }
         }
      }
   }   return new Locations(application, profile, label, this.version,
         output.toArray(new String[0]));
}复制代码

1、首先是通过String[] locations = this.searchLocations,设置对应的目录地址,可以看到,如果locations目前还没有设置值,就将其设置默认值DEFAULT_LOCATIONS。

2、如果label也没有设值也通过label = defaultLabel设置默认值master。

3、再是通过String[] profiles = new String[] { profile }、String[] apps = new String[] { application }对profile、application 的解析。StringUtils.commaDelimitedListToStringArray(...)方法是通过,来对字符串解析切割。由这里我们看到profile&application 都是可以配置读取多个对应组合的文件的,其是用,来进行分隔。

4、之后再是对{application}这种的替换。再通过isDirectory方法来判断这个是不是地址:

private boolean isDirectory(String location) {   return !location.contains("{") && !location.endsWith(".properties")
         && !location.endsWith(".yml") && !location.endsWith(".yaml");
}复制代码

是的话就将其添加output中。

5、再通过对addLabelLocations的判断,看是否label也参与目录组合(默认为true),是的会就通过String labelled = location + label.trim() + "/";来组合路径。

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig_04

3)、findOne结束逻辑

对于整个getArgs方法来说,就是将Locations的locations返回。再通过arrayToCommaDelimitedString方法来将其拼接为一个字符串(以,划分)

public static String arrayToCommaDelimitedString(@Nullable Object[] arr) {   return arrayToDelimitedString(arr, ",");
}复制代码

最后对于findOne方法来说:

return clean(new PassthruEnvironmentRepository(environment).findOne(config,
      profile, label));复制代码
protected Environment clean(Environment value) {
   Environment result = new Environment(value.getName(), value.getProfiles(),
         value.getLabel(), this.version, value.getState());   for (PropertySource source : value.getPropertySources()) {
      String name = source.getName();      if (this.environment.getPropertySources().contains(name)) {         continue;
      }
      ......
      logger.info("Adding property source: " + name);
      result.add(new PropertySource(name, source.getSource()));
   }   return result;
}复制代码

这里的逻辑,其就是将前面创建读取赋值的StandardEnvirement中的PropertySources添加到Environment中来用于本次远程调用的返回(这个StandardEnvirement的赋值我们后面就会分析到)。注意这个Envirement与接口Envirement的区别,这个是package org.springframework.cloud.config.environment;包中的,这个Envirement是一个类。

public class Environment {   private String name;   private String[] profiles = new String[0];   private String label;   private List<PropertySource> propertySources = new ArrayList<>();   private String version;   private String state;复制代码

下面我们来梳理下前面提到的ConfigFileApplicationListener对配置文件的读取解析主要就是这个类。

四、ConfigFileApplicationListener

1、结构&变量

public class ConfigFileApplicationListener  implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {   private static final String DEFAULT_PROPERTIES = "defaultProperties";   // Note the order is from least to most specific (last one wins)
   private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";   private static final String DEFAULT_NAMES = "application";   private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);   /**
    * The "active profiles" property name.
    */
   public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";   /**
    * The "includes profiles" property name.
    */
   public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";   /**
    * The "config name" property name.
    */
   public static final String CONFIG_NAME_PROPERTY = "spring.config.name";   /**
    * The "config location" property name.
    */
   public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";   /**
    * The "config additional location" property name.
    */
   public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";   /**
    * The default order for the processor.
    */
   public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;   /**
    * Name of the application configuration {@link PropertySource}.
    */
   public static final String APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME = "applicationConfigurationProperties";    
   private final DeferredLog logger = new DeferredLog();   private String searchLocations;   private String names;   private int order = DEFAULT_ORDER;复制代码

这里的变量主要是定义一些默认值,对于这些值通过前面的梳理,我们也是眼熟了。

2、postProcessEnvironment方法

@Overridepublic void postProcessEnvironment(ConfigurableEnvironment environment,
      SpringApplication application) {
   addPropertySources(environment, application.getResourceLoader());
}复制代码
protected void addPropertySources(ConfigurableEnvironment environment,
      ResourceLoader resourceLoader) {
   RandomValuePropertySource.addToEnvironment(environment);   new Loader(environment, resourceLoader).load();
}复制代码

这就是触发加载配置文件并将其添加到environment中的MutablePropertySources中的逻辑。主要处理的类就是Loader。下面我们具体来分析下这个类。

五、Loader

1、结构&变量

private class Loader {   private final Log logger = ConfigFileApplicationListener.this.logger;   private final ConfigurableEnvironment environment;   private final ResourceLoader resourceLoader;   private final List<PropertySourceLoader> propertySourceLoaders;   private Deque<Profile> profiles;   private List<Profile> processedProfiles;   private boolean activatedProfiles;   private Map<Profile, MutablePropertySources> loaded;   private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();

   Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {      this.environment = environment;      this.resourceLoader = (resourceLoader != null ? resourceLoader
            : new DefaultResourceLoader());      this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
            PropertySourceLoader.class, getClass().getClassLoader());
   }复制代码

其是ConfigFileApplicationListener的内部类。

2、load方法

public void load() {   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 (profile != null && !profile.isDefaultProfile()) {
         addProfileToEnvironment(profile.getName());
      }
      load(profile, this::getPositiveProfileFilter,
            addToLoaded(MutablePropertySources::addLast, false));      this.processedProfiles.add(profile);
   }
   load(null, this::getNegativeProfileFilter,
         addToLoaded(MutablePropertySources::addFirst, true));
   addLoadedPropertySources();
}复制代码

1)、这里先是通过initializeProfiles方法来对profiles赋值,然后对其进行遍历读取。

2)、先通过addProfileToEnvironment,来将该profile设置为激活的profile。

3)、再通过load(...)方法来进行解析读取配置文件。

4)、最后通过addLoadedPropertySources方法来将读取的PropertySource添加到envirement中。

5)、这些就是其的主要处理逻辑,为了更好的分层排版,我们将这个方法调用的其它方法也写在第二级。

3、initializeProfiles()

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);
   Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();   this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));   // Any pre-existing active profiles set via property sources (e.g.
   // System properties) take precedence over those added in config files.
   addActiveProfiles(activatedViaProperty);   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);
      }
   }
}复制代码

这个方法是对profiles的赋值,可以看到这里首先是this.profiles.add(null),这个赋值的意义类似与前面默认添加的application。例如你的应用名是configClient(application),然后你要读取的环境是dev(profile),所以拼接就可以是文件configClient_dev.yml,然后由于有这个赋值,其也会读取configClient.yml,不管你传入的profile是哪个,会以{application}.yml为公共的配置文件。同时如果this.profiles.size() == 1,表示这里只有默认的null,其也会添加一个默认的profile名称default,也就是RESERVED_DEFAULT_PROFILE_NAME = "default";。不过由于我们传入了dev,所以这里是有两个:

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig_05

1)、getProfilesActivatedViaProperty()

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig_06

从envirement中读取这些key 所对应的value。

4、addProfileToEnvironment(String profile)

private void addProfileToEnvironment(String profile) {   for (String activeProfile : this.environment.getActiveProfiles()) {      if (activeProfile.equals(profile)) {         return;
      }
   }   this.environment.addActiveProfile(profile);
}复制代码
@Overridepublic void addActiveProfile(String profile) {   if (logger.isDebugEnabled()) {
      logger.debug("Activating profile '" + profile + "'");
   }
   validateProfile(profile);
   doGetActiveProfiles();   synchronized (this.activeProfiles) {      this.activeProfiles.add(profile);
   }
}复制代码

5、load(Profile profile, ...)

private void load(Profile profile, DocumentFilterFactory filterFactory,
      DocumentConsumer consumer) {
   getSearchLocations().forEach((location) -> {      boolean isFolder = location.endsWith("/");
      Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
      names.forEach(
            (name) -> load(location, name, profile, filterFactory, consumer));
   });
}复制代码

这里就是遍历搜索目录。

1)、getSearchLocations()

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

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig_07

如果有设置对应的读取路径,就直接返回设置的值。如果没有设置就返回默认的读取路径。

2)、getSearchNames()

private Set<String> getSearchNames() {   if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
      String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);      return asResolvedSet(property, null);
   }   return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}复制代码

读取应用名称,我们这里的就是application,config-client。

3)、load(location, name, profile, filterFactory, consumer)

private void load(String location, String name, Profile profile,
      DocumentFilterFactory filterFactory, DocumentConsumer consumer) {   if (!StringUtils.hasText(name)) {      for (PropertySourceLoader loader : this.propertySourceLoaders) {         if (canLoadFileExtension(loader, location)) {
            load(loader, location, profile,
                  filterFactory.getDocumentFilter(profile), consumer);
         }
      }
   }   for (PropertySourceLoader loader : this.propertySourceLoaders) {      for (String fileExtension : loader.getFileExtensions()) {
         String prefix = location + name;
         fileExtension = "." + fileExtension;
         loadForFileExtension(loader, prefix, fileExtension, profile,
               filterFactory, consumer);
      }
   }
}复制代码

这里的this.propertySourceLoaders (PropertySourceLoader)是用来加载对应文件的。这里其会有两个子类:

public class PropertiesPropertySourceLoader implements PropertySourceLoader {   private static final String XML_FILE_EXTENSION = ".xml";   @Override
   public String[] getFileExtensions() {      return new String[] { "properties", "xml" };
   }复制代码
public class YamlPropertySourceLoader implements PropertySourceLoader {   @Override
   public String[] getFileExtensions() {      return new String[] { "yml", "yaml" };
   }复制代码

可以看到这里的YamlPropertySourceLoader是用来读取以.yml、yaml为后缀的文件,PropertiesPropertySourceLoader是用来读取.properties、xml文件的。然后上面的load(loader, location, profile...)方法我们到后面再梳理,因为其会在下面的loadForFileExtension(...)方法中调用**(注意String prefix = location + name;)**。我们主要来看下一个对this.propertySourceLoaders的for循环。上面的是如果location本身就是一个文件具体的地址了,就调用load方法。

6、loadForFileExtension

private void loadForFileExtension(PropertySourceLoader loader, String prefix,
      String fileExtension, Profile profile,
      DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
   DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
   DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);   if (profile != null) {      // Try profile-specific file & profile section in profile file (gh-340)  String profileSpecificFile = prefix + "-" + profile + fileExtension;
      load(loader, profileSpecificFile, profile, defaultFilter, consumer);
      load(loader, profileSpecificFile, profile, profileFilter, consumer);      // Try profile specific sections in files we've already processed  for (Profile processedProfile : this.processedProfiles) {         if (processedProfile != null) {
            String previouslyLoaded = prefix + "-" + processedProfile
                  + fileExtension;
            load(loader, previouslyLoaded, profile, profileFilter, consumer);
         }
      }
   }   // Also try the profile-specific section (if any) of the normal file
   load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}复制代码

这里我们需要注意对文件名称的整个拼接,profile != null就是表示如果profile是有值的,其的拼接是String profileSpecificFile = prefix + "-" + profile + fileExtension,如果其为null就是通过最后的load方法,其的加载名称是prefix + fileExtension。下面我们就来具体看下load方法。

SpringCloudConfig源码解读系列(1)-从配置中心获取配置信息-读取本地配置_SpringCloudConfig_08

7、load(PropertySourceLoader loader, String location,...)

private void load(PropertySourceLoader loader, String location, Profile profile,
      DocumentFilter filter, DocumentConsumer consumer) {   try {
      Resource resource = this.resourceLoader.getResource(location);
      String description = getDescription(location, resource);
      ......
      String name = "applicationConfig: [" + location + "]";
      List<Document> documents = loadDocuments(loader, name, resource);      if (CollectionUtils.isEmpty(documents)) {         this.logger.trace("Skipped unloaded config " + description);         return;
      }
      List<Document> loaded = new ArrayList<>();      for (Document document : documents) {         if (filter.match(document)) {
            addActiveProfiles(document.getActiveProfiles());
            addIncludedProfiles(document.getIncludeProfiles());
            loaded.add(document);
         }
      }
      Collections.reverse(loaded);      if (!loaded.isEmpty()) {
         loaded.forEach((document) -> consumer.accept(profile, document));         this.logger.debug("Loaded config file " + description);
      }
   }
   ......
}复制代码

1)、这里首先是通过Resource resource = this.resourceLoader.getResource(location)来加载对应的文件。

2)、再通过List<Document> documents = loadDocuments(loader, name, resource);来将对应的文件内容加载为Document

private static class Document {

   private final PropertySource<?> propertySource;

   private String[] profiles;

   private final Set<Profile> activeProfiles;

   private final Set<Profile> includeProfiles;复制代码

3)、然后再遍历将其添加到loaded.add(document);中。

4)、再就是通过loaded.forEach((document) -> consumer.accept(profile, document)); consumer去处理了。这里是入参,我们一直回到最前面,其的传值是在一开始了

public void load() {   this.profiles = new LinkedList<>();   this.processedProfiles = new LinkedList<>();   this.activatedProfiles = false;   this.loaded = new LinkedHashMap<>();
   initializeProfiles();
   ......
      load(profile, this::getPositiveProfileFilter,
            addToLoaded(MutablePropertySources::addLast, false));
   ......
   addLoadedPropertySources();
}复制代码
private DocumentFilter getPositiveProfileFilter(Profile profile) {   return (Document document) -> {      if (profile == null) {         return ObjectUtils.isEmpty(document.getProfiles());
      }      return ObjectUtils.containsElement(document.getProfiles(),
            profile.getName())
            && this.environment.acceptsProfiles(document.getProfiles());
   };
}复制代码
private DocumentConsumer addToLoaded(
      BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,      boolean checkForExisting) {   return (profile, document) -> {      if (checkForExisting) {         for (MutablePropertySources merged : this.loaded.values()) {if (merged.contains(document.getPropertySource().getName())) {               return;
            }
         }
      }
      MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
            (k) -> new MutablePropertySources());
      addMethod.accept(merged, document.getPropertySource());
   };
}复制代码

可以看到这里的DocumentConsumer其实是调用MutablePropertySources::addLast、this.loaded.computeIfAbsent(...)方法链。也就是读取Document的内容document.getPropertySource(),再调用的addLast方法来将其添加到this.loaded中(private Map<Profile, MutablePropertySources> loaded;)。

8、addLoadedPropertySources()

private void addLoadedPropertySources() {
   MutablePropertySources destination = this.environment.getPropertySources();
   List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
   Collections.reverse(loaded);
   String lastAdded = null;
   Set<String> added = new HashSet<>();   for (MutablePropertySources sources : loaded) {      for (PropertySource<?> source : sources) {         if (added.add(source.getName())) {
            addLoadedPropertySource(destination, lastAdded, source);
            lastAdded = source.getName();
         }
      }
   }
}复制代码

这个方法就是将前面读取到this.loaded中的内容通过addLoadedPropertySource方法将其添加到this.environment中。这里就填你上面说的对StandardEnvirement赋值的问题了。

至此,关于SpringCloudConfig读取远程配置文件的原理就梳理完成了。