一直對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進行操作。
加油!好好學習天天向上!