盘点 Cloud : SpringConfig 原理 Git 环节

521 阅读5分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

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

二. 原理分析

2.1 处理入口

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

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

对应处理接口 -> EnvironmentController

// 以上方式分别对应如下接口 : 
@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

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是可以通过自动装配改写的 ,这也意味着我们可以去实现不同的配置方式!

// 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

//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

    <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 主流程

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 文件


// 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
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 扫描主流程

// 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 解析路径

// 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

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

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

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 后 循环所有资源

// 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

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 主流程

// 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

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