likes
comments
collection
share

好奇怪,@Value不能获取最新的配置?

作者站长头像
站长
· 阅读数 16

小红书的姐妹说大润发榴莲20一斤,兴奋跑去发现35一斤,吃不起,根本吃不起。一看发帖时间一两星期之前了。气~

前言

公司使用Apollo做配置中心,也没怎么具体了解过。只知道有配置使用时用@Value就能获取到了,而且还能自动刷新。遂一直以为@Value本身有自动刷新功能。

直到有一天用nacos做配置中心。哎?我的配置怎么不能自动刷新了?@Value出问题了吗?

我的controller

好奇怪,@Value不能获取最新的配置?

我的nacos配置

好奇怪,@Value不能获取最新的配置?

修改nacos配置

account.name改为杜甫

好奇怪,@Value不能获取最新的配置?

可以看到控制台也打印了refresh keys changed

好奇怪,@Value不能获取最新的配置?

可是调用接口得到的值仍是更新前的

好奇怪,@Value不能获取最新的配置?

后面才知道nacos要配合@RefreshScope使用才能自动刷新。Apollo的@Value自动刷新功能算是一个机制了。

于是加上@RefreshScope试验一下

好奇怪,@Value不能获取最新的配置?

先把nacos中的account.name配置改为李白,启动项目之后,再改为杜甫,然后调用接口获取account.name

好奇怪,@Value不能获取最新的配置?

这次获取到的配置就是最新的了。

为什么Apollo能实现@Value的自动刷新,而nacos需要配合@RefreshScope呢?

本文旨在分析Apollo和nacos为什么能实现配置自动,和他们的实现方式有什么不同。

先简单看一下Apollo为什么能实现@Value的自动更新

Apollo的自动更新

Apollo的自动更新比较简单

AutoUpdateConfigChangeListener#onChange

AutoUpdateConfigChangeListener是一个监听器,监听ConfigChangeEvent事件。客户端会一直向远程拉取,当配置发生变化就会发布这个事件。

好奇怪,@Value不能获取最新的配置?

可以看到通过springValueRegistry获取到了SpringValue的集合。SpringValue是Apollo维护的来更新spring中@Value值变化后的类。里面维护了@Value的占位符,bean,field等信息。通过继承BeanPostProcessor实现postProcessBeforeInitialization来维护起来的,这里不管他,只要知道他维护了@Value注释的字段和bean信息就行了。

继续看updateSpringValue

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()方法

好奇怪,@Value不能获取最新的配置?

我们这里挑一个看

好奇怪,@Value不能获取最新的配置?

好奇怪,@Value不能获取最新的配置?

可以看到这里先获取bean(弱引用),然后更新这个字段的值(Field.set()方法)。

总结:

Apollo自己维护了@Value的信息,当有变化时,Apollo直接更新使用@Value注解bean的字段值

nacos的自动更新

nacos的自动更新使用了spring的机制,因此比较复杂,所以nacos的自动更新机制我们详细看看。

在nacos-config的spring.factoies中,注入了NacosConfigAutoConfiguration

好奇怪,@Value不能获取最新的配置?

NacosConfigAutoConfiguration里面注入了一个NacosContextRefresher的Bean

好奇怪,@Value不能获取最新的配置?

这个Bean实现了ApplicationListener,监听ApplicationReadyEvent事件,这个事件会在spring就绪后发布,也就是SpringApplication#run()的最后。

好奇怪,@Value不能获取最新的配置?

好奇怪,@Value不能获取最新的配置?

好奇怪,@Value不能获取最新的配置?

继续到registerNacosListener里,重点看一下画红圈的部分

好奇怪,@Value不能获取最新的配置?

applicationContext.publishEvent,这里发布了一个RefreshEvent事件,这个事件会由RefreshEventListener来处理,这个事件的处理我们下面说。

先来看configService.addListenerconfigService是nacos具体实现配置发布、更新、获取、删除的实现类,具体就是拼接url和参数调用远程的nacos服务。addListener就是添加监听器

好奇怪,@Value不能获取最新的配置?

好奇怪,@Value不能获取最新的配置?

到这里nacos添加上了配置更新的监听器,那么这个监听器是在哪触发的呢?

还是ConfigService,它的实现类NacosConfigService中的构造方法中。

好奇怪,@Value不能获取最新的配置?

在构造方法中,创建了ClientWorker

