一直对spring cloud config是如何从git中加载到配置文件的挺感兴趣的,正好最近不是很忙来跟踪个代码简单研究学习下
对于spring cloud config的教程和demo可以直接从spring的官网进行学习,https://spring.io/projects/spring-cloud-config
官方文档
我主要是看的这两个文档:
- https://cloud.spring.io/spring-cloud-config/reference/html/
- https://spring.io/guides/gs/centralized-configuration/
首先按照官方教程中搭建好一个spring cloud config的配置,如果要在config client端进行配置刷新的话,需要加入对于actuator的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
并开启refresh的endpoint,如要开启全部,将management的配置写为*即可
management: endpoints: web: exposure: include: "*"
源码探究
长话短说。
在spring cloud config client加载配置文件是通过访问config server的http接口来实现的。
通过输出的日志可以看到,其controller中的requestMapping地址为:
/{application}/{profile}[/{label}]
为了研究config server拉取配置文件的原理,我们研究下这个RequestMapping的代码即可
通过@EnableConfigServer注解找到:ConfigServerConfiguration.class->Marker->EnvironmentRepositoryConfiguration->
GitRepositoryConfiguration->DefaultRepositoryConfiguration->MultipleJGitEnvironmentRepository->org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository#findOne->org.springframework.cloud.config.server.environment.EnvironmentController
最后的EnvironmentController就是config server所提供出来的controller了,其上面描述的requestMapping地址就在这个contoller里面:
@RequestMapping("/{name}/{profiles:.*[^-].*}")
public Environment defaultLabel(@PathVariable String name,
@PathVariable String profiles) {
return labelled(name, profiles, null);
}
其最终会执行labelled方法:
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String 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("(_)", "/");
}
Environment environment = this.repository.findOne(name, profiles, label);
if (!this.acceptEmpty
&& (environment == null || environment.getPropertySources().isEmpty())) {
throw new EnvironmentNotFoundException("Profile Not found");
}
return environment;
}
然后又会执行this.repository.findOne方法,如果用的git做的环境配置文件,那么就会执行:org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository#findOne:
@Override
public Environment findOne(String application, String profile, String label) {
for (PatternMatchingJGitEnvironmentRepository repository : this.repos.values()) {
if (repository.matches(application, profile, label)) {
for (JGitEnvironmentRepository candidate : getRepositories(repository,
application, profile, label)) {
try {
if (label == null) {
label = candidate.getDefaultLabel();
}
Environment source = candidate.findOne(application, profile,
label);
if (source != null) {
return source;
}
}
catch (Exception e) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Cannot load configuration from " + candidate.getUri()
+ ", cause: (" + e.getClass().getSimpleName()
+ ") " + e.getMessage(),
e);
}
continue;
}
}
}
}
JGitEnvironmentRepository candidate = getRepository(this, application, profile,
label);
if (label == null) {
label = candidate.getDefaultLabel();
}
if (candidate == this) {
return super.findOne(application, profile, label);
}
return candidate.findOne(application, profile, label);
}
在findOne(application, profile, label)方法中的三个参数分别代表application的名称、profile的名称和代码分支的名称
从而进入org.springframework.cloud.config.server.environment.AbstractScmEnvironmentRepository#findOne方法:
@Override
public synchronized Environment findOne(String application, String profile,
String label) {
NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(
getEnvironment(), new NativeEnvironmentProperties());
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());
}
其中Locations locations = getLocations(application, profile, label);会执行org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository#getLocations,然后再执行org.springframework.cloud.config.server.environment.JGitEnvironmentRepository#getLocations方法:
@Override
public synchronized Locations getLocations(String application, String profile,
String label) {
if (label == null) {
label = this.defaultLabel;
}
String version = refresh(label);
return new Locations(application, profile, label, version,
getSearchLocations(getWorkingDirectory(), application, profile, label));
}
主要代码:
当看到这里的时候,进入到refresh里就基本能明白了:
/**
* Get the working directory ready.
* @param label label to refresh
* @return head id
*/
public String refresh(String label) {
Git git = null;
try {
git = createGitClient();
if (shouldPull(git)) {
FetchResult fetchStatus = fetch(git, label);
if (this.deleteUntrackedBranches && fetchStatus != null) {
deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(),
git);
}
// checkout after fetch so we can get any new branches, tags, ect.
checkout(git, label);
tryMerge(git, label);
}
else {
// nothing to update so just checkout and merge.
// Merge because remote branch could have been updated before
checkout(git, label);
tryMerge(git, label);
}
// always return what is currently HEAD as the version
return git.getRepository().findRef("HEAD").getObjectId().getName();
}
catch (RefNotFoundException e) {
throw new NoSuchLabelException("No such label: " + label, e);
}
catch (NoRemoteRepositoryException e) {
throw new NoSuchRepositoryException("No such repository: " + getUri(), e);
}
catch (GitAPIException e) {
throw new NoSuchRepositoryException(
"Cannot clone or checkout repository: " + getUri(), e);
}
catch (Exception e) {
throw new IllegalStateException("Cannot load environment", e);
}
finally {
try {
if (git != null) {
git.close();
}
}
catch (Exception e) {
this.logger.warn("Could not close git repository", e);
}
}
}
上面的代码中会先获取到一个git客户端,其git客户端的相关依赖在config server的pom依赖中能找到:
<dependency> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit</artifactId> </dependency>
拿到git客户端后会执行一个判断shouldPull方法,判断是否需要进入拉取远程的git库:
protected boolean shouldPull(Git git) throws GitAPIException {
boolean shouldPull;
if (this.refreshRate > 0 && System.currentTimeMillis()
- this.lastRefresh < (this.refreshRate * 1000)) {
return false;
}
Status gitStatus = git.status().call();
boolean isWorkingTreeClean = gitStatus.isClean();
String originUrl = git.getRepository().getConfig().getString("remote", "origin",
"url");
if (this.forcePull && !isWorkingTreeClean) {
shouldPull = true;
logDirty(gitStatus);
}
else {
shouldPull = isWorkingTreeClean && originUrl != null;
}
if (!isWorkingTreeClean && !this.forcePull) {
this.logger.info("Cannot pull from remote " + originUrl
+ ", the working tree is not clean.");
}
return shouldPull;
}
从代码可以看出,其先获取refreshRate判断是否在刷新周期内,这个refreshRate也就是config server提供出来让我们可以进行配置的,默认情况下refreshRate的值为0
然后会执行一个git status命令判断是否远程仓库有更新
如果需要进行更新的话,就将执行fetch命令、checkout命令并尝试执行merge命令将远程仓库的代码进行合并,然后再进行一系列的解析操作。
对于后面的具体的解析处理操作由于时间关系这里就不深入探索研究了,后面遇到问题时再进行查看。
总结:
通过此次的源码探究揭开了config server是如何加载远程仓库的神秘面纱,原来config server也并不是想象之中的那么神秘。
同时,知道了在java里有jgit这个客户端可以对git进行操作。
加油!好好学习天天向上!