Apollo源码阅读笔记(二)
Apollo源码阅读笔记(二)
前面 分析了apollo配置设置到Spring的environment的过程,此文继续PropertySourcesProcessor.postProcessBeanFactory里面调用的第二个方法initializeAutoUpdatePropertiesFeature(beanFactory),其实也就是配置修改后更新相关处理逻辑。
在继续分析之前,先来看下配置是怎么自动更新的。
1. 配置更新简单示例
通过portal页面,修改配置之后,我们可以通过@ApolloConfigChangeListener来自己监听配置的变化手动更新,也可以通过@Value标记字段,字段值会自动更新。
1.1 更新event相关对象
无论是哪种方式,都离不开event,所以先了解下event相关的对象
// change事件
public class ConfigChangeEvent {
private final String m_namespace;
private final Map<String, ConfigChange> m_changes;
// 省略了全部方法
}
// change实体bean
public class ConfigChange {
private final String namespace;
private final String propertyName;
private String oldValue;
private String newValue;
private PropertyChangeType changeType;
// 省略了全部方法
}
// change类型
public enum PropertyChangeType {
ADDED, MODIFIED, DELETED
}
1.2 Value注解方式
直接在属性字段上标记@Value就可以了。
eg:
@Service
public class XXXService {
@Value("${config.key}")
private String XXConfig;
....
}
如果修改了config.key配置项,那么这里的XXConfig的值是会自动更新的。
1.3 自定义ConfigChangeListener方式
@Value方式实现起来很简单,但如果要更灵活点,比如加上一些自己的业务处理,那就需要用到@ApolloConfigChangeListener了。
@ApolloConfigChangeListener
private void onChange(ConfigChangeEvent changeEvent) {
logger.info("配置参数发生变化[{}]", JSON.toJSONString(changeEvent));
doSomething();
}
标记有@ApolloConfigChangeListener的这个方法,必须带一个ConfigChangeEvent的入参,通过这个event可以拿到事件的类型、变化的具体字段、变化前后值。
拿到event之后,我们可以根据具体的变化做不同业务处理。
以上是更新的使用,下面来深入研究下源码的实现。
2. 配置更新监听listener
先继续文章开头留下的尾巴 initializeAutoUpdatePropertiesFeature方法。
2.1 AutoUpdateConfigChangeListener
PropertySourcesProcessor.postProcessBeanFactory –> initializeAutoUpdatePropertiesFeature中。具体逻辑如下:
构造一个 AutoUpdateConfigChangeListener 对象 [implements ConfigChangeListener];
拿到前面处理的所有的ConfigPropertySource组成的list,遍历ConfigPropertySource,设置listener
configPropertySource.addChangeListener(autoUpdateConfigChangeListener);
这里加事件,最终是加在Config上了。
我们前面使用的@Value注解标记的字段,在字段值发生变化时,就是通过这里加的listener,收到通知的。
@Value是org.springframework.beans.factory.annotation.value,Spring的注解。apollo通过自己的SpringValueProcessor来处理它。来看下完整的流程:
SpringValueProcessor继承了ApolloProcessor,间接实现了BeanPostProcessor
在启动的时候,会被调到 postProcessBeforeInitialization,进而调到processField
- 判断字段是否被@Value标记,如果没有则返回,有则继续往下
- 解析@Value注解里面设置的value
- 构造SpringValue对象 new SpringValue(key, value.value(), bean, beanName, field, false)
- 将构造的springValue存到SpringValueRegistry的Map<BeanFactory, Multimap<String, SpringValue>>中
当修改配置发布事件后,AutoUpdateConfigChangeListener就被触发onChange(ConfigChangeEvent changeEvent)事件
通过event拿到所有变更的keys
遍历keys,通过springValueRegistry.get(beanFactory, key) 拿到SpringValue集合对象
这里就是从前面的2.4的map里面获取的
判断配置是否真的发生了变化 shouldTriggerAutoUpdate(changeEvent, key)
遍历SpringValue集合,逐一通过反射改变字段的值
到这里,@Value的更新流程就清楚了。
下面来看看自定义listener是怎么通知更新的。
2.2 ConfigChangeListener
更多的时候,我们是通过在方法上标记@ApolloConfigChangeListener来实现自己的监听处理。[例子见1.3代码]
通过@ApolloConfigChangeListener注解添加的监听方法,默认关注的application namespace下的全部配置项。
有关该注解的处理逻辑在 com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor 中,我们重点关注如下代码段 processMethod :
protected void processMethod(final Object bean, String beanName, final Method method) {
ApolloConfigChangeListener annotation = AnnotationUtils.findAnnotation(method, ApolloConfigChangeListener.class);
if (annotation == null) {
return;
}
Class<?>[] parameterTypes = method.getParameterTypes();
Preconditions.checkArgument(parameterTypes.length == 1, "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length, method);
Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]), "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0], method);
ReflectionUtils.makeAccessible(method);
String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys();
Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
// 创建listener
ConfigChangeListener configChangeListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
ReflectionUtils.invokeMethod(method, bean, changeEvent);
}
};
// 给config设置listener
for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace);
if (interestedKeys == null) {
config.addChangeListener(configChangeListener);
} else {
config.addChangeListener(configChangeListener, interestedKeys);
}
}
}
经过这段代码处理,如果有change事件,我们通过@ApolloConfigChangeListener自定义的listener就会收到消息了。
前面了解完了监听,下面来看下事件的发布。
3. 配置更新事件发布
后台portal页面修改发布之后,client端怎么接收到事件呢?其实在client启动后,就会和服务端建一个长连接。代码见 com.ctrip.framework.apollo.internals.RemoteConfigRepository 。
先来看看RemoteConfigRepository 构造方法
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;
m_configCache = new AtomicReference<>();
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
m_longPollServiceDto = new AtomicReference<>();
m_remoteMessages = new AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());
m_configNeedForceRefresh = new AtomicBoolean(true);
m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),
m_configUtil.getOnErrorRetryInterval() * 8);
gson = new Gson();
this.trySync();
this.schedulePeriodicRefresh();
this.scheduleLongPollingRefresh();
}
从上面构造方法最后几行可以看出,启动的时候,会先尝试从server端同步,然后会启动2个定时刷新任务,一个是定时刷新,一个是长轮询。不过不管是哪种方式,最终进入的还是 trySync() 方法。
以com.ctrip.framework.apollo.internals.RemoteConfigLongPollService为例
RemoteConfigLongPollService
—> startLongPolling()
—> doLongPollingRefresh(appId, cluster, dataCenter),发起http长连接
—> 收到http response(server端发布了新的配置)
—> notify(lastServiceDto, response.getBody());
—> remoteConfigRepository.onLongPollNotified(lastServiceDto, remoteMessages);
—> 异步调用 remoteConfigRepository.trySync();
在trySync方法中,直接调用sync()后就返回,所以需要看sync()方法内部逻辑。在sync()方法内:
先获取本机缓存的当前配置 ApolloConfig previous = m_configCache.get()
获取server端的最新配置 ApolloConfig current = loadApolloConfig()
如果 previous != current ,更新m_configCache
m_configCache.set(current);
this.fireRepositoryChange(m_namespace, this.getConfig());在fireRepositoryChange里面,遍历当前namespace下的listener,调用RepositoryChangeListener事件
listener.onRepositoryChange(namespace, newProperties)
<—这里事件就传到LocalFileConfigRepository 这一层了
本机该namespace的LocalFileConfigRepository实现了RepositoryChangeListener,所以会受到通知调用
在LocalFileConfigRepository的onRepositoryChange方法中:
比较newProperties.equals(m_fileProperties),相同就直接return,否则继续往下
更新本机缓存文件 updateFileProperties
触发 fireRepositoryChange(namespace, newProperties); <—这里事件就传到Config这一层了
触发事件在 DefaultConfig 中得到响应处理。这里先对发生变化的配置做一些处理,然后发ConfigChangeEvent事件
this.fireConfigChange(new ConfigChangeEvent(m_namespace, actualChanges));
在DefaultConfig的this.fireConfigChange里面,就会遍历 listeners,依次调用onChange方法
准确的说,是在父类AbstractConfig中实现的;
这里的listeners就有前面提到的AutoUpdateConfigChangeListener 和 ApolloAnnotationProcessor 中定义的ConfigChangeListener
至此,事件的发布监听就形成闭环了,这里fireConfigChange(ConfigChangeEvent)后,@Value标记的字段、@ApolloConfigChangeListener都会被触发更新。
Apollo源码阅读笔记(二)的更多相关文章
- Apollo源码阅读笔记(一)
Apollo源码阅读笔记(一) 先来一张官方客户端设计图,方便我们了解客户端的整体思路. 我们在使用Apollo的时候,需要标记@EnableApolloConfig来告诉程序开启apollo配置,所 ...
- werkzeug源码阅读笔记(二) 下
wsgi.py----第二部分 pop_path_info()函数 先测试一下这个函数的作用: >>> from werkzeug.wsgi import pop_path_info ...
- werkzeug源码阅读笔记(二) 上
因为第一部分是关于初始化的部分的,我就没有发布出来~ wsgi.py----第一部分 在分析这个模块之前, 需要了解一下WSGI, 大致了解了之后再继续~ get_current_url()函数 很明 ...
- Detectron2源码阅读笔记-(二)Registry&build_*方法
Trainer解析 我们继续Detectron2代码阅读笔记-(一)中的内容. 上图画出了detectron2文件夹中的三个子文件夹(tools,config,engine)之间的关系.那么剩下的 ...
- Android源码阅读笔记二 消息处理机制
消息处理机制: .MessageQueue: 用来描述消息队列2.Looper:用来创建消息队列3.Handler:用来发送消息队列 初始化: .通过Looper.prepare()创建一个Loope ...
- 【原】FMDB源码阅读(二)
[原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...
- Three.js源码阅读笔记-5
Core::Ray 该类用来表示空间中的“射线”,主要用来进行碰撞检测. THREE.Ray = function ( origin, direction ) { this.origin = ( or ...
- jdk源码阅读笔记-LinkedHashMap
Map是Java collection framework 中重要的组成部分,特别是HashMap是在我们在日常的开发的过程中使用的最多的一个集合.但是遗憾的是,存放在HashMap中元素都是无序的, ...
- HashMap源码阅读笔记
HashMap源码阅读笔记 本文在此博客的内容上进行了部分修改,旨在加深笔者对HashMap的理解,暂不讨论红黑树相关逻辑 概述 HashMap作为经常使用到的类,大多时候都是只知道大概原理,比如 ...
随机推荐
- JavaScript中JSON对象和JSON字符串的相互转化
一.JSON字符串转换为JSON对象 var str = '{"name":"cxh","sex":"man",&quo ...
- Android精通:TableLayout布局,GridLayout网格布局,FrameLayout帧布局,AbsoluteLayout绝对布局,RelativeLayout相对布局
在Android中提供了几个常用布局: LinearLayout线性布局 RelativeLayout相对布局 FrameLayout帧布局 AbsoluteLayout绝对布局 TableLayou ...
- Bootstrap轮播如何支持移动端左右滑动
一直觉得bootstrap的轮播用起来很好用,代码简单,又支持响应式,不过从来没想过,也不知道原来bootstrap的轮播竟然不支持在手机上左右滑动 解决方法就是:使用滑动手势js插件:hammer. ...
- ubuntu解压rar文件
一般通过默认安装的ubuntu是不能解压rar文件的,只有在安装了rar解压工具之后,才可以解压.其实在ubuntu下安装rar解压工具是非常简 单的,只需要两个步骤就可以迅速搞定. ubuntu 下 ...
- 移动端h5页面的那些坑
最近一直在写移动端页面,由于之前写移动端写的比较少,所以此次踩过许多坑.特此总结一下: 1.<input type='button'>背景色在ios中的兼容性,颜色发白 解决办法:在全局样 ...
- LeetCode--No.009 Palindrome Number
9. Palindrome Number Total Accepted: 136330 Total Submissions: 418995 Difficulty: Easy Determine whe ...
- [COI2007] Sabor
下面给出这道一脸不可做的题的鬼畜性质: 1)对于一个点来说,其归属状态是确定的:走不到.A党或B党 .(黑白格染色) 方便起见,将包含所有不可达的点的极小矩形向外扩展一圈,设为矩形M. 2)矩形M的最 ...
- .net core + headless chrome实现动态网页爬虫
一般的http请求库只能够抓取到网页的静态内容,如果想抓取通过js动态生成的内容可以使用没有gui的browser库,之前许多人会使用phantomjs作为headless browser,不过现在p ...
- Docker修改默认网段
因阿里云服务器VPC默认占用了172.16.0.0/16 网段,与Docker里的网段相同,导致Docker里无法连接VPC服务器.后来找到的解决方案是修改Docker的默认网段. 由于Docker默 ...
- SQL 必知必会·笔记<18>管理事务处理
事务处理是一种机制,用来管理必须成批执行的SQL操作,保证数据库不包含不完整的操作结果.利用事务处理,可以保证一组操作不会中途停止,它们要 么完全执行,要么完全不执行(除非明确指示).如果没有错误发生 ...