知识点:如何执行被注解
ApolloConfigChangeListener
标注的方法
一、起因
事情的故事的是这个样子的,前面的文章我们自定义了一个starter①,并且集成了Apollo,可以自动从Apollo中获取配置,但是我们发现它没办法保证在Apollo修改了配置之后,同步更新到本地。我们找到官方的WIKI介绍②,我用人话重新组织了下他们的语言:我们“不支持@ConfigurationProperties
自动注入,需要自己写点东西才行,具体是啥,想知道直接查看附录的超链接。
二、方案
根据附录链接,我很快定位并写了测试方法,很快....真的很快!代码如下,经过测试发现,随时修改,随时变化了,达到了我想要的效果
package com.example.fg;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author cattle - 稻草鸟人
* @date 2020/4/14 下午2:01
*/
@Slf4j
@Component
public class ApolloRefreshListener implements ApplicationContextAware {
private ApplicationContext applicationContext;
@ApolloConfigChangeListener(value = {"application","promotion"})
private void onChange(ConfigChangeEvent changeEvent) {
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
log.info("change - {}", change.toString());
}
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
但是....事情没完!
从上面的代码我们发现@ApolloConfigChangeListener
注解中的value写死了,这可愁死我了,我可不想要写死,为啥呢?这话可能扯的有点远,涉及到我对软件架构的思路,以后有机会写写。总之,我不想写死。所以我就开始了翻看apollo-client源码,这才会有了这篇文章
三、学习
1)执行过程
跟着@ApolloConfigChangeListener
经过一番云雨...不对,一顿操作,让我找到了它的祖宗,大概明白了该注解到底是怎么起作用的。下面我大概说明一下apollo-client整个工程的启动顺序
ApolloApplicationContextInitializer实现了ApplicationContextInitializer接口用于初始化一些内容。
在初始化之前会先去判断apollo.bootstrap.enabled
是否为true
然后会获取apollo.bootstrap.namespaces
配置的namespace并分别获取到所有namespaces的Config对象
for (String namespace : namespaceList) {
Config config = ConfigService.getConfig(namespace);
.....
}
下面我贴一下获取Config执行的代码片段
ConfigService
public static Config getConfig(String namespace) {
return s_instance.getManager().getConfig(namespace);
}
DefaultConfigManager
public Config getConfig(String namespace) {
Config config = m_configs.get(namespace);
if (config == null) {
synchronized (this) {
config = m_configs.get(namespace);
if (config == null) {
ConfigFactory factory = m_factoryManager.getFactory(namespace);
config = factory.create(namespace);
m_configs.put(namespace, config);
}
}
}
return config;
}
DefaultConfigFactory
public Config create(String namespace) {
ConfigFileFormat format = determineFileFormat(namespace);
if (ConfigFileFormat.isPropertiesCompatible(format)) {
return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
}
return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
}
DefaultConfig
public DefaultConfig(String namespace, ConfigRepository configRepository) {
m_namespace = namespace;
m_resourceProperties = loadFromResource(m_namespace);
m_configRepository = configRepository;
m_configProperties = new AtomicReference<>();
m_warnLogRateLimiter = RateLimiter.create(0.017); // 1 warning log output per minute
initialize();
}
private void initialize() {
try {
updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType());
} catch (Throwable ex) {
Tracer.logError(ex);
logger.warn("Init Apollo Local Config failed - namespace: {}, reason: {}.",
m_namespace, ExceptionUtil.getDetailMessage(ex));
} finally {
//register the change listener no matter config repository is working or not
//so that whenever config repository is recovered, config could get changed
m_configRepository.addChangeListener(this);
}
}
从上面的代码我们知道ApolloApplicationContextInitializer启动的时候获取了一个或者多个DefaultConfig。DefaultConfig再初始化的时候m_configRepository.addChangeListener(this);
。这段代码成功的引起的我的注意,就像一个仙女从你面前路过,就算是直男也应该有所反应,除非他在打游戏!这段代码成功的将当前DefaultConfig对象加入到了抽象类AbstractConfigRepository
中RepositoryChangeListener
的集合中,
我们仔细观察一下AbstractConfigRepository
的几个方法,它基本上就是一个标准的模板方法模式啊!通过调用trySync()方法执行sync(),sync()是抽象方法说明它是由子类实现的。
[图片上传失败...(image-5b8c3a-1587292509810)]
这里,我们介绍下RemoteConifgRepository
。它是在什么时候初始化的呢,我们继续撸...发现DefaultConfigFactory在new DefaultConfig(namespace, createLocalConfigRepository(namespace))
时createLocalConfigRepository(namespace)里面实例化一个RemoteConifgRepository
。
OK,到这里我们简单在总结下过程,首先项目启动的时候初始化了RemoteConifgRepository
然后初始化DefaultConfig
,并且将DefaultConfig
加入到了RepositoryChangeListener
的集合中。RemoteConfigRepository
的初始化方法很有意思,我们注意到它的构造器有如下三个方法都会执行trySync(),而trySync()方法会执行sync(),sync()方法则会执行fireRepositoryChange
然后执行onRepositoryChange
再执行fireConfigChange
最后执行ConfigChangeListener
的onChange
方法.....
public RemoteConfigRepository(String namespace) {
....
//启动时执行一次
this.trySync();
//定时执行
this.schedulePeriodicRefresh();
//长连接拉取
this.scheduleLongPollingRefresh();
}
终于到正题了,到底是哪个ConfigChangeListener
执行onChange
方法,onChange
方法的参数ConfigChangeEvent
又是怎么得到的呢
2)添加ConfigChangeListener
在步骤一中,我们看到了在自定义的onChange方法上增加了一个ApolloConfigChangeListener
注解,那么这个注解是如何生效并且添加ConfigChangeListener的呢
它是通过Service Provider Interface(SPI)机制,实现了一个DefaultApolloConfigRegistrarHelper
,这里我们关注ApolloAnnotationProcessor
类。ApolloAnnotationProcessor
的父亲也就是BeanPostProcessor
利用了BeanPostProcessor
功能,在类初始化之前执行了自己的processMethod
方法。该方法则会将自定义的listener方法添加到DefaultConfig
中
protected void processMethod(final Object bean, String beanName, final Method method) {
ApolloConfigChangeListener annotation = AnnotationUtils
.findAnnotation(method, ApolloConfigChangeListener.class);
if (annotation == null) {
return;
}
....
String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys();
String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
ConfigChangeListener configChangeListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
ReflectionUtils.invokeMethod(method, bean, changeEvent);
}
};
....
// 添加自定义的方法到DefaultConfig中的ConfigChangeListener集合属性中
for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace);
if (interestedKeys == null && interestedKeyPrefixes == null) {
config.addChangeListener(configChangeListener);
} else {
config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
}
}
}
到这里我们基本了解作者的意图了,目的就是为了 执行被注解ApolloConfigChangeListener
标注的自定义的方法,执行过程我们也了如指掌了。那么.....问题来了,到底怎样做ApolloConfigChangeListener
上的namespace才可以不写死呢,随着项目配置自动获取所有的namespace呢?
四、附录
①创建属于自己的starter(https://mp.weixin.qq.com/s/X3kJyFHrn7tul4PiGlfT5g)
② Apollo指南(https://github.com/ctripcorp/apollo/wiki/Java客户端使用指南#323-spring-annotation支持)
一起学习吧
网友评论