Loading...
墨滴

lyq

2022/01/27  阅读:32  主题:橙心

Spring Cloud Config Client源码分析之加载外部化配置

spring cloud 版本为Hoxton.SR9


前言

在上一篇《Spring Cloud 引导上下文源码分析》中,知道了在Spring Cloud环境中,是怎么加载相关配置文件的。今天一起看看在Spring Cloud Config Client中,是怎么加载外部配置的。

在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是如何在启动过程中从远程服务器上加载配置的

源码分析

Config Client 配置加载过程

从前面的代码分析过程中我们知道,Environment中所有外部化配置,针对不同类型的配置都会有与之对应的PropertySource,比如(SystemEnvironmentPropertySourceCommandLinePropertySource)。以及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);
  // 准备spring上下文,内部会回调所有ApplicationContextInitializer的实例
  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;
}

在前文中介绍到,在spring cloud引导上下文启动的时候,会通过spring boot的工厂模式加载key为org.springframework.cloud.bootstrap.BootstrapConfiguration的配置,其中就有一个PropertySourceBootstrapConfiguration类,会被加载到spring的上下文中,并且这个类实现了ApplicationContextInitializer接口,所以在spring boot的prepareContext方法中,也会回调这个类的initialize方法。

PropertySourceBootstrapConfiguration

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
 List<PropertySource<?>> composite = new ArrayList<>();
 //排序
 AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
 //加载配置标识
 boolean empty = true;
 //获取上下文中的Environment对象
 ConfigurableEnvironment environment = applicationContext.getEnvironment();
 //遍历propertySourceLocators列表
 for (PropertySourceLocator locator : this.propertySourceLocators) {
  //调用PropertySourceLocator实例的locateCollection方法,加载外部配置
  Collection<PropertySource<?>> source = locator.locateCollection(environment);
  if (source == null || source.size() == 0) {
   continue;
  }
  List<PropertySource<?>> sourceList = new ArrayList<>();
  for (PropertySource<?> p : source) {
   if (p instanceof EnumerablePropertySource) {
    EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
    sourceList.add(new BootstrapPropertySource<>(enumerable));
   }
   else {
    sourceList.add(new SimpleBootstrapPropertySource(p));
   }
  }
  logger.info("Located property source: " + sourceList);
  //将PropertySource列表加入到复合列表
  composite.addAll(sourceList);
  // 设置标识
  empty = false;
 }
 // 只有在empty=false的时候,会执行以下代码
 if (!empty) {
  //从environment获取属性源
  MutablePropertySources propertySources = environment.getPropertySources();
  String logConfig = environment.resolvePlaceholders("${logging.config:}");
  LogFile logFile = LogFile.get(environment);
  for (PropertySource<?> p : environment.getPropertySources()) {
   //移除名称为bootstrapProperties的属性源
   if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
    propertySources.remove(p.getName());
   }
  }
  //将外部配置添加到environment中
  insertPropertySources(propertySources, composite);
  reinitializeLoggingSystem(environment, logConfig, logFile);
  setLogLevels(applicationContext, environment);
  handleIncludedProfiles(environment);
 }
}

具体逻辑,详见代码中注释。其中有两处关键代码:

  • 一处是this.propertySourceLocators的来源
  • 另一处是PropertySourceLocator的locateCollection方法,是怎么加载远程配置的。

针对以上2个问题,进行代码分析:

this.propertySourceLocators的来源

@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

此行代码解释了,是通过依赖注入的方式,注入到当前实例中。那么这个PropertySourceLocator实例又是在哪里装载到应用上下文中的呢?当引入spring-cloud-config-client依赖后,会从spring.factories中加载以下配置:

# Bootstrap components
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration,\
org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration

ConfigServiceBootstrapConfiguration

@Bean
@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(name 
= ConfigClientProperties.PREFIX + ".enabled", matchIfMissing = true)
public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
 return new ConfigServicePropertySourceLocator(properties);
}

在上下文中缺少ConfigServicePropertySourceLocator类型的bean时,会加载这个类型的bean到上下文中,而ConfigServicePropertySourceLocator实现了PropertySourceLocator接口,从而也就解释了上面的疑问。

PropertySourceLocator是如何加载远程配置的

PropertySourceLocator

/**
 * @param environment The current Environment.
 * @return A PropertySource, or null if there is none.
 * @throws IllegalStateException if there is a fail-fast condition.
 */

PropertySource<?> locate(Environment environment);

