文章目录
前言
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。
Apollo支持4个维度管理Key-Value格式的配置:
- application (应用)
- environment (环境)
- cluster (集群)
- namespace (命名空间) 同时,Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo
诞生背景:
随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的地址……
对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制……
在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。
Apollo配置中心应运而生!
简单来说,Apollo 是一个配置中心,可以非常方便管理公司所有项目配置,并支持热更新。本文主要讲解 Apollo 在 Spring 环境下的热更新操作。
一、Apollo热更新配置
1. @Value的方式
这种方式不需要改任何代码,支持热更新
@Value(value = "${config_key}")
private int configValue;
2. @RefreshScope
定义配置类,配置类中加上 @RefreshScope
注解,加上改注解的 Bean 能在运行时被实时刷新
,即销毁然后重新创建,所有引用到该 Bean 的地方在下次访问时都会指向新的对象
@Configuration
@EnableConfigurationProperties(ConfigProperties.class)
@ConfigurationProperties(prefix = "config")
@Setter
@Getter
@RefreshScope
public class ConfigProperties {
private int configValue;
}
3. 监听apollo配置更新事件
在方法上加 @ApolloConfigChangeListener
注解,监听 Apollo 配置变更事件。
@Slf4j
@Component
public class SpringBootApolloRefreshConfig {
private final ConfigProperties configProperties;
private final RefreshScope refreshScope;
public SpringBootApolloRefreshConfig(ConfigProperties configProperties, RefreshScope refreshScope) {
this. configProperties = configProperties;
this.refreshScope = refreshScope;
}
@ApolloConfigChangeListener(value = {"namespace"}, interestedKeyPrefixes = {"key_prefix"}, interestedKeys = {"key_prefix.key"})
public void onChange(ConfigChangeEvent changeEvent) {
log.info("before refresh {}", configProperties.toString());
refreshScope.refresh("configProperties");
log.info("after refresh {}", configProperties.toString());
}
}
- ApolloConfigChangeListener 注解,value 中的值为感兴趣的 namespace,interestedKeyPrefixes 中的值为感兴趣的 key 前缀(不指定表示全部),interestedKeys 表示具体对哪些 key 感兴趣(不指定表示全部)。
- 当检测到 Apollo 配置变更时,
refreshScope.refresh("beanName")
刷新配置。
二、源码解析
1. RefreshScope#refresh
方法如下:
- 调父类 GenericScope#destroy 清空缓存(对应的 BeanLifecycleWrapper 缓存)
- 发布 RefreshScopeRefreshedEvent 事件,该事件会最终会触发/refresh actuator endpoint
@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;
}
// Ensure lifecycle is finished if bean was disposable
if (super.destroy(name)) {
// 发布配置刷新事件
this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
return true;
}
return false;
}
}
2. RefreshEndpoint
@Endpoint(id = "refresh")
public class RefreshEndpoint {
private ContextRefresher contextRefresher;
public RefreshEndpoint(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}
@WriteOperation
public Collection<String> refresh() {
// 触发刷新环境变量
Set<String> keys = this.contextRefresher.refresh();
return keys;
}
}
3. ContextRefresher
刷新环境变量&发布变更事件
- #refresh 触发入口
public class ContextRefresher {
...
private ConfigurableApplicationContext context;
private RefreshScope scope;
public ContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
this.context = context;
this.scope = scope;
}
protected ConfigurableApplicationContext getContext() {
return this.context;
}
protected RefreshScope getScope() {
return this.scope;
}
// 刷新环境变量
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();
// 获取变更的变量key
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
// 发布环境变量变更事件
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
}
4. 配置刷新
- #onApplicationEvent 方法监听环境变更事件
- #rebind 方法遍历所有加 RefreshScope 注解的配置类,刷新配置
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private ApplicationContext applicationContext;
private Map<String, Exception> errors = new ConcurrentHashMap<>();
/**
* beans中包含所有加上RefreshScope注解的配置类实例
*/
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
this.beans = beans;
}
...
/**
* 遍历每一个加了RefreshScope注解的配置类,刷新配置
*/
@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) {
// TODO: determine a more general approach to fix this.
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
// 销毁旧的配置类实例
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean);
// 根据新的环境变量,重新初始化配置类
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;
}
@ManagedAttribute
public Set<String> getNeverRefreshable() {
String neverRefresh = this.applicationContext.getEnvironment().getProperty(
"spring.cloud.refresh.never-refreshable",
"com.zaxxer.hikari.HikariDataSource");
return StringUtils.commaDelimitedListToSet(neverRefresh);
}
@ManagedAttribute
public Set<String> getBeanNames() {
return new HashSet<>(this.beans.getBeanNames());
}
/**
* 监听环境变量变更事件,重新绑定刷新配置
*/
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
}
5. 配置类注册
- 该类实现了BeanPostProcessor接口,在Bean初始化之前会进入postProcessBeforeInitialization方法
- 加上RefreshScope注解的配置类会注册进beans map中,用于刷新配置
@Component
public class ConfigurationPropertiesBeans
implements BeanPostProcessor, ApplicationContextAware {
private Map<String, ConfigurationPropertiesBean> beans = new HashMap<>();
private ApplicationContext applicationContext;
private ConfigurableListableBeanFactory beanFactory;
private String refreshScope;
private boolean refreshScopeInitialized;
private ConfigurationPropertiesBeans parent;
...
/**
* 在Bean初始化之前,会调用这里,用于注册配置类对象
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// 判断Bean是否加上了RefreshScope注解
if (isRefreshScoped(beanName)) {
return bean;
}
// 判断Bean是否加上了ConfigurationProperties注解
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
.get(this.applicationContext, bean, beanName);
// 将加上RefreshScope注解的配置类对象放在map中,用于刷新配置
if (propertiesBean != null) {
this.beans.put(beanName, propertiesBean);
}
return bean;
}
private boolean isRefreshScoped(String beanName) {
if (this.refreshScope == null && !this.refreshScopeInitialized) {
this.refreshScopeInitialized = true;
for (String scope : this.beanFactory.getRegisteredScopeNames()) {
if (this.beanFactory.getRegisteredScope(
scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) {
this.refreshScope = scope;
break;
}
}
}
if (beanName == null || this.refreshScope == null) {
return false;
}
return this.beanFactory.containsBeanDefinition(beanName) && this.refreshScope
.equals(this.beanFactory.getBeanDefinition(beanName).getScope());
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
public Set<String> getBeanNames() {
return new HashSet<String>(this.beans.keySet());
}
}