写于: 2019-10-21 22:52:37

参考资料: Spring Cloud Config 官网

Spring Cloud Config GItub

Spring Cloud Config 概述

# Config Server 配置的读取与存储

# @EnableConfigServer 为入口

@EnableConfigServer 开启了 Spring Cloud Config 功能

@EnableXXX 注解通常使用 @Import(xxxConfiguration.class) 的方式引导配置项进行配置

友链:[PRACTICE]-Springboot 自动化配置#模式一

@EnableConfigServer 导入的是 ConfigServerConfiguration,来看看其代码实现

@Configuration
public class ConfigServerConfiguration {
	@Bean
	public Marker enableConfigServerMarker() {
		return new Marker();
	}
	class Marker {
	}
}

代码中的 Bean Marker 是开启 Spring Cloud Config Server 自动配置的开关。

通过在相关配置类上加上 @ConditionalOnBean(ConfigServerConfiguration.Marker.class)实现开关配置。

Spring Cloud Zuul【源码篇】揭秘 Zuul 中提到了 Zuul 开启的注解 @EnableZuulProxy 所导入的配置类**ZuulProxyMarkerConfiguration** 同样是通过实例化 Marker 作为开启条件。

# 通过 ConfigServerConfiguration.Marker 定位 Config Server 配置类

通过定位,定位到自动配置类 ConfigServerAutoConfiguration

代码如下:

@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class) 
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class,
		ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class,
		ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {

}

上述代码中值得关注的两个点

  • ConfigServerProperties

    Config Server 的配置文件

  • @Import(xxx)

    导入了 Config Server 的多种配置信息

    EnvironmentRepositoryConfiguration 仓储配置,包括:JDBC、GIT、SVN 等多种配置仓储实现配置。

    CompositeConfiguration 多环境仓储配置。EnvironmentRepositoryConfiguration 中如果配置了多种仓储,CompositeConfiguration 会整合生成 SearchPathLocator

    ResourceRepositoryConfiguration 文件资源仓库配置。

    ConfigServerEncryptionConfiguration 加解密功能配置。

    ConfigServerMvcConfiguration API 开放查询配置接口配置。如:EnvironmentController 仓储配置查询 API。

# EnvironmentRepositoryConfiguration.class 仓储配置

EnvironmentRepositoryConfiguration 中有关于:JDBC、GIT、SVN 等仓储配置,为了节省篇幅,直接来看默认实现仓储,代码如下

在多种配置仓储实现中,Spring Cloud 提供了默认的仓储实现:MultipleJGitEnvironmentRepository,对应的自动配置类为:DefaultRepositoryConfiguration

@Configuration
@ConditionalOnMissingBean(value = EnvironmentRepository.class, search = SearchStrategy.CURRENT)
class DefaultRepositoryConfiguration {
	@Bean
	public MultipleJGitEnvironmentRepository defaultEnvironmentRepository(
			MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory,
			MultipleJGitEnvironmentProperties environmentProperties) throws Exception {
		return gitEnvironmentRepositoryFactory.build(environmentProperties);
	}
}

通过代码能够得知,Spring Cloud Config 默认使用 Git 作为配置文件的管理仓储。

所有仓储的实现默认实现自接口 EnvironmentRepository ,也就是说,如果需要替换仓储实现,只需要实例化相关的实现 Bean,如:JdbcEnvironmentRepository 。同样的,如果需要自定义仓储实现,同样只需要实现接口 EnvironmentRepository 即可自定义仓储实现。

# 默认仓储实现 MultipleJGitEnvironmentRepository 分析

类图结构如下

Git仓储实现_类结构图

关注点:

  • InitializingBean

    实现该接口的 Bean 在初始化的时候,会执行 InitializingBean#afterPropertiesSet 方法

  • SearchPathLocator

    提供获取配置文件地址的能力

  • EnvironmentRepository

    接口规范:实现 EnvironmentRepository#findOne ,获取环境数据。

  • ResourceLoaderAware

    默认是实现:DefaultResourceLoader 。根据配置文件地址,加载配置文件。

以上几个接口实现,提供了 git 仓储,从初始化、获取git地址、根据git地址加载配置信息、构建 Environment 等一系列能力。

# MultipleJGitEnvironmentRepository#findone-环境配置获取代码

API_演示

① 相关代码:

public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository {
    /**
     * @param application 应用名称【spring.application.name 配置】
     * @param profile 配置文件环境【如:dev、test。默认:default】
     * @param label 分支 【默认:master】
     */
    @Override
	public Environment findOne(String application, String profile, String label) {
     	......
        return super.findOne(application, profile, label);
     	......
    }
}

聚焦 ② AbstractScmEnvironmentRepository#findOne 代码实现

public abstract class AbstractScmEnvironmentRepository{
    @Override
	public synchronized Environment findOne(String application, String profile,String label){
        NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(
				getEnvironment(), new NativeEnvironmentProperties());
        // 获取 git 仓库地址
		Locations locations = getLocations(application, profile, label);
		delegate.setSearchLocations(locations.getLocations());
        // 加载配置
		Environment result = delegate.findOne(application, profile, "");
		result.setVersion(locations.getVersion());
		result.setLabel(label);
        // 组装数据,并返回
		return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(),
				getUri());
    }
}

