好奇怪,@Value不能获取最新的配置?
前言
公司使用Apollo做配置中心,也没怎么具体了解过。只知道有配置使用时用@Value就能获取到了,而且还能自动刷新。遂一直以为@Value本身有自动刷新功能。
直到有一天用nacos做配置中心。哎?我的配置怎么不能自动刷新了?@Value
出问题了吗?
我的controller
我的nacos配置
修改nacos配置
把account.name
改为杜甫
可以看到控制台也打印了refresh keys changed
可是调用接口得到的值仍是更新前的
后面才知道nacos要配合@RefreshScope
使用才能自动刷新。Apollo的@Value
自动刷新功能算是一个机制了。
于是加上@RefreshScope
试验一下
先把nacos中的account.name
配置改为李白,启动项目之后,再改为杜甫,然后调用接口获取account.name
这次获取到的配置就是最新的了。
为什么Apollo能实现@Value
的自动刷新,而nacos需要配合@RefreshScope
呢?
本文旨在分析Apollo和nacos为什么能实现配置自动,和他们的实现方式有什么不同。
先简单看一下Apollo为什么能实现@Value
的自动更新
Apollo的自动更新
Apollo的自动更新比较简单
AutoUpdateConfigChangeListener#onChange
AutoUpdateConfigChangeListener
是一个监听器,监听ConfigChangeEvent
事件。客户端会一直向远程拉取,当配置发生变化就会发布这个事件。
可以看到通过springValueRegistry
获取到了SpringValue
的集合。SpringValue
是Apollo维护的来更新spring中@Value
值变化后的类。里面维护了@Value
的占位符,bean,field等信息。通过继承BeanPostProcessor实现postProcessBeforeInitialization
来维护起来的,这里不管他,只要知道他维护了@Value
注释的字段和bean信息就行了。
继续看updateSpringValue
typescript复制代码private void updateSpringValue(SpringValue springValue) {
try {
//解析出表达式的值
Object value = resolvePropertyValue(springValue);
//更新
springValue.update(value);
logger.info("Auto update apollo changed value successfully, new value: {}, {}", value,
springValue);
} catch (Throwable ex) {
logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex);
}
}
继续走springValue.update()方法
我们这里挑一个看
可以看到这里先获取bean(弱引用),然后更新这个字段的值(Field.set()
方法)。
总结:
Apollo自己维护了@Value
的信息,当有变化时,Apollo直接更新使用@Value
注解bean的字段值
nacos的自动更新
nacos的自动更新使用了spring的机制,因此比较复杂,所以nacos的自动更新机制我们详细看看。
在nacos-config的spring.factoies
中,注入了NacosConfigAutoConfiguration
在NacosConfigAutoConfiguration
里面注入了一个NacosContextRefresher
的Bean
这个Bean实现了ApplicationListener
,监听ApplicationReadyEvent
事件,这个事件会在spring就绪后发布,也就是SpringApplication#run()
的最后。
继续到registerNacosListener
里,重点看一下画红圈的部分
applicationContext.publishEvent
,这里发布了一个RefreshEvent
事件,这个事件会由RefreshEventListener
来处理,这个事件的处理我们下面说。
先来看configService.addListener
。configService
是nacos具体实现配置发布、更新、获取、删除的实现类,具体就是拼接url和参数调用远程的nacos服务。addListener
就是添加监听器
到这里nacos添加上了配置更新的监听器,那么这个监听器是在哪触发的呢?
还是ConfigService
,它的实现类NacosConfigService
中的构造方法中。
在构造方法中,创建了ClientWorker
。
在ClientWorker
会启动定时任务调用checkConfigInfo()
。还创建了一个线程池赋值给executorService。
在checkConfigInfo()
中会使用这个线程池执行任务
可以看到提交了LongPollingRunnable
这个任务,继续看一下他的run
方法
这个方法有点长,只截取一部分
可以看到如果 远程发现了有配置更新,就会更新缓存为最新值,checkUpdateDataIds
和getServerConfig
就是http请求nacos服务端获取数据,这里不展开了,感兴趣的可以自己看一下。
到此为止,我们动态更新的配置已经可以被nacos检测到了。那为什么@Value
返回的值还是旧的呢。
这就要回到我们刚才略过的applicationContext.publishEvent
,发布的RefreshEvent
事件了。
RefreshEventListener
监听RefreshEvent
事件,在RefreshEventListener
的handle
里对事件做处理
refresh
方法就是能刷新配置的重点了。refresh
方法里一共调用了两个方法我们一个一个来看
先看refreshEnvironment
refreshEnvironment
是一个更新配置的方法,可以看到是先获取到before(之前的配置),然后用之后的配置更新配置。同样都是使用this.context.getEnvironment().getPropertySources()
来获取配置,在中间调用了addConfigFilesToEnvironment()
,为什么调用addConfigFilesToEnvironment
之后就能获取到新的了呢?
在addConfigFilesToEnvironment
中会创建一个新的spring上下文,最终会调用到SpringApplication.run()
里(是不是很熟悉,又到了spring经典的run
方法)。
在run()
方法中有个prepareContext()
,再往里调用applyInitializers
。在applyInitializers
里会遍历所有实现了ApplicationContextInitializer
的接口(在Spring容器刷新之前执行的一个回调函数,通常用于向Spring容器中注入属性),调用其中的initialize
方法。
啊啊啊?这都哪到哪啊。这个关我自动刷新什么事?别急,这就有关联了。
PropertySourceBootstrapConfiguration
(也会由spring.factories
注入),这个类就实现了ApplicationContextInitializer
(也就是向spring容器注入属性的接口)。所以在prepareContext
的applyInitializers
方法中就会调用它的initialize
方法。
在initialize
中会调用PropertySourceLocator
接口的locateCollection
。nacos的NacosPropertySourceLocator
就实现了这个接口,会向nacos获取配置,这样nacos里的配置就是最新的了。再往下他会塞到environment
里,这样this.context.getEnvironment().getPropertySources()
就能获取到最新的配置了。
在addConfigFilesToEnvironment
做完这些工作后就会把新创建的spring上下文关闭。
看到这好像大功告成了,但其实还有个问题,配置变了,bean里通过@Value
获取到的值可没变。怎么把它的值给更新呢?spring选择了非常简便的方法,弄个新的出来。
继续看refresh里调用的refreshAll()
方法
这里的this.cache
是BeanLifecycleWrapperCache
,里边存放BeanLifecycleWrapper
。BeanLifecycleWrapper
里存放了bean的名称和ObjectFactory
。ObjectFactory
提供了一个getObject()
方法,用于获取对象实例。
那代码把cache
清空,把BeanLifecycleWrapper
都销毁有什么用呢?我们先去看一下BeanLifecycleWrapper
是怎么放进去的。
看一下Scope
的实现类GenericScope
的get
方法
get
方法创建了新的BeanLifecycleWrapper
放入到cache
里。注意这里的cache.put()
最终调用的是ConcurrentHashMap
的putIfAbsent
,如果已有可就不放了。返回的即是BeanLifecycleWrapper
的getBean()
方法。
getBean
就会调用ObjectFactory
的getObject
方法来获取对象。
那这个get
方法在哪里被调用的呢,就是我们最熟悉的doGetBean
在doGetBean
里会检测bean的scope
,scope
不是单例和原型的会走这个else里的逻辑。然后回去scope
里调用get
。标注了@RefreshScope
注解的bean的scope
为refresh
,就会走这个else里的逻辑。
那逻辑就清楚了:在有值更新后,销毁scope里cache里的BeanLifecycleWrapper
,当有请求到达bean的时候,doGetBean
获取bean时,由于找不到原来的BeanLifecycleWrapper
,又新建一个BeanLifecycleWrapper
然后调用getBean
,返回一个全新的bean,在新的bean里就有刷新的配置了。如果没有值更新,下次当有请求到达bean的时候,doGetBean
获取bean时,BeanLifecycleWrapper
仍存在,返回的bean仍是上次那个。
到这里,自动刷新才算完成了。最后我们大白话总结一下。
总结
nacos有任务去检查远程的配置和本地的配置是否一样,不一样的话就发布spring的事件,spring监听到后会创建一个新的上下文环境获取最新配置然后刷新配置,并且删除掉scope的缓存,使下次获取bean时重新创建一个新的bean,在新的bean中就会有刷新后的配置。