盘点 Cloud SpringConfig 原理 Git

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

作为开源框架 , springConfig 有很多功能是不适合业务场景的 , 所以我们需要通过定制重写来达到自己的业务需求的 , 这一篇就来看看 , 如果定制 SpringCloud Config 模块

二. 原理分析

2.1 处理入口

SpringCloudConfig 的入口是 EnvironmentController , 当我们输入以下地址时 , 会进行对应的处理 :

1
2
3
4
5
java复制代码/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

对应处理接口 -> EnvironmentController

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 以上方式分别对应如下接口 : 
@RequestMapping(path = "/{name}/{profiles:.*[^-].*}", produces = MediaType.APPLICATION_JSON_VALUE)
public Environment defaultLabel(@PathVariable String name, @PathVariable String profiles)

@RequestMapping(path = "/{name}/{profiles:.*[^-].*}", produces = EnvironmentMediaType.V2_JSON)
public Environment defaultLabelIncludeOrigin(@PathVariable String name, @PathVariable String profiles)

@RequestMapping(path = "/{name}/{profiles}/{label:.*}", produces = MediaType.APPLICATION_JSON_VALUE)
public Environment labelled(@PathVariable String name, @PathVariable String profiles, @PathVariable String label)

@RequestMapping(path = "/{name}/{profiles}/{label:.*}", produces = EnvironmentMediaType.V2_JSON)
public Environment labelledIncludeOrigin(@PathVariable String name, @PathVariable String profiles,@PathVariable String label)

主入口逻辑 : getEnvironment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public Environment getEnvironment(String name, String profiles, String label, boolean includeOrigin) {

// 就如方法名 ,此处是对参数格式化/规范化
name = normalize(name);
label = normalize(label);

// 核心逻辑 , 查找对应的配置
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;
}
}

2.2 处理逻辑

处理逻辑主要在 EnvironmentEncryptorEnvironmentRepository 中进行 , 这也是我们的核心定制类

这里先来看一下整体的查询体系 :

Config-EnvironmentRepository.png

2.2.1 delegate 查找配置

注意 ,此处的delegate是可以通过自动装配改写的 ,这也意味着我们可以去实现不同的配置方式!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// C- 
public Environment findOne(String name, String profiles, String label, boolean includeOrigin) {

// 核心查询点 , 也是定制点
Environment environment = this.delegate.findOne(name, profiles, label, includeOrigin);
if (this.environmentEncryptors != null) {

for (EnvironmentEncryptor environmentEncryptor : environmentEncryptors) {
// 对配置解码
environment = environmentEncryptor.decrypt(environment);
}
}
return environment;
}

2.2.2 Git 处理流程

对应 Git 查询使用的是 MultipleJGitEnvironmentRepository , 当然还有几个其他的 , 这里先不深入 :

PS : 此处依次调用了多个 Repository , 最终是调用父类 AbstractScmEnvironmentRepository 获取 Locations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码//C- AbstractScmEnvironmentRepository
public synchronized Environment findOne(String application, String profile, String label, boolean includeOrigin) {

NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(),
new NativeEnvironmentProperties());

// 获取 Location , 在此环节拉取 Git 到本地 -> 详见 2.3
Locations locations = getLocations(application, profile, label);
delegate.setSearchLocations(locations.getLocations());

// 调用上面的 native 继续处理 -> NativeEnvironmentRepository -> 2.2.3
Environment result = delegate.findOne(application, profile, "", includeOrigin);

result.setVersion(locations.getVersion());
result.setLabel(label);

return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(), getUri());
}

2.3 Git 文件的下载

Git 文件的下载其实不麻烦 , 其主要也是使用工具类 , 以下有相关的使用 : Git Plugin

1
2
3
4
5
java复制代码    <dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>5.1.3.201810200350-r</version>
</dependency>

JGitEnvironmentRepository # getLocation 环节中 , 存在一个 refresh 操作

2.3.1 refresh git 主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public String refresh(String label) {
Git git = null;

// 拉取 Git 文件
git = createGitClient();

// 断是否需要 pull 拉取
if (shouldPull(git)) {
FetchResult fetchStatus = fetch(git, label);
if (this.deleteUntrackedBranches && fetchStatus != null) {
deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git);
}
}