关键逻辑:

  • 1、根据 application、profile、label 获取配置文件地址
  • 2、加载配置信息
  • 3、组装 Environment 数据

这里只关注业务主线,不关注具体实现。

# Config Server 提供配置读取API

Spring Cloud Config Server 提供了一套完整的 API ,供客户端调用。

# 相关 API 代码

在上面的提到了 ConfigServerMvcConfiguration 配置类,该配置类中实例化了 EnvironmentController ,该类提供了相关 API 。

EnvironmentController 部分代码如下:

@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {
    // 环境仓储:默认 git 实现
    private EnvironmentRepository repository;
    @RequestMapping("/{name}/{profiles}/{label:.*}")
	public Environment labelled(@PathVariable String name, @PathVariable String profiles,
			@PathVariable String label) {
        ......
		Environment environment = this.repository.findOne(name, profiles, label);
        ......
		return environment;
	}
}

EnvironmentRepository默认实现为:MultipleJGitEnvironmentRepository

# 测试 API

参考 Spring-Cloud-Config概述 中的 config server 服务

在浏览器中,通过:http://{ip}:{port}/{name}/{profiles}/{label} 的方式获取配置信息。

API_演示

# 扩展:Jdbc 仓储的实现

JDBC 仓储的实现类为:JdbcEnvironmentRepository

该类实现了 EnvironmentRepository 和 Ordered 接口。

# 相关代码实现

聚焦 JdbcEnvironmentRepository#findOne JDBC 仓储实现

public class JdbcEnvironmentRepository implements EnvironmentRepository, Ordered {
    @Override
	public Environment findOne(String application, String profile, String label) {
        Map<String, String> next = (Map<String, String>) this.jdbc.query(this.sql, new Object[] { app, env, label }, this.extractor);
		if (!next.isEmpty()) {
			environment.add(new PropertySource(app + "-" + env, next));
		}
    }
}

代码很直观,简单粗暴,直接通过 SQL 语句获取配置信息。

来看看,其默认的 SQL 语句

# SELECT KEY, VALUE from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=?

SQL 语句也是十分的清晰直观:直接通过 3个条件 来获取配置信息

  • 1、application【应用名称】
  • 2、profile【配置环境】
  • 3、label【标签,版本】

执行 SQL 获取 keyvalue 的键值对。然后组装成 Environment 对象。

# 源码-Client 端如何获取配置信息

# 简单案例

官方文档介绍 Client 特性:config server 配置信息,填入 bootstrap.properties 中。

bootstrap.properteis 配置如下

# 访问 url
spring.cloud.config.uri = http://localhost:28080
# 配置文件名称
spring.cloud.config.name= config-client
# 配置文件环境
spring.cloud.config.profile =   dev

在引入 spring-cloud-starter-config client 的依赖之后,添加上面的两个配置,即可完成配置文件的读取。

在上面提到了的 EnvironmentController 提供的某个API 为 http://{ip}:{port}/{name}/{profiles}。对比配置文件中的 uri,name,profile 就能够猜测得到 client 获取配置的方式

# 源码查看client具体操作

以 client 端的自动配置类 ConfigServiceBootstrapConfiguration 为入口进行分析

Config Client 自动配置类 ConfigServiceBootstrapConfiguration 代码如下

@Configuration
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration {
	@Autowired
	private ConfigurableEnvironment environment;

	@Bean
	public ConfigClientProperties configClientProperties() {
		ConfigClientProperties client = new ConfigClientProperties(this.environment);
		return client;
	}

	@Bean
	@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
	@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
	public ConfigServicePropertySourceLocator configServicePropertySource(
			ConfigClientProperties properties) {
		ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
				properties);
		return locator;
	}
}

关注 ConfigServicePropertySourceLocator,代码如下:

单从字面意思能够猜测该类的功能是 资源定位加载

@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
	@Override
	@Retryable(interceptor = "configServerRetryInterceptor")
	public org.springframework.core.env.PropertySource<?> locate(org.springframework.core.env.Environment environment) {
		......
		CompositePropertySource composite = new CompositePropertySource("configService");
		RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties) : this.restTemplate;
		Environment result = getRemoteEnvironment(restTemplate, properties, label.trim(), state);
		composite.addFirstPropertySource( new MapPropertySource("configClient", map));
		......
		return composite;
	}

	private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties, String label, String state) {
		ResponseEntity<Environment> response = null;
		// uri = 配置文件中的 spring.cloud.config.uri 
		// path 拼接的 url:该实例为: /{name}/{profile}
		response = restTemplate.exchange(uri + path, HttpMethod.GET, entity, Environment.class, args);
		Environment result = response.getBody();
		return result;
	}
}

根据案例中给出的配置信息结合代码实现,整理逻辑流程如下:

  • 1、拼接 url :http://localhost:28080/{name}/{profile} 其中:name = config-client ,profile = default
  • 2、通过 RestTempalte 发起 HTTP 请求,获取的响应数据 Environment

