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

一、基本原理

​ 我们知道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接口。下面我们来看其的方法调用:

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

​ 可以看到这里还有决定是添加在addLastaddBefore

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)、(_)替换符处理

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

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

在这里插入图片描述

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

2)、this.repository.findOne(…)

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

​ 这个就是环境仓库接口:

在这里插入图片描述

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

public class EnvironmentEncryptorEnvironmentRepository implements EnvironmentRepository {
@Override
public 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;
@Override
public 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(…)方法

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

在这里插入图片描述

​ 这个方法就是获取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.ymlapplication.properties文件。

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

@Override
public 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 }profileapplication的解析。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() + "/";来组合路径。

在这里插入图片描述

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方法

@Override
public 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,所以这里是有两个:

在这里插入图片描述

1)、getProfilesActivatedViaProperty()

在这里插入图片描述

​ 从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);
}
@Override
public 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;
}

在这里插入图片描述

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

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是用来读取以.ymlyaml为后缀的文件,PropertiesPropertySourceLoader是用来读取.propertiesxml文件的。然后上面的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方法。

在这里插入图片描述

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::addLastthis.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读取远程配置文件的原理就梳理完成了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值