Spring Cloud Config 服务端源码简析

1、Config Server的任务

  • 从环境仓库中拉取配置信息到本地
  • 对外提供服务接口
    所以我们核心关注如何拉去配置信息并且如何对外暴露接口的。

2、如何拉取配置信息到本地?

  1. 使用@EnableConfigServer注解成为配置中心,发现导入了配置类ConfigServerConfiguration;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigServerConfiguration.class)
public @interface EnableConfigServer {

}
  1. ConfigServerConfiguration中装配了MarkerBean(这个实例化Marker的操作,Zuul也是这样做的),Marker 是开启 Spring Cloud Config Server 自动配置类的开关;
@Configuration
public class ConfigServerConfiguration {
   @Bean
   public Marker enableConfigServerMarker() {
      return new Marker();
   }
   class Marker {
   }
}
  1. 我们在spring-cloud-config-server-2.1.8.RELEASE.jar中的spring.factories中查看自动配置的类,我们先看ConfigServerAutoConfiguration
  2. 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仓库连接,分别使用SshSessionFactorySshTransport两种方式与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 实现了 AbstractScmAccessorEnvironmentRepository ,主要就是EnvironmentRepositoryfindOne 的实现:
//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.相关资料连接

  1. SpringCloud-Config 配置中心原理
  2. Spring Cloud Config 【源码篇】 分布式配置中心实现
  3. spring-cloud-config-server分布式配置中心
  4. Spring Cloud Config Server 源码分析 : 配置中心如何提供配置
  5. zuul概述及源码分析
  6. Spring Cloud Config源码解析(一)
  7. Spring Cloud Config源码解析(二)
  8. Spring Cloud Config源码解析(三)
  9. Spirng Cloud Config源码解析(四)
  10. Spring Cloud Config源码解析(五)
  11. Spring Cloud Config 流程源码详解
  12. spring cloud config配置中心源码分析之注解@EnableConfigServer
  13. Spring Cloud config之二:Spring cloud config Server源码分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值