好奇怪,@Value不能获取最新的配置?

ClientWorker会启动定时任务调用checkConfigInfo()。还创建了一个线程池赋值给executorService。

checkConfigInfo()中会使用这个线程池执行任务

好奇怪,@Value不能获取最新的配置?

可以看到提交了LongPollingRunnable这个任务,继续看一下他的run方法

这个方法有点长,只截取一部分

好奇怪,@Value不能获取最新的配置?

可以看到如果 远程发现了有配置更新,就会更新缓存为最新值,checkUpdateDataIdsgetServerConfig就是http请求nacos服务端获取数据,这里不展开了,感兴趣的可以自己看一下。

到此为止,我们动态更新的配置已经可以被nacos检测到了。那为什么@Value返回的值还是旧的呢。

这就要回到我们刚才略过的applicationContext.publishEvent,发布的RefreshEvent事件了。

好奇怪,@Value不能获取最新的配置?

RefreshEventListener监听RefreshEvent事件,在RefreshEventListenerhandle里对事件做处理

好奇怪,@Value不能获取最新的配置?好奇怪,@Value不能获取最新的配置?

refresh方法就是能刷新配置的重点了。refresh方法里一共调用了两个方法我们一个一个来看

先看refreshEnvironment

好奇怪,@Value不能获取最新的配置?

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容器注入属性的接口)。所以在prepareContextapplyInitializers方法中就会调用它的initialize方法。

initialize中会调用PropertySourceLocator接口的locateCollection。nacos的NacosPropertySourceLocator就实现了这个接口,会向nacos获取配置,这样nacos里的配置就是最新的了。再往下他会塞到environment里,这样this.context.getEnvironment().getPropertySources()就能获取到最新的配置了。

addConfigFilesToEnvironment做完这些工作后就会把新创建的spring上下文关闭。

看到这好像大功告成了,但其实还有个问题,配置变了,bean里通过@Value获取到的值可没变。怎么把它的值给更新呢?spring选择了非常简便的方法,弄个新的出来。

继续看refresh里调用的refreshAll()方法

好奇怪,@Value不能获取最新的配置?

好奇怪,@Value不能获取最新的配置?好奇怪,@Value不能获取最新的配置?

这里的this.cacheBeanLifecycleWrapperCache,里边存放BeanLifecycleWrapperBeanLifecycleWrapper里存放了bean的名称和ObjectFactoryObjectFactory提供了一个getObject()方法,用于获取对象实例。

好奇怪,@Value不能获取最新的配置?

那代码把cache清空,把BeanLifecycleWrapper都销毁有什么用呢?我们先去看一下BeanLifecycleWrapper是怎么放进去的。

看一下Scope的实现类GenericScopeget方法

好奇怪,@Value不能获取最新的配置?

get方法创建了新的BeanLifecycleWrapper放入到cache里。注意这里的cache.put()最终调用的是ConcurrentHashMapputIfAbsent,如果已有可就不放了。返回的即是BeanLifecycleWrappergetBean()方法。

好奇怪,@Value不能获取最新的配置?

getBean就会调用ObjectFactorygetObject方法来获取对象。

那这个get方法在哪里被调用的呢,就是我们最熟悉的doGetBean

好奇怪,@Value不能获取最新的配置?

doGetBean里会检测bean的scopescope不是单例和原型的会走这个else里的逻辑。然后回去scope里调用get。标注了@RefreshScope注解的bean的scoperefresh,就会走这个else里的逻辑。

那逻辑就清楚了:在有值更新后,销毁scope里cache里的BeanLifecycleWrapper,当有请求到达bean的时候,doGetBean获取bean时,由于找不到原来的BeanLifecycleWrapper,又新建一个BeanLifecycleWrapper然后调用getBean,返回一个全新的bean,在新的bean里就有刷新的配置了。如果没有值更新,下次当有请求到达bean的时候,doGetBean获取bean时,BeanLifecycleWrapper仍存在,返回的bean仍是上次那个。

到这里,自动刷新才算完成了。最后我们大白话总结一下。

总结

nacos有任务去检查远程的配置和本地的配置是否一样,不一样的话就发布spring的事件,spring监听到后会创建一个新的上下文环境获取最新配置然后刷新配置,并且删除掉scope的缓存,使下次获取bean时重新创建一个新的bean,在新的bean中就会有刷新后的配置。