前言
nacos 热更新主要分为全局环境变量热更新和局部 Bean 字段热更新,分别由 @NacosPropertySource 和 @NacosValue 的 autoRefreshed 字段控制,接下来分别看看原理。
全局环境变量热更新
全局环境变量热更新由 @NacosPropertySource 的 autoRefreshed 字段控制,接下来看看源码。
// com.alibaba.nacos.spring.core.env.NacosPropertySourcePostProcessor#doProcessPropertySource
protected void doProcessPropertySource(String beanName, BeanDefinition beanDefinition) {
// 根据注解或者xml配置解析bean对象中配置的配置源
List<NacosPropertySource> nacosPropertySources = buildNacosPropertySources(
beanName, beanDefinition);
// Add Orderly
for (NacosPropertySource nacosPropertySource : nacosPropertySources) {
// 将配置源添加到环境对象中
addNacosPropertySource(nacosPropertySource);
// 将NacosPropertySource注解中的非空字符串属性和全局配置合并返回
Properties properties = configServiceBeanBuilder
.resolveProperties(nacosPropertySource.getAttributesMetadata());
// 添加配置变更监听器
addListenerIfAutoRefreshed(nacosPropertySource, properties, environment);
}
}
// com.alibaba.nacos.spring.core.env.NacosPropertySourcePostProcessor#addListenerIfAutoRefreshed
public static void addListenerIfAutoRefreshed(
final NacosPropertySource nacosPropertySource, final Properties properties,
final ConfigurableEnvironment environment) {
// 如果NacosPropertySource未开启自动刷新,直接返回
if (!nacosPropertySource.isAutoRefreshed()) { // Disable Auto-Refreshed
return;
}
// 省略部分代码
try {
ConfigService configService = nacosServiceFactory.createConfigService(properties);
Listener listener = new AbstractListener() {
@Override
public void receiveConfigInfo(String config) {
String name = nacosPropertySource.getName();
// 使用新配置数据构建配置员
NacosPropertySource newNacosPropertySource = new NacosPropertySource(
dataId, groupId, name, config, type);
// 拷贝旧源配置的元数据
newNacosPropertySource.copy(nacosPropertySource);
MutablePropertySources propertySources = environment
.getPropertySources();
// 替换环境对象中的源配置
propertySources.replace(name, newNacosPropertySource);
}
};
// 省略部分代码
configService.addListener(dataId, groupId, listener);
}
catch (NacosException e) {
}
}
从上面的代码可以看到,@NacosPropertySource 的 autoRefreshed 字段仅控制环境对象中的配置源更新。
局部 Bean 字段热更新
接下来看看局部 Bean 字段更新,局部 Bean 字段更新主要由 @NacosValue 的 autoRefreshed 字段控制,接下来同样看看源码。
// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor
@Override
public Object postProcessBeforeInitialization(Object bean, final String beanName)
throws BeansException {
doWithFields(bean, beanName);
doWithMethods(bean, beanName);
return super.postProcessBeforeInitialization(bean, beanName);
}
// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor#doWithFields
private void doWithFields(final Object bean, final String beanName) {
// 解析bean对象中字段的NacosValue注解
ReflectionUtils.doWithFields(bean.getClass(),
new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException {
NacosValue annotation = getAnnotation(field, NacosValue.class);
doWithAnnotation(beanName, bean, annotation, field.getModifiers(),
null, field);
}
});
}
// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor#doWithAnnotation
private void doWithAnnotation(String beanName, Object bean, NacosValue annotation,
int modifiers, Method method, Field field) {
if (annotation != null) {
// 静态字段不处理
if (Modifier.isStatic(modifiers)) {
return;
}
// 开启自动刷新
if (annotation.autoRefreshed()) {
// 解析占位符
String placeholder = resolvePlaceholder(annotation.value());
// 将占位符,bean对象,字段信息等缓存在内存中
NacosValueTarget nacosValueTarget = new NacosValueTarget(bean, beanName,
method, field, annotation.value());
put2ListMap(placeholderNacosValueTargetMap, placeholder,
nacosValueTarget);
}
}
}
从上述代码中可以看到,在 Bean 对象初始化前,会解析对象中添加了 @NacosValue 的字段或者方法,并将相关的字段、对象、注解信息缓存在内存中。接下来看看这些字段是如何实现热更新的。
// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor#onApplicationEvent
public void onApplicationEvent(NacosConfigReceivedEvent event) {
// In to this event receiver, the environment has been updated the
// latest configuration information, pull directly from the environment
// fix issue #142
for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap
.entrySet()) {
String key = environment.resolvePlaceholders(entry.getKey());
// 从环境变量中取出,因为环境变量更新是在事件发布之前应用事件发布前完成的
// 所以此处获取到的值是已经更新完成之后的数据
String newValue = environment.getProperty(key);
if (newValue == null) {
continue;
}
List<NacosValueTarget> beanPropertyList = entry.getValue();
for (NacosValueTarget target : beanPropertyList) {
// 比较新旧数据md5校验和
String md5String = MD5Utils.md5Hex(newValue, "UTF-8");
boolean isUpdate = !target.lastMD5.equals(md5String);
if (isUpdate) {
target.updateLastMD5(md5String);
Object evaluatedValue = resolveNotifyValue(target.nacosValueExpr, key, newValue);
// 更新bean对象字段或方法
if (target.method == null) {
setField(target, evaluatedValue);
}
else {
setMethod(target, evaluatedValue);
}
}
}
}
}
可以看到,当接收到配置变更事件时,会遍历内存中的需要自动更新的 Bean 字段信息,对比 MD5 校验和,如果发现存在变更,则更新 Bean 字段或方法。(此处存在一个疑问,每接收到一个事件都会遍历所有 Bean 字段信息,效率是否较低?)
这里还有一个点需要注意,新的配置数据是直接从环境对象中取出的,这也意味着 @NacosValue 字段的自动更新是会受 @NacosPropertySource 自动更新的影响的。如果 @NacosPropertySource 未开启自动更新,即使 @NacosValue 开启自动更新最终还是无法更新。
更新事件接送
接下来再来验证一下上面所说的,全局环境变量的更新是局部 Bean 字段更新之前完成的。
// com.alibaba.nacos.client.config.impl.CacheData#safeNotifyListener
private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
final String md5, final String encryptedDataKey, final ManagerListenerWrap listenerWrap) {
NotifyTask job = new NotifyTask() {
@Override
public void run() {
long start = System.currentTimeMillis();
ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader appClassLoader = listener.getClass().getClassLoader();
ScheduledFuture<?> timeSchedule = null;
try {
// 省略部分代码
timeSchedule = getNotifyBlockMonitor().schedule(
new LongNotifyHandler(listener.getClass().getSimpleName(), dataId, group, tenant, md5,
notifyWarnTimeout, Thread.currentThread()), notifyWarnTimeout,
TimeUnit.MILLISECONDS);
listenerWrap.inNotifying = true;
listener.receiveConfigInfo(contentTmp);
// 省略部分代码
} catch (NacosException ex) {
} catch (Throwable t) {
} finally {
}
}
};
// 省略部分代码
}
// com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#receiveConfigInfo
public void receiveConfigInfo(String content) {
onReceived(content);
publishEvent(content);
}
// com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#publishEvent
private void publishEvent(String content) {
NacosConfigReceivedEvent event = new NacosConfigReceivedEvent(configService,
dataId, groupId, content, configType);
applicationEventPublisher.publishEvent(event);
}
// com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#onReceived
private void onReceived(String content) {
delegate.receiveConfigInfo(content);
}
可以看到,先执行 Nacos 原生监听器的 receiveConfigInfo 方法,再发布应用事件。
总结
- @NacosPropertySource 的 autoRefreshed 字段控制全局环境变量的更新。
- @NacosValue 的 autoRefreshed 字段控制局部 Bean 对象字段更新。
- Bean 对象字段更新还是会受到 @NacosPropertySource 的 autoRefreshed 字段的影响,只有 @NacosPropertySource 和 @NacosValue 同时开启自动刷新才能真正自动更新。