default Collection<PropertySource<?>> locateCollection(Environment environment) {
 return locateCollection(this, environment);
}

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

内部最终会通过调用locate方法,由子类去具体实现。那么在spring cloud config环境下,看看它是怎么具体实现的,进入子类ConfigServicePropertySourceLocator

ConfigServicePropertySourceLocator

@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(org.springframework.core.env.Environment environment) {
 //拷贝默认的config client配置,通过以spring.cloud.config为前缀的配置
 ConfigClientProperties properties = this.defaultProperties.override(environment);
 //创建名称为configService的PropertySource
 CompositePropertySource composite = new OriginTrackedCompositePropertySource("configService");
 //创建requestTemplate工厂,创建requestTemplate以及发送远程请求
 ConfigClientRequestTemplateFactory requestTemplateFactory = new ConfigClientRequestTemplateFactory(logger,
   properties);

 Exception error = null;
 String errorBody = null;
 try {
  String[] labels = new String[] { "" };
  //从配置中获取label
  if (StringUtils.hasText(properties.getLabel())) {
   labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel());
  }
  //从threadLocal中获取state
  String state = ConfigClientStateHolder.getState();
  // Try all the labels until one works
  //遍历label
  for (String label : labels) {
   //从config server获取配置
   Environment result = getRemoteEnvironment(requestTemplateFactory, 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));
     }
    }

    HashMap<String, Object> map = new HashMap<>();
    if (StringUtils.hasText(result.getState())) {
     putValue(map, "config.client.state", result.getState());
    }
    if (StringUtils.hasText(result.getVersion())) {
     putValue(map, "config.client.version", result.getVersion());
    }
    // the existence of this property source confirms a successful
    // response from config server
    // 将远程属性添加到列表开头
    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;

}

进入getRemoteEnvironment方法:

private Environment getRemoteEnvironment(ConfigClientRequestTemplateFactory requestTemplateFactory, String label,
   String state)
 
{
 // 获取restTemplate
 RestTemplate restTemplate = this.restTemplate == null ? requestTemplateFactory.create() : this.restTemplate;
 ConfigClientProperties properties = requestTemplateFactory.getProperties();
 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)) {
  // workaround for Spring MVC matching / in paths
  label = Environment.denormalize(label);
  args = new String[] { name, profile, label };
  path = path + "/{label}";
 }
 ResponseEntity<Environment> response = null;
 List<MediaType> acceptHeader = Collections.singletonList(MediaType.parseMediaType(properties.getMediaType()));

 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();
   headers.setAccept(acceptHeader);
   requestTemplateFactory.addAuthorizationToken(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.classargs);
  }
  catch (HttpClientErrorException e) {
   if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
    throw e;
   }
  }
  catch (ResourceAccessException e) {
   logger.info("Connect Timeout Exception on Url - " + uri + ". Will be trying the next url if available");
   if (i == noOfUrls - 1) {
    throw e;
   }
   else {
    continue;
   }
  }

  if (response == null || response.getStatusCode() != HttpStatus.OK) {
   return null;
  }

  Environment result = response.getBody();
  return result;
 }

 return null;
}

此方法比较简单:构造请求参数以及请求url,最后通过RestTemplate向config server发起请求,获取配置,将响应封装成Environment对象。

以上就是spring cloud config client在启动阶段加载远程配置的过程分析,有了前几篇文章的基础,相信理解起来是比较容易的。

至于spring cloud config client怎么在运行时,配置变更时,怎么做到刷新配置的,需要借助spring cloud bus,关于这个,将在下一篇文章中进行分析,敬请期待!

总结

spring boot在启动阶段,在prepareContext方法中,会循环调用ApplicationContextInitializer类的子类的initialize方法。在spring cloud环境中,由于PropertySourceBootstrapConfiguration类实现了ApplicationContextInitializer,并重写了initialize方法,所以也会被调用。在其方法内部,会从spring上下文中查找PropertySourceLocator类型的bean,并调用它的locateCollection方法,spring cloud config client依赖存在的情况下,会调用子类ConfigServicePropertySourceLocatorlocate方法,最终通过RestTemplate对象,向config server端发起远程调用并将返回结果封装成Environment。将远程配置添加到本地环境之前(也就是说远程配置会覆盖本地同名配置)。

欢迎关注我的公众号:程序员L札记

更多原创文章,请扫码关注我的微信公众号
更多原创文章,请扫码关注我的微信公众号

lyq

2022/01/27  阅读:32  主题:橙心

作者介绍

lyq