spring cloud config 工作原理
1. spring cloud config 测试案例
说明:为方便起见,这里使用本地文件(native)作数据后端,Spring cloud config 支持的数据后端参考:EnvironmentRepositoryConfiguration
1.0 spring cloud config server
启动类:ConfigServerApplication
// 开启Spring Config Server 功能,主要为引入Server自动化配置开启服务器功能:ConfigServerAutoConfiguration
// 对外开放http功能(ConfigServerMvcConfiguration):EnvironmentController、ResourceController
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
配置服务器配置:bootstrap.yml
server:
port: 8080
spring:
application:
name: local-config-server
profiles:
# NativeRepositoryConfiguration 加载本地环境(git/subversion/jdbc等)
active: native
cloud:
config:
server:
native:
# 注意: 新版本本地文件目录结尾要加 /
search-locations: classpath:/configdata/
# 版本号
version: V1.0.0
# label 缺省默认配置目录
# default-label: common
样例测试文件:classpath:/configdata/
application-common.properties:
common.config=application common config
server.port=8088
application-private.properties:
private.config=application private config
server.port=8089
项目结构图:
1.1 spring cloud config client
启动类:
@SpringBootApplication
public class ConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
}
控制器:
@RestController
public class ConfigController {
@Autowired
private Environment environment;
@Value("${common.config:common}")
private String commonConfig;
@Value("${private.config:private}")
private String privateConfig;
@GetMapping("/common")
public String commonConfig(){
return commonConfig;
}
@GetMapping("/private")
public String privateConfig(){
return privateConfig;
}
@GetMapping("/getConfig/{env}")
public String getConfig(@PathVariable("env") String env){
return environment.getProperty(env);
}
}
配置文件:bootstrap.yml
spring:
application:
name: local-config-client
cloud:
config:
# 配置名称:自定义 > ${spring.application.name} > application(优先级顺序)
name: ${spring.application.name}
# ConfigClientProperties构造时默认spring.profiles.active
# 注意:后面覆盖前面的配置
profile: common,private
# 标签:git 默认master, file: 指定项目配置目录,一般默认项目名,存在服务器配置目录根目录下
label: ${spring.application.name}-config
## 配置中心服务器地址
uri: http://localhost:8080
1.2 测试效果
spring cloud config server直接访问: http://localhost:8080/local-config-client/common,private/local-config-client-config
{
"name": "local-config-client",
"profiles": [
"common,private"
],
"label": "local-config-client-config",
"version": "V1.0.0",
"state": null,
"propertySources": [
{
"name": "classpath:/configdata/local-config-client-config/application-private.properties",
"source": {
"private.config": "application private config",
"server.port": "8089"
}
},
{
"name": "classpath:/configdata/application-common.properties",
"source": {
"common.config": "application common config",
"server.port": "8088"
}
}
]
}
spring cloud config client 访问:
私有配置访问:
GET http://localhost:8089/private
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 26
Date: Mon, 27 Jul 2020 00:59:19 GMT
application private config
公共配置访问:
GET http://localhost:8089/common
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 25
Date: Mon, 27 Jul 2020 01:00:19 GMT
application common config
Response code: 200; Time: 24ms; Content length: 25 bytes
重叠配置访问:取决于profiles顺序,后面优先级 > 前面,底层使用LinkedHashSet保证有序
GET http://localhost:8089/getConfig/server.port
HTTP/1.1 200
Content-Disposition: inline;filename=f.txt
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Mon, 27 Jul 2020 01:01:18 GMT
8089
2. spring cloud config client 工作原理
原理:PropertySourceLocator:spring cloud扩展属性源,用于自定义加载环境属性源,常用配置中心基本都是基于PropertySourceLocator实现,例如:NACOS, 详见博客:nacos 分布式配置中心工作原理源码分析
ConfigServicePropertySourceLocator: spring cloud client属性源加载器,本质通过Http请求从配置服务器获取
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
private static Log logger = LogFactory.getLog(ConfigServicePropertySourceLocator.class);
private RestTemplate restTemplate;
private ConfigClientProperties defaultProperties;
// ConfigServicePropertySourceLocator 使用 ConfigClientProperties 客户端属性初始化
public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
this.defaultProperties = defaultProperties;
}
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(org.springframework.core.env.Environment environment) {
// 配置覆盖:spring cloud config client私有配置由于spring boot全局配置(name:应用名、profile:生效环境、label: 标签)
ConfigClientProperties properties = this.defaultProperties.override(environment);
// 创建组合属性源
CompositePropertySource composite = new OriginTrackedCompositePropertySource("configService");
// 创建Http客户端RestTemplate, 支持用户名/密码(Http Basic认证)、Token访问,账户信息配置在ConfigClientProperties
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties) : this.restTemplate;
Exception error = null;
String errorBody = null;
try {
String[] labels = new String[] { "" };
if (StringUtils.hasText(properties.getLabel())) {
labels = StringUtils
.commaDelimitedListToStringArray(properties.getLabel());
}
// 获取状态信息,主要用于存储当前配置的状态,用于后续版本匹配
String state = ConfigClientStateHolder.getState();
// Try all the labels until one works
for (String label : labels) {
// 从spring cloud config server 获取最新配置信息
Environment result = getRemoteEnvironment(restTemplate, properties, label.trim(), state);
if (result != null) {
log(result); // 服务端响应配置信息打印
// result.getPropertySources() can be null if using xml
if (result.getPropertySources() != null) {
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
// 进行Origin值对象处理,满足条件则保存现值和原始值
Map<String, Object> map = translateOrigins(source.getName(),(Map<String, Object>) source.getSource());
composite.addPropertySource(new OriginTrackedMapPropertySource(source.getName(),map));
}
}
// 若服务端有访问state(Vault 配置存储)或version(SCM 系统数据存储,例如:git、svn等)字段,则新增PropertySource,用于维护这2种状态
if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
composite.addFirstPropertySource(new MapPropertySource("configClient", map));
}
return composite;
}
}
errorBody = String.format("None of labels %s found", Arrays.toString(labels));
}
catch (HttpServerErrorException e) {
error = e;
if (MediaType.APPLICATION_JSON
.includes(e.getResponseHeaders().getContentType())) {
errorBody = e.getResponseBodyAsString();
}
}
catch (Exception e) {
error = e;
}
if (properties.isFailFast()) {
throw new IllegalStateException(
"Could not locate PropertySource and the fail fast property is set, failing"
+ (errorBody == null ? "" : ": " + errorBody),
error);
}
logger.warn("Could not locate PropertySource: "
+ (error != null ? error.getMessage() : errorBody));
return null;
}
private void log(Environment result) {
if (logger.isInfoEnabled()) {
logger.info(String.format(
"Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",
result.getName(),
result.getProfiles() == null ? ""
: Arrays.asList(result.getProfiles()),
result.getLabel(), result.getVersion(), result.getState()));
}
if (logger.isDebugEnabled()) {
List<PropertySource> propertySourceList = result.getPropertySources();
if (propertySourceList != null) {
int propertyCount = 0;
for (PropertySource propertySource : propertySourceList) {
propertyCount += propertySource.getSource().size();
}
logger.debug(String.format(
"Environment %s has %d property sources with %d properties.",
result.getName(), result.getPropertySources().size(),
propertyCount));
}
}
}
private Map<String, Object> translateOrigins(String name, Map<String, Object> source) {
Map<String, Object> withOrigins = new HashMap<>();
for (Map.Entry<String, Object> entry : source.entrySet()) {
boolean hasOrigin = false;
if (entry.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> value = (Map<String, Object>) entry.getValue();
if (value.size() == 2 && value.containsKey("origin") && value.containsKey("value")) {
Origin origin = new ConfigServiceOrigin(name, value.get("origin"));
OriginTrackedValue trackedValue = OriginTrackedValue.of(value.get("value"), origin);
withOrigins.put(entry.getKey(), trackedValue);
hasOrigin = true;
}
}
if (!hasOrigin) {
withOrigins.put(entry.getKey(), entry.getValue());
}
}
return withOrigins;
}
private void putValue(HashMap<String, Object> map, String key, String value) {
if (StringUtils.hasText(value)) {
map.put(key, value);
}
}
// 重点: 从 spring cloud config server 获取配置
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();
//设置请求头对应spring cloud config server: EnvironmentController.labelledIncludeOrigin
headers.setAccept(Collections.singletonList(MediaType.parseMediaType(V2_JSON)));
// 若spring cloud config server 需安全认证,用户信息保存在请求头中
addAuthorizationToken(properties, headers, username, password);
if (StringUtils.hasText(token)) {
// 请求头添加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);
// 发送Http请求获取配置:http://localhost:8080/local-config-client/common,private/local-config-client-config
response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,Environment.class, args);
}
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;
}
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// 创建Http客户端(RestTemplate)获取配置数据
private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
if (client.getRequestReadTimeout() < 0) {
throw new IllegalStateException("Invalid Value for Read Timeout set.");
}
if (client.getRequestConnectTimeout() < 0) {
throw new IllegalStateException("Invalid Value for Connect Timeout set.");
}
requestFactory.setReadTimeout(client.getRequestReadTimeout());
requestFactory.setConnectTimeout(client.getRequestConnectTimeout());
RestTemplate template = new RestTemplate(requestFactory);
Map<String, String> headers = new HashMap<>(client.getHeaders());
if (headers.containsKey(AUTHORIZATION)) {
headers.remove(AUTHORIZATION); // To avoid redundant addition of header
}
// 自定义 RestTemplate 拦截器,这里主要给Http请求额外增加自定义请求头信息
if (!headers.isEmpty()) {
template.setInterceptors(Arrays.<ClientHttpRequestInterceptor>asList(new GenericRequestHeaderInterceptor(headers)));
}
return template;
}
// 添加认证信息:支持Http Basic认证和Token认证(Authorization)
private void addAuthorizationToken(ConfigClientProperties configClientProperties,
HttpHeaders httpHeaders, String username, String password) {
String authorization = configClientProperties.getHeaders().get(AUTHORIZATION);
if (password != null && authorization != null) {
throw new IllegalStateException(
"You must set either 'password' or 'authorization'");
}
if (password != null) {
byte[] token = Base64Utils.encode((username + ":" + password).getBytes());
httpHeaders.add("Authorization", "Basic " + new String(token));
}
else if (authorization != null) {
httpHeaders.add("Authorization", authorization);
}
}
/**
* Adds the provided headers to the request.
* RestTemplate 自定义拦截器,主要用于请求添加请求头信息,工作原理类似J2EE中的过滤器:Filter
*/
public static class GenericRequestHeaderInterceptor implements ClientHttpRequestInterceptor {
private final Map<String, String> headers;
public GenericRequestHeaderInterceptor(Map<String, String> headers) {
this.headers = headers;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
for (Entry<String, String> header : this.headers.entrySet()) {
request.getHeaders().add(header.getKey(), header.getValue());
}
return execution.execute(request, body);
}
protected Map<String, String> getHeaders() {
return this.headers;
}
}
static class ConfigServiceOrigin implements Origin {
private final String remotePropertySource;
private final Object origin;
ConfigServiceOrigin(String remotePropertySource, Object origin) {
this.remotePropertySource = remotePropertySource;
Assert.notNull(origin, "origin may not be null");
this.origin = origin;
}
@Override
public String toString() {
return "Config Server " + this.remotePropertySource + ":"
+ this.origin.toString();
}
}
}
2.1 spring cloud config client 补充内容
自动配置:ConfigClientAutoConfiguration
ConfigServerHealthIndicator: 配置服务器检查指示器
org.springframework.cloud.config.client.ConfigServerHealthIndicator.doHealthCheck
@Override
protected void doHealthCheck(Builder builder) throws Exception {
// 本质调用 ConfigServicePropertySourceLocator 从新拉取一下远程配置旁段检查状态
PropertySource<?> propertySource = getPropertySource();
builder.up();
if (propertySource instanceof CompositePropertySource) {
List<String> sources = new ArrayList<>();
for (PropertySource<?> ps : ((CompositePropertySource) propertySource)
.getPropertySources()) {
sources.add(ps.getName());
}
builder.withDetail("propertySources", sources);
}
else if (propertySource != null) {
builder.withDetail("propertySources", propertySource.toString());
}
else {
builder.unknown().withDetail("error", "no property sources located");
}
}
ConfigClientWatch: 客户端监视器,定时检查State是否更新,若更新则通过ContextRefresher更新配置
public class ConfigClientWatch implements Closeable, EnvironmentAware {
private static Log log = LogFactory.getLog(ConfigServicePropertySourceLocator.class);
private final AtomicBoolean running = new AtomicBoolean(false);
private final ContextRefresher refresher;
private Environment environment;
public ConfigClientWatch(ContextRefresher refresher) {
this.refresher = refresher;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@PostConstruct
public void start() {
this.running.compareAndSet(false, true);
}
// 定时任务刷新,主要要开启定时任务需要借助@EnableScheduling开启@Scheduled扫描,定时spring task才生效
@Scheduled(initialDelayString = "${spring.cloud.config.watch.initialDelay:180000}", fixedDelayString = "${spring.cloud.config.watch.delay:500}")
public void watchConfigServer() {
if (this.running.get()) {
// 获取最新配置状态
String newState = this.environment.getProperty("config.client.state");
// 线程上下文持有状态:旧状态
String oldState = ConfigClientStateHolder.getState();
//状态不一致说明配置更新,则ContextRefresher刷新环境:刷新配置 + 刷新RefreshScope Bean,注意,要想动态刷新,组件必须声明 @RefreshScope,否则只更新Environment不更新Bean
if (stateChanged(oldState, newState)) {
ConfigClientStateHolder.setState(newState);
this.refresher.refresh();
}
}
}
/* for testing */ boolean stateChanged(String oldState, String newState) {
return (!hasText(oldState) && hasText(newState)) || (hasText(oldState) && !oldState.equals(newState));
}
@Override
public void close() {
this.running.compareAndSet(true, false);
}
}
特别注意:ConfigClientWatch 检测environment.getProperty("config.client.state")与ConfigClientStateHolder存储值是否相同来判断配置是否更新,但是在native、git 不借助spring cloud bus 客户端无法检测服务端配置更新,根本无法更新本地Environment中的state,因此ConfigClientWatch在次环境基本无效,官方声明该组件目前只适用于Vault作为配置数据后端存储
3. spring cloud config server 工作原理
3.1 获取Spring cloud config client 环境配置基础流程
接口:org.springframework.cloud.config.server.environment.EnvironmentController.getEnvironment
public Environment getEnvironment(String name, String profiles, String label, boolean includeOrigin) {
//spring clond config client: name、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("(_)", "/");
}
//org.springframework.cloud.config.server.config.ConfigServerMvcConfiguration.environmentController
// repository: EnvironmentEncryptorEnvironmentRepository装饰 SearchPathCompositeEnvironmentRepository, 额外增加配置解密功能
// SearchPathCompositeEnvironmentRepository是 CompositeEnvironmentRepository 子类,这里是Native则维护 NativeEnvironmentRepository
Environment environment = this.repository.findOne(name, profiles, label, includeOrigin);
if (!this.acceptEmpty && (environment == null || environment.getPropertySources().isEmpty())) {
throw new EnvironmentNotFoundException("Profile Not found");
}
return environment;
}
3.2 获取配置 SearchPathCompositeEnvironmentRepository 父类 CompositeEnvironmentRepository
org.springframework.cloud.config.server.environment.CompositeEnvironmentRepository.findOne
@Override
public Environment findOne(String application, String profile, String label, boolean includeOrigin) {
Environment env = new Environment(application, new String[] { profile }, label, null, null);
// 一般只有1个后端作为配置数据存储
if (this.environmentRepositories.size() == 1) {
// 调用内部维护EnvironmentRepository获取真正的配置:NativeEnvironmentRepository
Environment envRepo = this.environmentRepositories.get(0).findOne(application,profile, label, includeOrigin);
env.addAll(envRepo.getPropertySources());
env.setVersion(envRepo.getVersion());
env.setState(envRepo.getState());
}
else {
for (EnvironmentRepository repo : environmentRepositories) {
env.addAll(repo.findOne(application, profile, label, includeOrigin).getPropertySources());
}
}
return env;
}
3.3 底层获取配置逻辑: NativeEnvironmentRepository
接口: org.springframework.cloud.config.server.environment.NativeEnvironmentRepository.findOne
工作原理:采用 SpringApplicationBuilder 采用配置路径根路径和标签路径(根路径/标签名),以WebApplicationType.NONE形式启动,相当于重新构建并启动SpringApplication, 但是新启动的IOC容器不对外提供服务,相当于是沙箱内运行的容器,这里主要借助SpringApplication更好进行环境参数处理,类似操作逻辑参考:ContextRefresher.addConfigFilesToEnvironment
public Environment findOne(String config, String profile, String label, boolean includeOrigin) {
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);
}
// SpringApplication 启动参数配置
//--spring.config.name 应用名(sringcloud config 客户端应用名)
//--spring.cloud.bootstrap.enabled=false: 取消 bootstrap.yaml 配置引入
//--spring.config.location: 指定SpringApplication启动配置目录: 配置根目录、配置目录/标签目录 列表
String[] args = getArgs(config, profile, label);
//配置 ConfigFileApplicationListener 用于 SpringApplication 加载配置文件
builder.application().setListeners(Arrays.asList(new ConfigFileApplicationListener()));
// SpringApplication 启动
try (ConfigurableApplicationContext context = builder.run(args)) {
// 清除spring cloud config client 相关属性源配置:spring.main.web-application-type、spring.main.web-application-type
environment.getPropertySources().remove("profiles");
// 清理spring自身属性源配置,只保留spring cloud config client自身相关配置
// PassthruEnvironmentRepository.findOne:去除spring boot相关配置:PassthruEnvironmentRepository.standardSources
// 将 org.springframework.core.env.PropertySource 转为 org.springframework.cloud.config.environment.PropertySource 封装到
// org.springframework.cloud.config.environment.Environment 返回
return clean(new PassthruEnvironmentRepository(environment).findOne(config, profile, label, includeOrigin));
}
catch (Exception e) {
String msg = String.format(
"Could not construct context for config=%s profile=%s label=%s includeOrigin=%b",
config, profile, label, includeOrigin);
String completeMessage = NestedExceptionUtils.buildMessage(msg,
NestedExceptionUtils.getMostSpecificCause(e));
throw new FailedToConstructEnvironmentException(completeMessage, e);
}
}
至此spring cloud config client/server 工作原理分析完成
来源:oschina
链接:https://my.oschina.net/yangzhiwei256/blog/4438095