结论:Spring Cloud Config Client 端,通过配置的 URL 地址,拼接 HTTP 请求,以 API 的方式,直接从 Server 端获取配置信息。

# 扩展:客户端如何加载配置中心配置?

友链:Spring Boot run 直接启动应用

通过工具(如:IDEA) 查找 ConfigServicePropertySourceLocator 在那里被调用,整理出调用 UML

# ③ SpringApplication#applyInitializers

代码如下

public class SpringApplication {
	protected void applyInitializers(ConfigurableApplicationContext context) {
		for (ApplicationContextInitializer initializer : getInitializers()) {
			Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
					initializer.getClass(), ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
			initializer.initialize(context);
		}
	}
}

代码循环遍历所有的 ApplicationContextInitializer 并调用其 initialize 方法。

# ④ PropertySourceBootstrapConfiguration#initialize

类图如下:

PropertySourceBootstrapConfiguration 是一个 ApplicationContextInitializer

来看看 PropertySourceBootstrapConfiguration#initialize 部分代码实现

@Configuration
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration ......{
	@Override
	public void initialize(ConfigurableApplicationContext applicationContext) {
		// ConfigServicePropertySourceLocator 配置文件拉取实现类
		for (PropertySourceLocator locator : this.propertySourceLocators) {
			source = locator.locate(environment);
		}
	}
}

在 Config client 中完成远程配置拉取的代码在 ConfigServicePropertySourceLocator#locate 。而该类实现了接口 PropertySourceLocator

# ⑤ ConfigServicePropertySourceLocator#locate

配置文件的拉取在该代码中实现

类图如下

关键代码如下

@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
	@Override
	@Retryable(interceptor = "configServerRetryInterceptor")
	public org.springframework.core.env.PropertySource<?> locate(
			org.springframework.core.env.Environment environment) {
		// 加载 RestTemplate 请求时需要的配置信息:如 URL ,已经请求参数等。
		ConfigClientProperties properties = this.defaultProperties.override(environment);
		Environment result = getRemoteEnvironment(restTemplate, properties,
						label.trim(), state);
	}

	private Environment getRemoteEnvironment(RestTemplate restTemplate,
			ConfigClientProperties properties, String label, String state) {
		// 拼接请求链接
		String path = "/{name}/{profile}";
		String name = properties.getName();
		String profile = properties.getProfile();
		......
		// HTTP 请求
		response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,Environment.class, args);
		Environment result = response.getBody();
		return result;
	}
}

上述代码很直观,直接读取配置文件的配置,然后根据配置拼接 url ,发起 HTTP 请求获取到配置。

# 扩展:Client 端配置需要配置在上下文 Bootstrap 中

回顾 ConfigServicePropertySourceLocator#locate 中的一段代码

public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
	public org.springframework.core.env.PropertySource<?> locate(
			org.springframework.core.env.Environment environment) {
        // 加载 RestTemplate 请求时需要的配置信息:如 URL ,已经请求参数等。
		ConfigClientProperties properties = this.defaultProperties.override(environment);
        ......
    }
}

上述代码逻辑:获取 client 端的配置信息,这些配置信息是发起对 Server 端 URL 请求的关键信息。

而重点就是配置类 ConfigClientProperties

# ConfigClientProperties 配置类

@ConfigurationProperties(ConfigClientProperties.PREFIX)
public class ConfigClientProperties {
	public static final String PREFIX = "spring.cloud.config";
    private String profile = "default";
    private String label;
    private String[] uri = { "http://localhost:8888" };
    @Value("${spring.application.name:application}")
	private String name;
    ......
}

该配置主要加载的是:spring.cloud.config 为前缀的配置信息。

# ConfigServiceBootstrapConfiguration 加载 ConfigClientProperties 配置类

代码如下:

@Configuration
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration {
    @Autowired
	private ConfigurableEnvironment environment;

	@Bean
	public ConfigClientProperties configClientProperties() {
        // 设置 profile 值
		ConfigClientProperties client = new ConfigClientProperties(this.environment);
		return client;
	}
    
    @Bean
	@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
	@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
	public ConfigServicePropertySourceLocator configServicePropertySource(
			ConfigClientProperties properties) {
		ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
				properties);
		return locator;
	}
}

由于配置加载优先级的原因,此时 ConfigurableEnvironment 中只加载了 bootstrap.properties 中的配置信息,而 application.properties 的配置信息未加载。换言之,如果此时 spring cloud client 的相关配置配置在了application.properteis 中,便无法读取到相关的配置信息,所有 spring cloud client 的相关配置需要配置在 bootstrap.properteis 。

# 总结

# Spring Cloud Config Server 端

提供多种配置仓储实现:SVN、Git、JDBC、zk等。默认:Git 仓储实现。 同时通过 EnvironmentController 对外提供一整套完整的 API供客户端调用。

# Spring Cloud Config Client 端

通过 Spring Boot 启动运行过程中,context 上下文初始化的过程中,根据相关的配置,从 Server 端拉取配置信息 Environment

精彩内容推送,请关注公众号!
最近更新时间: 4/8/2020, 10:26:23 PM