前置说明
源码来自springcloud.G版本
以下说明是个人观点,如有错误,欢迎评论处进行讨论
SpringCloud config
springcloud 提供了分布式配置中心, 支持git, svn, native(文件配置).jdbc等. 现在就springcloud-config,探究以下其配置加载和刷新的原理.
首先springcloud-config 分为服务端和客户端, 那么服务端必然是加载配置(无论来自git还是文件等), 然后提供rest接口可以进行查询; 而客户端的话,也是相似, 启动的时候加载远程的配置, 在运行时支持动态的刷新配置.
那么其中的关键点就是 服务端如何加载配置, 客户端如何加载配置, 客户端如何刷新配置, 那么以下就这个三点进行分开论述.
服务端加载配置
首先,服务端启动时会有个自动配置类叫做ConfigServerAutoConfiguration
, 很明显这个类主要是为了导入其他重要的类, 其中我觉得重要的就是ConfigServerMvcConfiguration
,EnvironmentRepositoryConfiguration
接下来先看下ConfigServerMvcConfiguration
, 明显也可以发现它有一个EnvironmentController
的controller 进行了bean的注册, 其简略代码如下
// 配置中心 服务提供端口
@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {
private EnvironmentRepository repository;
...
@RequestMapping("/{name}/{profiles:.*[^-].*}")
public Environment defaultLabel(@PathVariable String name,
@PathVariable String profiles) {
return labelled(name, profiles, null);
}
@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;
}
...
那么明显呢这个controller 就是提供客户端进行访问配置的接口.
接下来看看EnvironmentRepositoryConfiguration
, 这个配置类很明显就是和我们的配置信息源有关系, 很明显能看到git 之类的配置等.
我们随意观察一个配置源 MultipleJGitEnvironmentRepository
,其简略代码如下
public class MultipleJGitEnvironmentRepository extends JGitEnvironmentRepository {
...
// 拉取最新的git文件
@Override
public Locations getLocations(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 {
Environment source = candidate.findOne(application, profile,
label);
if (source != null) {
return candidate.getLocations(application, profile, label);
}
}
catch (Exception e) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Cannot retrieve resource locations from "
+ candidate.getUri() + ", cause: ("
+ e.getClass().getSimpleName() + ") "
+ e.getMessage(), e);
}
continue;
}
}
}
}
JGitEnvironmentRepository candidate = getRepository(this, application, profile,
label);
if (candidate == this) {
return super.getLocations(application, profile, label);
}
return candidate.getLocations(application, profile, label);
}
// 从配置文件读取成抽象的环境实例
@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);
}
...
我们明显可以看到这个就是获取git的配置属性的, 到现在的话基本可以知道服务端的运行逻辑了
客户端加载配置
了解服务端的配置后, 基本可以知道客户端肯定在某个时刻进行了远程调用, 那么客户端是在合适进行获取的呢, 这个就涉及springboot 的启动时的事件驱动的.
要想最快知道在什么时候调用最简单的就是在Resttemplate的exchange打个断点, 于是我就发现了是ConfigServicePropertySourceLocator
进行了远程配置的调用, 那么这个类是有自动配置类ConfigServiceBootstrapConfiguration
进行配置的, 这个也没啥好说的. 简略代码如下
// 自动配置类
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
PropertySource<?> source = null;
// 调用对应的 获取远程的配置的执行器
source = locator.locate(environment);
if (source == null) {
continue;
}
logger.info("Located property source: " + source);
composite.addPropertySource(source);
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
}
// 将获取到的配置加入的当前上下文的环境中去
insertPropertySources(propertySources, composite);
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment);
handleIncludedProfiles(environment);
}
}
...
}
// 远程配置获取
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
ConfigClientProperties properties = this.defaultProperties.override(environment);
CompositePropertySource composite = new CompositePropertySource("configService");
RestTemplate restTemplate = this.restTemplate == null
? getSecureRestTemplate(properties) : this.restTemplate;
Exception error = null;
String errorBody = null;
try {
String[] labels = new String[] { "" };
if (StringUtils.hasText(properties.getLabel())) {
labels = StringUtils
.commaDelimitedListToStringArray(properties.getLabel());
}
String state = ConfigClientStateHolder.getState();
// Try all the labels until one works
for (String label : labels) {
// 获取远程的配置
Environment result = getRemoteEnvironment(restTemplate, properties,
label.trim(), state);
if (result != null) {
log(result);
if (result.getPropertySources() != null) { // result.getPropertySources()
// can be null if using
// xml
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) source
.getSource();
composite.addPropertySource(
new MapPropertySource(source.getName(), map));
}
}
if (StringUtils.hasText(result.getState())
|| StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
composite.addFirstPropertySource(
new MapPropertySource("configClient", map));
}
return composite;
}
}
}
catch (HttpServerErrorException e) {
error = e;
if (MediaType.APPLICATION_JSON
.includes(e.getResponseHeaders().getContentType())) {
errorBody = e.getResponseBodyAsString();
}
}
...
return null;
}
...
}
至此的话,客户端的远程配置获取基本已经讲完了, 接下来的客户端配置的刷新
客户端配置刷新
其实客户端配置刷新的逻辑也比较简单, 主要涉及到一下几个类
- RefreshEndpoint 提供刷新的端口, 也就是 /refresh
- RefreshEventListener 对 RefreshEvent 事件监听来刷新配置 这个是给 springcloud-bus 使用的, 当然我们也可以发布事件
- ContextRefresher 刷新配置的协调者, 进行环境的更新以及 refreshScope 的刷新
- ConfigurationPropertiesRebinder 监听EnvironmentChangeEvent事件, 重新绑定ConfigurationProperties
- RefreshScope 用于destory所有的被@RefreshScope注解的bean, 然后发布相关事件
基本上客户端刷新的要点都在上面的几个类中了, 具体介绍一下几个重要的方法
public class ContextRefresher {
//刷新配置
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
// 刷新环境
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
// 获取远程的配置
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
// 发布一个环境刷新的事件, 主要是给ConfigurationPropertiesRebinder 进行重新绑定
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
// 获取远程的配置
ConfigurableApplicationContext addConfigFilesToEnvironment() {
ConfigurableApplicationContext capture = null;
try {
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
.environment(environment);
builder.application()
.setListeners(Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
// 这里模仿启动的时的样子, 通过BootstrapApplicationListener进行获取最新的配置
capture = builder.run();
if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
}
MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
String targetName = null;
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
if (!this.standardSources.contains(name)) {
if (target.contains(name)) {
target.replace(name, source);
}
else {
if (targetName != null) {
target.addAfter(targetName, source);
}
else {
// targetName was null so we are at the start of the list
target.addFirst(source);
targetName = name;
}
}
}
}
}
finally {
ConfigurableApplicationContext closeable = capture;
while (closeable != null) {
try {
closeable.close();
}
catch (Exception e) {
// Ignore;
}
if (closeable.getParent() instanceof ConfigurableApplicationContext) {
closeable = (ConfigurableApplicationContext) closeable.getParent();
}
else {
break;
}
}
}
return capture;
}
...
}
// 监听EnvironmentChangeEvent事件然后对@ConfigurationProperties 的重新绑定
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean);
//重新调用initializeBean 方法, 进行属性的绑定, 具体的不进行介绍
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
}
return false;
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
...
}
// 这个类也很简单, 主要就是对有这个注解的bean 进行注销
@ManagedResource
public class RefreshScope extends GenericScope implements ApplicationContextAware,
ApplicationListener<ContextRefreshedEvent>, Ordered {
@ManagedOperation(description = "Dispose of the current instance of bean name "
+ "provided and force a refresh on next method execution.")
public boolean refresh(String name) {
if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
// User wants to refresh the bean with this name but that isn't the one in the
// cache...
name = SCOPED_TARGET_PREFIX + name;
}
// 注销bean
if (super.destroy(name)) {
this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
return true;
}
return false;
}
@ManagedOperation(description = "Dispose of the current instance of all beans "
+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
结尾
到此为止, 开篇的三个点都讲完了, 基本上springcloud 的配置理的比较清楚了, 也是能看出springcloud强大的架构能力, 设计非常多的有意思的代码: 包括事件驱动的运用, 以及在拉取远程配置时通过模拟启动的SpringApplication进行获取, 也是非常的奇技淫巧, 最后给上一副交互图