Nacos回调机制

目录

1. 创建 ConfigService并实例化 ConfigService

1)ConfigFactory.createConfigService()创建ConfigService

2)NacosConfigService 的构造方法实例化 ConfigService :

HttpAgent

ClientWorker

--checkConfigInfo():

--LongPollingRunnable

    ① 本地检查

    ② 服务端检查

2. 添加Listener

ClientWorker.addTenantListeners()

CacheData类

3. 触发回调

cacheData.checkListenerMd5()

safeNotifyListener()


Nacos的动态配置:

nacos 服务端保存了配置信息,客户端连接到服务端之后,根据 dataID,group可以获取到具体的配置信息,当服务端的配置发生变更时,客户端会收到通知。

Nacos回调机制:

  • Nacos 服务端创建了相关的配置项(创建 ConfigService并实例化 ConfigService)。
  • 客户端就可以进行监听了(添加 Listener)
  • 客户端通过一个定时任务(cacheData.checkListenerMd5())来检查自己监听的配置项的数据的,一旦服务端的数据发生变化时,客户端将会获取到最新的数据,并将最新的数据保存在一个 CacheData 对象中,然后会重新计算 CacheData 的 md5 属性的值,此时(safeNotifyListener())就会对该 CacheData 所绑定的 Listener 触发 receiveConfigInfo 回调。

demo:

package com.learn.nacos;
 
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
 
import java.util.Properties;
import java.util.concurrent.Executor;
 
/**
 * @author liuliuyang001
 *
 */
public class NacosConfig {
 
