最近项目中完成了配置中心从Spring Cloud Config Server到Alibaba Nacos的迁移工作,那Spring Cloud应用集成Nacos后,配置的自动刷新机制是什么呢?配置变更后,又是怎样影响已经装配好的Bean?在Nacos官方文档中,找到以下一段关于配置自动刷新配置的描述:
通过Spring Cloud原生注解@RefreshScope实现配置自动更新:
public class ConfigController {
private boolean useLocalCache;
public boolean get() {
return useLocalCache;
}
}
示例
我们创建一个简单的Spring Cloud项目来验证配置的自动更新是否生效。
引入相关依赖并使用上面的
ConfigController
定义来获取实时的配置值。在Nacos中更新配置值后,调用
/config/get
接口返回了配置的新值。查看日志
2024-01-18 10:58:45.143 INFO 2289 --- [or-127.0.0.1-59] com.alibaba.nacos.common.remote.client : [7ff5ec9b-478a-40e7-b505-77af547b7d6d_config-0] Receive server push request, request = ConfigChangeNotifyRequest, requestId = 133
2024-01-18 10:58:45.144 INFO 2289 --- [or-127.0.0.1-59] c.a.n.client.config.impl.ClientWorker : [7ff5ec9b-478a-40e7-b505-77af547b7d6d_config-0] [server-push] config changed. dataId=config-demo.yaml, group=DEFAULT_GROUP,tenant=null
2024-01-18 10:58:45.144 INFO 2289 --- [or-127.0.0.1-59] com.alibaba.nacos.common.remote.client : [7ff5ec9b-478a-40e7-b505-77af547b7d6d_config-0] Ack server push request, request = ConfigChangeNotifyRequest, requestId = 133
2024-01-18 10:58:45.553 INFO 2289 --- [s.client.Worker] c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.1_8848] [data-received] dataId=config-demo.yaml, group=DEFAULT_GROUP, tenant=, md5=00b77727c9b4a661b8ae12cd7de1395f, content=useLocalCache: true, type=yaml
2024-01-18 10:58:45.557 INFO 2289 --- [ternal.notifier] c.a.nacos.client.config.impl.CacheData : [fixed-127.0.0.1_8848] [notify-context] dataId=config-demo.yaml, group=DEFAULT_GROUP, md5=00b77727c9b4a661b8ae12cd7de1395f
2024-01-18 10:58:45.557 INFO 2289 --- [s.client.Worker] c.a.nacos.client.config.impl.CacheData : [fixed-127.0.0.1_8848] [notify-listener] time cost=0ms in ClientWorker, dataId=config-demo.yaml, group=DEFAULT_GROUP, md5=00b77727c9b4a661b8ae12cd7de1395f, listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1@e15133a
2024-01-18 10:58:45.770 WARN 2289 --- [ternal.notifier] c.a.c.n.c.NacosPropertySourceBuilder : Ignore the empty nacos configuration and get it based on dataId[config-demo] & group[DEFAULT_GROUP]
2024-01-18 10:58:46.093 INFO 2289 --- [ternal.notifier] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-config-demo.yaml,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-config-demo,DEFAULT_GROUP'}]
2024-01-18 10:58:46.255 INFO 2289 --- [ternal.notifier] o.s.boot.SpringApplication : No active profile set, falling back to 1 default profile: "default"
2024-01-18 10:58:46.264 INFO 2289 --- [ternal.notifier] o.s.boot.SpringApplication : Started application in 0.685 seconds (JVM running for 119.187)
2024-01-18 10:58:46.337 INFO 2289 --- [ternal.notifier] o.s.c.e.event.RefreshEventListener : Refresh keys changed: [useLocalCache]
2024-01-18 10:58:46.337 INFO 2289 --- [ternal.notifier] c.a.nacos.client.config.impl.CacheData : [fixed-127.0.0.1_8848] [notify-ok] dataId=config-demo.yaml, group=DEFAULT_GROUP, md5=00b77727c9b4a661b8ae12cd7de1395f, listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1@e15133a ,cost=780 millis.
添加@RefreshScope
注解后,更新Nacos配置中心的配置可以实现应用的自动更新。
注意:Nacos 2.x版本之后使用gRpc通信框架同步配置变更,请确保9848端口开放否则无法获取最新的配置。示例使用的相关版本: - Nacos Server: 2.2.3 - Spring Boot: 2.7.16 - Spring Cloud: 2021.0.8
源码剖析
2.1 Nacos是如何同步配置数据并引发配置在应用内更新的?
1. 程序启动时,spring-cloud-alibaba-nacos-config
集成包会自动装配一个NacosPropertySourceLocator
。
false) (proxyBeanMethods =
"spring.cloud.nacos.config.enabled", matchIfMissing = true) (name =
public class NacosConfigBootstrapConfiguration {
...
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
NacosPropertySourceLocator
继承了Spring Cloud Context
的PropertySourceLocator
接口并实现了locate 方法。PropertySourceLocator
是Spring Cloud Context用来支持用户扩展定位外部配置源,比如Nacos或其他分布式配置中心(Spring Cloud Config Server或Git仓库),详细内容可阅读Reference 1. Spring Cloud Context。
在locate
方法中,Nacos定位到应用对应dataId的配置内容并返回了CompositePropertySource
。最终创建一个ApplicationContext
作为子上下文添加到应用程序中,子上下文中的属性配置会覆盖父上下文中配置,并通过BeanFactory
初始化Bean。
public class NacosPropertySourceLocator implements PropertySourceLocator {
public PropertySource<?> locate(Environment env) {
...
CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
loadSharedConfiguration(composite);
loadExtConfiguration(composite);
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
}
2. 程序启动的同时,Nacos为每个dataId注册一个单独的listener缓存到CacheData
中,并在接收到配置更新内容后,回调Listener中的方法并推送一个RefreshEvent
事件。
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
public void innerReceive(String datad, String group,String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));
...
}
});
try {
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
...
}
}
3. RefreshEventListener
监听RefreshEvent
事件并更新应用程序上下文的属性和配置。
public class RefreshEventListener implements SmartApplicationListener {
...
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
}
通过分析日志和阅读源代码,我们基本理解了Nacos的初始化过程以及配置同步的基本逻辑。然而,是否在类上添加@RefreshScope
注解并不影响Nacos的同步最新配置以及发布RefreshEvent
事件。那么,为什么只有在添加@RefreshScope
后,配置才会自动刷新呢?
2.2 @RefreshScope是如何更新配置的?
1. 查看@RefreshScope
源代码定义,@RefreshScope
继承自@Scope
注解。ScopedProxyMode.TARGET_CLASS
的原理是使用CGLIB创建一个基于类的代理,这个代理会拦截所有对Bean的调用,并根据Bean的作用域(例如,原型作用域或会话作用域)来决定是否需要生成新的实例。应用启动时,Spring会为标记了@RefreshScope
注解的Bean生成两个实例,一个代理类和一个实际使用的Bean。代理类的作用是拦截所有对Bean的请求,在请求到达之前,检查配置是否已经刷新。如果配置已经刷新,代理会先销毁旧的bean实例,然后创建一个新的bean实例。
public RefreshScope {
/**
* @see Scope#proxyMode()
* @return proxy mode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
Spring Bean的作用域决定了Spring IoC容器如何创建Bean的实例。Spring定义了以下几种作用域:
单例(Singleton):在整个Spring IoC容器中,只创建Bean的一个实例。这是默认的作用域。
原型(Prototype):每次请求都会创建一个新的Bean实例。
请求(Request):在一个HTTP请求中,一个Bean对应一个实例。
会话(Session):在一个HTTP会话中,一个Bean对应一个实例。这个作用域只在基于web的Spring ApplicationContext情境有效。
Request,Session只针对基于web的Spring ApplicationContext上下文有效。RefreshScope
是Spring Cloud在此基础上扩展的一种作用域。BeanFactory
对于不同作用域的Bean选择不同的Scope实现来管理Bean的生命周期。
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
protected <T> T doGetBean(String name, Class<T> requiredType, Object[] args, boolean typeCheckOnly) throws BeansException {
...
if (mbd.isSingleton()) {
...
}
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
...
}
else {
String scopeName = mbd.getScope();
if (!StringUtils.hasLength(scopeName)) {
throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
}
Scope scope = this.scopes.gt(scopeName);
...
}
}
2. RefreshScope
继承了GenericScope
的实现,应用启动后,会将生成好的Bean实例放到cache中,确保配置没有刷新的情况下,每次获取到的是同一个实例,减少应用的开销。如果缓存中不存在,则调用BeanFactory重新创建Bean。
public class GenericScope
implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {
...
public Object get(String name, ObjectFactory<?> objectFactory) {
BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); // 存入缓存
this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
try {
return value.getBean();
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
private static class BeanLifecycleWrapper {
public Object getBean() {
if (this.bean == null) {
synchronized (this.name) {
if (this.bean == null) {
this.bean = this.objectFactory.getObject(); // 创建Bean
}
}
}
return this.bean;
}
}
}
3. Nacos配置更新后推送RefreshEvent
事件,RefreshEventListener
监听RefreshEvent
事件调用ContextRefresher#refresh
方法。该方法会重新从远端配置源获取最新的配置信息,并调用RefreshScope#refershAll
方法清空RefreshScope中的缓存,并在下次获取Bean时重新创建。
public abstract class ContextRefresher {
...
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment(); // 将新配置加载到环境中
this.scope.refreshAll(); // 刷新配置
return keys;
}
}
public class RefreshScope extends GenericScope
implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {
...
"Dispose of the current instance of all beans " (description =
+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
}
public class GenericScope
implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {
...
public void destroy() {
List<Throwable> errors = new ArrayList<Throwable>();
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear(); // 清空RefreshScope的缓存,下次重新创建
...
}
总结
Scope
是Spring IoC用于管理Bean生命周期和作用域的重要概念,Spring IoC也是Spring框架的核心。加深对Spring IoC的理解对于学习Spring框架很有必要,详细信息可阅读Reference 2. Core Technologies。
Reference
1. Spring Cloud Context: Application Context Services :https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#_application_context_hierarchies
2. Core Technologies: https://docs.spring.io/spring-framework/docs/5.3.31/reference/html/core.html#beans
关于领创集团