checkout(git, label);
tryMerge(git, label);

return git.getRepository().findRef("HEAD").getObjectId().getName();

}

2.3.2 拉取 Git 文件

1
2
3
4
5
6
7
8
9
10
java复制代码
// Step 1 : createGitClient 流程
在这个流程中主要2个逻辑 , 判断路径是否存在 .git , 分别调用 :
- openGitRepository :
- copyRepository :

// Step 2 : Git 核心拉取流程
- copyRepository -> cloneToBasedir : 从远端 clone 项目
- cloneToBasedir -> getCloneCommandByCloneRepository : 获取 clone 命令
- cloneToBasedir -> clone.call() : 完成 clone 流程

到了这里已经在本地下载到了本地 , 后面就是读取了

2.2.3 NativeEnvironmentRepository 的处理

在上文获取完 Location 后 , 会继续进行 delegate.findOne 进行链式调用 , 这里会进行如下调用 :

  • C- NativeEnvironmentRepository # findOne
  • C- ConfigDataEnvironmentPostProcessor # applyTo : 调用 EnvironmentPostProcessor 进行处理
  • C- ConfigDataEnvironmentPostProcessor # postProcessEnvironment
  • C- ConfigDataEnvironmentPostProcessor # getConfigDataEnvironment : 构建 ConfigDataEnvironment
1
2
3
4
5
6
7
8
java复制代码ConfigDataEnvironment getConfigDataEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
Collection<String> additionalProfiles) {
return new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, environment, resourceLoader,
additionalProfiles, this.environmentUpdateListener);
}

// PS : 核心 , 在构造器中构建了ConfigDataLocationResolvers
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);

2.4 文件的扫描

Git 获取数据分为2步 :

  • Step 1 : 从远程拉取配置到本地
  • Step 2 : 从本地读取配置

2.4.1 扫描主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码// C- ConfigDataEnvironmentContributors
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
ConfigDataActivationContext activationContext) {

// ImportPhase 是一个枚举 , 包含 BEFORE_PROFILE_ACTIVATION 和 AFTER_PROFILE_ACTIVATION 2 个属性
// BEFORE_PROFILE_ACTIVATION : 启动配置文件之前的阶段
ImportPhase importPhase = ImportPhase.get(activationContext);

ConfigDataEnvironmentContributors result = this;
int processed = 0;

// 死循环处理 , 直到路径文件其全部处理完成
while (true) {
// ConfigDataProperties 中包含一个 Location 对象
ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
if (contributor == null) {
return result;
}
if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
Iterable<ConfigurationPropertySource> sources = Collections
.singleton(contributor.getConfigurationPropertySource());
PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
result, activationContext, true);
Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
result.getRoot().withReplacement(contributor, bound));
continue;
}
ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
result, contributor, activationContext);

// 准备 Loader 容器
ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);

// 获取全部 Location 列表
List<ConfigDataLocation> imports = contributor.getImports();

// 核心逻辑 >>> 进行 resolver 处理 , 最终调用 2.3.2 resolve
Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
locationResolverContext, loaderContext, imports);

ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
asContributors(imported));
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
result.getRoot().withReplacement(contributor, contributorAndChildren));
processed++;
}
}

2.4.2 resolve 解析路径

1
2
3
4
5
6
7
8
9
10
java复制代码// C- ConfigDataImporter # resolveAndLoad : 在这个方法中主要分为核心的三个步骤 

// Step 1 : 获取 Profiles 信息
Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;

// Step 2 : resolved 解析路径
List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);

// Step 3 : load 加载为 Map<ConfigDataResolutionResult, ConfigData>
return load(loaderContext, resolved);

resolved 的过程也有点绕 ,推荐看图 , 这里列出主要逻辑 :

Step 2-1 : resolved 循环 location

1
2
3
4
5
6
7
8
9
java复制代码private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext,
Profiles profiles, List<ConfigDataLocation> locations) {
List<ConfigDataResolutionResult> resolved = new ArrayList<>(locations.size());
// 遍历所有的 location
for (ConfigDataLocation location : locations) {
resolved.addAll(resolve(locationResolverContext, profiles, location));
}
return Collections.unmodifiableList(resolved);
}

