1、Config Server的任务
- 从环境仓库中拉取配置信息到本地
- 对外提供服务接口
所以我们核心关注如何拉去配置信息并且如何对外暴露接口的。
2、如何拉取配置信息到本地?
- 使用
@EnableConfigServer
注解成为配置中心,发现导入了配置类ConfigServerConfiguration
;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigServerConfiguration.class)
public @interface EnableConfigServer {
}
ConfigServerConfiguration
中装配了Marker
Bean(这个实例化Marker的操作,Zuul也是这样做的),Marker
是开启 Spring Cloud Config Server 自动配置类的开关;
@Configuration
public class ConfigServerConfiguration {
@Bean
public Marker enableConfigServerMarker() {
return new Marker();
}
class Marker {
}
}
- 我们在spring-cloud-config-server-2.1.8.RELEASE.jar中的spring.factories中查看自动配置的类,我们先看
ConfigServerAutoConfiguration
ConfigServerAutoConfiguration
,这个自动配置类中引入了其他很多配置类。
@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class,
ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class,
ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {
}
类名 | 备注 |
---|---|
EnvironmentRepositoryConfiguration | 环境存储配置类 (最重要的类) |
CompositeConfiguration | 复合配置,即组合方式的环境配置类 |
ResourceRepositoryConfiguration | 资源仓库的配置类 |
ConfigServerEncryptionConfiguration | 加密断点的配置类(config自带加密) |
ConfigServerMvcConfigurationConfigServerMvcConfiguration | 对外暴露的MVC端点控制器的配置类(第二重要) |
2.1 EnvironmentRepositoryConfiguration
2.1.1 @EnableConfigurationProperties
注解引入了各种properties文件,并导入了众多环境(git/jdbc/native/svn/vault),相关的仓库配置类为默认bean,这些bean就是我们常用的配置公共引入属性的来源地方;
@Configuration
@EnableConfigurationProperties({ SvnKitEnvironmentProperties.class,
CredhubEnvironmentProperties.class, JdbcEnvironmentProperties.class,
NativeEnvironmentProperties.class, VaultEnvironmentProperties.class })
@Import({ CompositeRepositoryConfiguration.class, JdbcRepositoryConfiguration.class,
VaultConfiguration.class, VaultRepositoryConfiguration.class,
CredhubConfiguration.class, CredhubRepositoryConfiguration.class,
SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class,
GitRepositoryConfiguration.class, DefaultRepositoryConfiguration.class })
2.1.2 我们可以发现默认的存储配置使用的是Git
@Configuration
@ConditionalOnMissingBean(value = EnvironmentRepository.class, search = SearchStrategy.CURRENT)
class DefaultRepositoryConfiguration {
@Bean
public MultipleJGitEnvironmentRepository defaultEnvironmentRepository(
MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory,
MultipleJGitEnvironmentProperties environmentProperties) throws Exception {
return gitEnvironmentRepositoryFactory.build(environmentProperties);
}
}
EnvironmentRepositoryConfiguration
中通过profile
注解决定使用装配哪个EnvironmentRepository
Bean
@Configuration
@Profile("git")
class GitRepositoryConfiguration extends DefaultRepositoryConfiguration {
}
具体做法是在配置服务端的 application.properties(application.yml) 中来指定,未填写则默认git;
spring.profile.active=git
另外,我们ctrl+H 查看层级关系可以发现,所有的仓储的实现都实现默认接口
EnvironmentRepository
,也就是说,如果需要替换仓储实现,只需要实例化相关的实现 Bean,如:JdbcEnvironmentRepository
。同样的,如果需要自定义仓储实现,同样只需要实现接口EnvironmentRepository
即可自定义仓储实现。
2.2 以git为例,我们看看是怎么实现的
2.2.1.git bean在EnvironmentRepositoryConfiguration
有两处内部类实现:
- static class JGitFactoryConfig
- class DefaultRepositoryConfiguration
我们查看两个类可以发现,先执行的静态类先初始化了一个工厂类,然后再由DefaultRepositoryConfiguration
调用工厂类里的build()
方法构建出bean
//MultipleJGitEnvironmentRepositoryFactory里的build方法
@Override
public MultipleJGitEnvironmentRepository build(
MultipleJGitEnvironmentProperties environmentProperties) throws Exception {
if (this.connectionFactory.isPresent()) {
HttpTransport.setConnectionFactory(this.connectionFactory.get());
this.connectionFactory.get().addConfiguration(environmentProperties);
}
MultipleJGitEnvironmentRepository repository = new MultipleJGitEnvironmentRepository(
this.environment, environmentProperties);
repository.setTransportConfigCallback(this.customTransportConfigCallback
.orElse(buildTransportConfigCallback(environmentProperties)));
if (this.server.getDefaultLabel() != null) {
repository.setDefaultLabel(this.server.getDefaultLabel());
}
return repository;
}
build()
方法先判断ConfigurableHttpConnectionFactory
对象是否存在,通过查看该对象类,我们可以了解到该类主要是读取配置文件中的perfix的值来组成HttpClientConnection
连接信息,用于连接git服务器的基础信息,处理完HttpClientConnection
信息再初始化MultipleJGitEnvironmentRepository
类(马上我们会分析),然后给MultipleJGitEnvironmentRepository
类设置回调类,回调类不存在的情况下则新增一个回调类用于与git仓库连接,分别使用SshSessionFactory
和SshTransport
两种方式与git仓库连接,最后判断是否有默认的标签(label,版本),有就设置。
2.2.2 从上面我们可以看出DefaultRepositoryConfiguration
返回了一个 MultipleJGitEnvironmentRepository
bean,查看该类类图:
类名 | 备注 |
---|---|
InitializingBean | 配置加载完后调用 afterPropertiesSet 进行一些操作 |
SearchPathLocator | 提供获取配置文件地址的能力 |
EnvironmentRepository | 所有bean的接口规范,获取环境数据 |
ResourceLoaderAware | 默认实现DefaultResourceLoader ,根据配置文件地址,加载配置文件 |
git 仓储的实现MultipleJGitEnvironmentRepository
实现了以上几个接口,使其具有从初始化、获取git地址、根据git地址加载配置信息、构建 Environment 等一系列能力。
- 我们先看看
InitializingBean
的具体实现
@Override
public synchronized void afterPropertiesSet() throws Exception {
Assert.state(getUri() != null, MESSAGE);
initialize();
if (this.cloneOnStart) {
initClonedRepository();
}
}
从源码中我们再进入initialize()
、initClonedRepository()
看到,首先会将配置从git上clone到本地,然后再进行其他的操作。还有一个getLocations。
SearchPathLocator
有一个内部类 Locations ,Locations中定义了应用服务配置存储信息。AbstractScmEnvironmentRepository
实现了AbstractScmAccessor
和EnvironmentRepository
,主要就是EnvironmentRepository
中findOne
的实现:
//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());
}
getLocations 是一个模板方法,Config Server中目前提供了五种实现,我们是以git为例,就跳转到 JGitEnvironmentRepository#getLocations ()
方法
@Override
public synchronized Locations getLocations(String application, String profile,
String label) {
if (label == null) {
label = this.defaultLabel;
}
// 刷新本地仓库的配置状态,获取最新的版本号
String version = refresh(label);
// 根据最新的版本号返回 Locations 定位到资源的搜索路径
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客户端
git = createGitClient();
// 是否需要执行 git pull
if (shouldPull(git)) {
FetchResult fetchStatus = fetch(git, label);
if (this.deleteUntrackedBranches && fetchStatus != null) {
deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(),
git);
}
// 获取后checkout,这样我们就可以获得任何新的分支、tag等。
// checkout after fetch so we can get any new branches, tags, ect.
checkout(git, label);
tryMerge(git, label);
}
else {
// 没有什么要更新,所以只是checkout和merge。
// 合并是因为远程分支以前可能已经更新过
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客户端就会执行fetch,然后再进行merge,更新到本地仓库。
最终是装配一个MultipleJGitEnvironmentRepository的bean,实际每种配置类的实现的最终都是装配一个EnvironmentRepository的子类。
MultipleJGitEnvironmentRepository 多仓库的支持,实际上就是遍历了所有的仓库。其他仓库和单仓库是一样的。
经过一系列的操作,我们得到了MultipleJGitEnvironmentRepository的bean类,接下来就是对外提供服务接口了
3.如何对外提供服务接口?
3.1在ConfigServerAutoConfiguration
类中曾经导入了ConfigServerMvcConfiguration
类,而这个类正是向外暴露出端口供客户端访问的配置,在里面组装了两个的 Controller:
@Bean
@RefreshScope
public EnvironmentController environmentController(
EnvironmentRepository envRepository, ConfigServerProperties server) {
EnvironmentController controller = new EnvironmentController(
encrypted(envRepository, server), this.objectMapper);
controller.setStripDocumentFromYaml(server.isStripDocumentFromYaml());
controller.setAcceptEmpty(server.isAcceptEmpty());
return controller;
}
@Bean
@ConditionalOnBean(ResourceRepository.class)
public ResourceController resourceController(ResourceRepository repository,
EnvironmentRepository envRepository, ConfigServerProperties server) {
ResourceController controller = new ResourceController(repository,
encrypted(envRepository, server));
return controller;
}
我们以EnvironmentController
为栗子:其中最重要方法labelled
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) {
name = normalize(name);
label = normalize(label);
Environment environment = this.repository.findOne(name, profiles, label);
if (!this.acceptEmpty
&& (environment == null || environment.getPropertySources().isEmpty())) {
throw new EnvironmentNotFoundException("Profile Not found");
}
return environment;
}
注意这里代码的EnvironmentController#repository
属性就是GitRepositoryConfiguration
实例化的MultipleJGitEnvironmentRepository
,如果是别的实现方式就是别的EnvironmentRepository
。
可以看出”/{name}/{profiles}/{label:.*}”路径参数正好与我们的请求方式相对应,因此Config Server是通过建立一个RestController来接收读取配置请求的,然后使用EnvironmentRepository
来进行配置查询,最后返回一个这个对象的JSON。
public class Environment {
/**
* "(_)" is uncommon in a git repo name, but "/" cannot be matched by Spring MVC.
*/
public static final String SLASH_PLACEHOLDER = "(_)";
private String name;
private String[] profiles = new String[0];
private String label;
private List<PropertySource> propertySources = new ArrayList<>();
private String version;
private String state;
...
}
我用之前的项目访问http://localhost:7101/microservice_config/microservice-config-client/main
得到以下信息,而这个信息正是 Environment 类
EnvironmentController里面的其他RequestMapping里调用了labelled
方法,所以不难理解就略过了。我懒。
其他RequestMapping里的
ObjectMapper
用于当请求json格式的配置时的序列化
相比拉取环境配置信息,对外提供服务接口就简单粗暴多了
3.尾言
config 服务端中还有很多地方没有讲到,比如Jdbc 仓储的实现、SVN仓库配置、EnvironmentRepositoryConfiguration里的健康检测,多个git配置的情况下(MultipleJGitEnvironmentRepository
)afterPropertiesSet方法和findOne方法、加密解密等其他类的使用。
4.相关资料连接
- SpringCloud-Config 配置中心原理
- Spring Cloud Config 【源码篇】 分布式配置中心实现
- spring-cloud-config-server分布式配置中心
- Spring Cloud Config Server 源码分析 : 配置中心如何提供配置
- zuul概述及源码分析
- Spring Cloud Config源码解析(一)
- Spring Cloud Config源码解析(二)
- Spring Cloud Config源码解析(三)
- Spirng Cloud Config源码解析(四)
- Spring Cloud Config源码解析(五)
- Spring Cloud Config 流程源码详解
- spring cloud config配置中心源码分析之注解@EnableConfigServer
- Spring Cloud config之二:Spring cloud config Server源码分析