    public static void main(String[] args) {
        try {
            String serverAddr = "127.0.0.1:8848";
            String dataId = "nacos-sdk-java-config";
            String group = "DEFAULT_GROUP";
            String content = "nacos-sdk-java-config:init";
            Properties properties = new Properties();
            properties.put("serverAddr", serverAddr);
 
            // 方式一:创建ConfigService
            ConfigService configService = NacosFactory.createConfigService(serverAddr);
 
            // 方式二:创建ConfigService
            //ConfigService configService = ConfigFactory.createConfigService(serverAddr);
 
            // 方式三:创建ConfigService
            //ConfigService configService = NacosFactory.createConfigService(properties);
 
            // 方式四:创建ConfigService
            //ConfigService configService = ConfigFactory.createConfigService(properties);
 
            // 获取服务状态
            System.out.println("当前线程:" + Thread.currentThread().getName() + " ,服务状态:" + configService.getServerStatus());
 
            Listener configListener = new Listener() {
                public Executor getExecutor() {
                    return null;
                }
 
                public void receiveConfigInfo(String configInfo) {
                    System.out.println("当前线程:" + Thread.currentThread().getName() + " ,监听到配置内容变化:" + configInfo);
                }
            };
 
            // 添加监听(有时候能监听到,有时候监听不到,为什么?)
            System.out.println("添加监听");
            configService.addListener(dataId, group, configListener);
            System.out.println("添加监听成功");
 
            // 发布配置(多次运行,有时候会获取不到配置内容,难道要等待一段时间才能获取?)
            System.out.println("发布配置");
            configService.publishConfig(dataId, group, content);
            System.out.println("发布配置成功");
          
            try {
                Thread.sleep(1000 * 3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
            // 本地缓存:\nacos\config\fixed-127.0.0.1_8848_nacos\snapshot\DEFAULT_GROUP
            // 读取配置
            String configAfterPublish = configService.getConfig(dataId, group, 3000);
            System.out.println("当前线程:" + Thread.currentThread().getName() + " ,发布配置后获取配置内容:" + configAfterPublish);
 
            // 重新发布配置
            content = "sdk-java-config:update";
            System.out.println("重新发布配置");
            boolean rePublishFlag = configService.publishConfig(dataId, group, content);
            if(rePublishFlag) {
                System.out.println("重新发布配置成功");
            } else {
                System.out.println("重新发布配置失败");
            }
 
            // 重新读取配置
            String configAfterUpdate = configService.getConfig(dataId, group, 3000);
            System.out.println("当前线程:" + Thread.currentThread().getName() + " ,重新发布配置后获取配置内容:" + configAfterUpdate);
 
            try {
                Thread.sleep(1000 * 30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
            // 移除配置
            System.out.println("移除配置");
            boolean removeFlag = configService.removeConfig(dataId, group);
            if(removeFlag) {
                System.out.println("移除配置成功");
            } else {
                System.out.println("移除配置失败");
            }
 
         try {
                Thread.sleep(1000 * 3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
          
            String configAfterRemove = configService.getConfig(dataId, group, 3000);
            System.out.println("当前线程:" + Thread.currentThread().getName() + " ,移除配置后获取配置内容:" + configAfterRemove);
 
          /*  // 取消监听
            System.out.println("取消监听");
            configService.removeListener(dataId, group, configListener);
            System.out.println("取消监听成功");*/
        } catch (NacosException e) {
            e.printStackTrace();
        }
    }
}

1. 创建 ConfigService并实例化 ConfigService

1)ConfigFactory.createConfigService()创建ConfigService

实际是通过反射调用了 NacosConfigService 的构造方法来创建 ConfigService 的,而且是有一个 Properties 参数的构造方法。
需要注意的是,这里并没有通过单例或者缓存技术,也就是说每次调用都会重新创建一个 ConfigService的实例。

2)NacosConfigService 的构造方法实例化 ConfigService :

实例化时主要是初始化了两个对象,他们分别是:

  • HttpAgent
  • ClientWorker

HttpAgent

agent 是通过装饰者模式实现的,ServerHttpAgent 是实际工作的类,MetricsHttpAgent 在内部也是调用了 ServerHttpAgent 的方法,另外加上了一些统计操作,所以我们只需要关心 ServerHttpAgent 的功能就可以了。

ClientWorker

  • 第一个线程池是只拥有一个线程用来执行定时任务的 executor,executor 每隔 10ms 就会执行一次 checkConfigInfo() 方法,从方法名上可以知道是每 10 ms 检查一次配置信息。
  • 第二个线程池是一个普通的线程池,从 ThreadFactory 的名称可以看到这个线程池是做长轮询LongPollingRunnable的。

--checkConfigInfo()

checkConfigInfo方法是取出了一批任务,然后提交给executorService线程池去执行,执行的任务就是LongPollingRunnable,每个任务都有一个 taskId。

--LongPollingRunnable

    ① 本地检查

首先取出与该 taskId 相关的 CacheData,然后对 CacheData 进行检查,包括本地配置检查和监听器的 md5 检查,本地检查主要是做一个故障容错,当服务端挂掉后,Nacos 客户端可以从本地的文件系统中获取相关的配置信息,如下图所示:

    ② 服务端检查

通过 checkUpdateDataIds() 方法从服务端获取那些值发生了变化的 dataId 列表,
通过 getServerConfig 方法,根据 dataId 到服务端获取最新的配置信息,接着将最新的配置信息保存到 CacheData 中。
最后调用 CacheData 的 checkListenerMd5 方法,可以看到该方法在第一部分也被调用过。

在该任务的最后,也就是在 finally 中又重新通过 executorService 提交了本任务。

2. 添加Listener

ClientWorker.addTenantListeners()

调用了ClientWorker 的 addTenantListeners 方法为 ConfigService 来添加一个 Listener

  • 首先根据 dataId,group 和当前的场景获取一个 CacheData 对象
  • 然后将当前要添加的 listener 对象添加到 CacheData 中去。

listener 最终是被这里的 CacheData 所持有了,那 listener 的回调方法 receiveConfigInfo 就应该是在 CacheData 中触发的。CacheData 是出现频率非常高的一个类,在 LongPollingRunnable 的任务中,几乎所有的方法都围绕着 CacheData 类,现在添加 Listener 的时候,实际上该 Listener 也被委托给了 CacheData。

CacheData类

成员变量:

  • dataId,group,content,taskId:跟配置相关的属性
  • listeners:该 CacheData 所关联的所有 listener,不过不是保存的原始的 Listener 对象,而是包装后的ManagerListenerWrap 对象,该对象除了持有 Listener 对象,还持有了一个 lastCallMd5 属性。
  •  md5:根据当前对象的 content 计算出来的 md5 值。

3. 触发回调

cacheData.checkListenerMd5()

在 ClientWorker 中的定时任务中,启动了一个长轮询的任务:LongPollingRunnable,该任务多次执行了 cacheData.checkListenerMd5() 方法:

该方法会检查 CacheData 当前的 md5 与 CacheData 持有的所有 Listener 中保存的 md5 的值是否一致,如果不一致,就执行一个安全的监听器的通知方法:safeNotifyListener(),通应该是通知 Listener 的使用者,该 Listener 所关注的配置信息已经发生改变了。

safeNotifyListener()

那 CacheData 的 md5 值是何时发生改变的呢?我们可以回想一下,在上面的 LongPollingRunnable 所执行的任务中,在获取服务端发生变更的配置信息时,将最新的 content 数据写入了 CacheData 中,我们可以看下该方法如下:

在长轮询的任务中,当服务端配置信息发生变更时,客户端将最新的数据获取下来之后,保存在了 CacheData 中,同时更新了该 CacheData 的 md5 值,所以当下次执行 checkListenerMd5 方法时,就会发现当前 listener 所持有的 md5 值已经和 CacheData 的 md5 值不一样了,也就意味着服务端的配置信息发生改变了,这时就需要将最新的数据通知给 Listener 的持有者。

Nacos 并不是通过推的方式将服务端最新的配置信息发送给客户端的,而是客户端维护了一个长轮询的任务,定时去拉取发生变更的配置信息,然后将最新的数据推送给 Listener 的持有者。

### 关于回调地址的配置及相关 API 使用说明 #### 回调地址的概念与作用 回调地址通常用于服务之间的通信机制中,当某个事件发生时,目标服务会向指定的回调地址发起请求以通知该事件的发生。例如,在钉钉开发场景下,如果用户的回调地址为 `https://dingdingCallBack.cn/dingtalk/callback`[^1],则表示钉钉会在特定情况下(如接收到消息或状态更新)向此 URL 发起 HTTP 请求。 为了确保回调功能正常运行,需注意以下几点: - **安全性**:建议使用 HTTPS 协议保护数据传输的安全性。 - **可用性**:确保服务器能够稳定响应来自外部的服务请求。 - **路径匹配**:确认回调地址中的路径部分与接收方处理逻辑一致。 #### 配置回调地址的示例代码 以下是基于 Java Spring Boot 的简单实现方式,展示如何设置并监听回调接口: ```java @RestController @RequestMapping("/dingtalk/callback") // 定义回调路径 public class DingTalkCallbackController { @PostMapping // 响应 POST 请求 public ResponseEntity<String> handleDingTalkCallback(@RequestBody Map<String, Object> payload) { System.out.println("Received callback from DingTalk: " + payload); return ResponseEntity.ok("Success"); // 返回成功状态给调用方 } } ``` 上述代码片段定义了一个 RESTful 接口 `/dingtalk/callback` 来接受来自钉钉或其他系统的回调请求,并打印日志以便调试和记录信息。 --- #### 结合 Kafka 生产者的回调机制 在某些分布式系统架构设计里,除了传统的 HTTP 回调外,还可以利用消息队列工具如 Apache Kafka 实现更高效的消息传递模式。下面给出一段关于 Kafka 生产者异步发送带回调的例子[^2]: ##### Maven 依赖引入 (POM 文件) 首先需要添加必要的库支持到项目的构建文件当中: ```xml <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>3.0.0</version> </dependency> ``` ##### 异步发送消息的 API 示例 接着编写具体的业务逻辑代码如下所示: ```java import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; import java.util.Properties; import java.util.concurrent.ExecutionException; public class KafkaAsyncSenderExample { private static final String TOPIC_NAME = "test-topic"; public static void main(String[] args) throws ExecutionException, InterruptedException { Properties props = new Properties(); props.put("bootstrap.servers", "localhost:9092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) { ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC_NAME, null, "Hello Kafka!"); // 设置回调函数 producer.send(record, (metadata, exception) -> { if (exception != null) { exception.printStackTrace(); // 如果失败,则输出错误堆栈 } else { StringBuilder sb = new StringBuilder() .append("Message sent successfully! Topic=").append(metadata.topic()) .append(", Partition=").append(metadata.partition()) .append(", Offset=").append(metadata.offset()); System.out.println(sb.toString()); // 成功后的元数据反馈 } }); Thread.sleep(500); // 等待一段时间让消息真正发出后再退出程序 } } } ``` 以上展示了如何通过 Kafka 的生产者客户端执行带有回调特性的异步操作流程。 --- #### Nacos 中动态获取配置服务实例的方式 对于微服务体系下的应用来说,可能还会涉及到像阿里巴巴开源项目 Nacos 这样的注册中心组件管理情况。其中提到过一种做法就是借助反射手段去初始化它的核心类对象之一 —— `NacosConfigService` ,具体过程可以参见相关内容描述[^3] : 每当应用程序想要访问远程存储好的参数集合时候,就会触发一次新的连接建立动作从而获得最新的设定值副本而不是长期维持单一不变的对象引用关系 。这种方式虽然增加了额外开销但是也带来了灵活性优势即允许随时调整全局范围内的默认行为而无需重启任何节点即可生效更改效果 。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值