Step 2-2 : 循环其他 resolve

这里是对多种格式的资源解析处理

1
2
3
4
5
6
7
8
9
java复制代码List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location,
Profiles profiles) {
// 循环且判断能处理
for (ConfigDataLocationResolver<?> resolver : getResolvers()) {
if (resolver.isResolvable(context, location)) {
return resolve(resolver, context, location, profiles);
}
}
}

Step 2-3 : getReference 后 循环所有资源

1
2
3
4
5
6
7
8
9
10
java复制代码// C- StandardConfigDataLocationResolver
private List<StandardConfigDataResource> resolve(Set<StandardConfigDataReference> references) {
List<StandardConfigDataResource> resolved = new ArrayList<>();

// reference 对 reference 进行循环
for (StandardConfigDataReference reference : references) {
resolved.addAll(resolve(reference));
}
return resolved;
}

image.png

image.png

Step 2-4 : 循环所有的 ConfigDataResource

1
2
3
4
5
6
7
8
9
10
11
java复制代码private List<ConfigDataResolutionResult> resolve(ConfigDataLocation location, boolean profileSpecific,
Supplier<List<? extends ConfigDataResource>> resolveAction) {
// 注意 , 2-3 在这环节发生 , 此时已经拿到了最终的资源
List<ConfigDataResource> resources = nonNullList(resolveAction.get());
List<ConfigDataResolutionResult> resolved = new ArrayList<>(resources.size());

for (ConfigDataResource resource : resources) {
resolved.add(new ConfigDataResolutionResult(location, resource, profileSpecific));
}
return resolved;
}

image.png

2.5 load 加载流程

2.5.1 load 主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码// C- ConfigDataImporter
private Map<ConfigDataResolutionResult, ConfigData> load(ConfigDataLoaderContext loaderContext,
List<ConfigDataResolutionResult> candidates) throws IOException {
Map<ConfigDataResolutionResult, ConfigData> result = new LinkedHashMap<>();

// 对所有的 candidates 解析循环
for (int i = candidates.size() - 1; i >= 0; i--) {
ConfigDataResolutionResult candidate = candidates.get(i);

// 获取 location
ConfigDataLocation location = candidate.getLocation();
// 获取所有的 Resource 资源
ConfigDataResource resource = candidate.getResource();
if (this.loaded.add(resource)) {
try {
// 调用 loaders 对 resource 进行加载
ConfigData loaded = this.loaders.load(loaderContext, resource);
if (loaded != null) {
// 这里会统一放在一个 map 中
result.put(candidate, loaded);
}
}
catch (ConfigDataNotFoundException ex) {
handle(ex, location);
}
}
}
return Collections.unmodifiableMap(result);
}

!!!!!! 我这里被坑的很惨 , 一个简单的案例怎么都加载不出来 , 有兴趣的可以看我下面的图猜一下为什么,很重要的一点!!!

image.png

2.5.2 loaders 加载

此处的家长对象主要为 ConfigDataLoaders , 最终调用对象为 StandardConfigDataLoader

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public ConfigData load(ConfigDataLoaderContext context, StandardConfigDataResource resource)
throws IOException, ConfigDataNotFoundException {

// 省略为空及不存在 Reource 抛出异常
StandardConfigDataReference reference = resource.getReference();
Resource originTrackedResource = OriginTrackedResource.of(resource.getResource(),
Origin.from(reference.getConfigDataLocation()));
String name = String.format("Config resource '%s' via location '%s'", resource,
reference.getConfigDataLocation());

// 最终通过 YamlPropertySourceLoader 对 YAML 文件进行加载
List<PropertySource<?>> propertySources = reference.getPropertySourceLoader().load(name, originTrackedResource);
return new ConfigData(propertySources);
}

最终加载出来的 PropertySources

image.png

到了这里属性就正式被加载完成

总结

这一篇已经写的很长了 , 单纯点好 , 所以 Native 和属性的使用在后面再看看 , 贡献一个流程图

Config-git.jpg

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%