当前位置: 首页 > news >正文

手写分布式配置中心(六)整合springboot(自动刷新)

对于springboot配置自动刷新,原理也很简单,就是在启动过程中用一个BeanPostProcessor去收集需要自动刷新的字段,然后在springboot启动后开启轮询任务即可。
不过需要对之前的代码再次做修改,因为springboot的配置注入@value("${}"),允许多个${}和嵌套,所以不能确定简单的确定用到了那个配置,本文为了简单就把所有的配置都认为需要动态刷新,实际用的时候可以在application.yml中配置需要动态刷新的配置id列表。代码在https://gitee.com/summer-cat001/config-center。其中设计到的原理都在之前的一篇文章中,感兴趣可以去看看springboot配置注入增强(二)属性注入的原理_springboot bean属性增强-CSDN博客

新增注解

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigRefresh {
}

加上这个注解的字段并且字段上有@value注解就会自动刷新

收集自动刷新的字段

这里会收集自动刷新的字段,并加到ConfigCenterClient的refreshFieldValueList中。长轮询会从这里取数据进行对比,如果发生变化就更新bean中的字段

@Slf4j
public class ConfigRefreshAnnotationBeanPostProcessor implements ApplicationRunner, BeanPostProcessor, BeanFactoryAware, EnvironmentAware {private Environment environment;private ConfigurableBeanFactory beanFactory;@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {if (!(beanFactory instanceof ConfigurableBeanFactory)) {log.warn("ConfigurableBeanFactory requires a ConfigurableListableBeanFactory");return;}this.beanFactory = (ConfigurableBeanFactory) beanFactory;}@Overridepublic Object postProcessBeforeInitialization(Object bean, final String beanName) throws BeansException {if (beanFactory != null) {ReflectionUtils.doWithFields(bean.getClass(), field -> {try {ConfigRefresh configRefresh = AnnotationUtils.getAnnotation(field, ConfigRefresh.class);if (configRefresh == null) {return;}Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);if (valueAnnotation == null) {return;}String value = valueAnnotation.value();String relValue = beanFactory.resolveEmbeddedValue(value);ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);configCenterClient.addRefreshFieldValue(bean, field, relValue);} catch (Exception e) {log.error("set bean field fail,beanName:{},fieldName:{}", bean.getClass().getName(), field.getName(), e);}});}return bean;}@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}@Overridepublic void run(ApplicationArguments args) {ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);configCenterClient.startSpringBootLongPolling((ConfigurableEnvironment) environment, beanFactory);}
}

把该bean注入到springboot中,即在spring.factories中加入自动注入

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.config.center.autoconfigure.ConfigAutoConfiguration

这是一个ImportSelector会自动注入返回的类

@Import(ConfigAutoConfiguration.class)
public class ConfigAutoConfiguration implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {return new String[]{ConfigRefreshAnnotationBeanPostProcessor.class.getName()};}
}

启动长轮询

springboot启动完成后会发一个ApplicationRunner事件,我们只要在实现这个接口的bean中启动即可

@Overridepublic void run(ApplicationArguments args) {ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);configCenterClient.startSpringBootLongPolling((ConfigurableEnvironment) environment, beanFactory);}
    public void startSpringBootLongPolling(ConfigurableEnvironment environment, ConfigurableBeanFactory beanFactory) {if (configMap.isEmpty() || refreshFieldValueList.isEmpty()) {log.info("configMap.size:{} refreshFieldValueList.size:{}", configMap.size(), refreshFieldValueList.size());return;}MutablePropertySources propertySources = environment.getPropertySources();MapPropertySource configCenter = (MapPropertySource) propertySources.get(PROPERTY_SOURCE_NAME);if (configCenter == null) {log.warn("configCenter is null");return;}Map<String, Object> source = configCenter.getSource();Thread thread = new Thread(() -> {while (!Thread.interrupted()) {try {Map<String, Integer> configIdMap = configMap.values().stream().collect(Collectors.toMap(c -> c.getId() + "", ConfigBO::getVersion));HttpRespBO httpRespBO = HttpUtil.httpPostJson(url + "/config/change/get/long", JSON.toJSONString(configIdMap), 30000);List<ConfigVO> configList = httpResp2ConfigVOList(httpRespBO);if (configList.isEmpty()) {continue;}configList.forEach(configVO -> {Map<String, Object> result = new HashMap<>();DataTransUtil.buildFlattenedMap(result, configVO.getConfigData(), "");ConfigBO configBO = this.configMap.get(configVO.getId());configBO.setVersion(configVO.getVersion());List<ConfigDataBO> configDataList = configBO.getConfigDataList();Map<String, ConfigDataBO> configDataMap = configDataList.stream().collect(Collectors.toMap(ConfigDataBO::getKey, Function.identity()));result.forEach((key, value) -> {ConfigDataBO configDataBO = configDataMap.get(key);if (configDataBO == null) {configDataList.add(new ConfigDataBO(key, value.toString()));} else {configDataBO.setValue(value.toString());source.put(key, value);}});});refreshFieldValueList.forEach(refreshFieldBO -> {try {Field field = refreshFieldBO.getField();Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);if (valueAnnotation == null) {return;}String value = valueAnnotation.value();String relValue = beanFactory.resolveEmbeddedValue(value);if(relValue.equals(refreshFieldBO.getValue())){return;}field.setAccessible(true);field.set(refreshFieldBO.getBean(), relValue);} catch (Exception e) {log.error("startSpringBootLongPolling set Field error", e);}});} catch (Exception e) {log.error("startSpringBootLongPolling error", e);}}});thread.setName("startSpringBootLongPolling");thread.setDaemon(true);thread.start();}

效果

@Value

@Data
@Component
public class ConfigTest {@ConfigRefresh@Value("${user.name}")private String name;}
    @Autowiredprivate ConfigTest configTest;@Testpublic void configTest() throws InterruptedException {while (true) {System.out.println(configTest.getName());Thread.sleep(1000);}}

@ConfigurationProperties

增加同时有@ConfigurationProperties和@ConfigRefresh的收集

ConfigRefresh configRefresh = AnnotationUtils.findAnnotation(bean.getClass(), ConfigRefresh.class);if (configRefresh != null) {ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);if (configurationProperties != null) {ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);configCenterClient.addRefreshBeanList(bean);}}

在长轮询的返回中对@ConfigurationProperties重新绑定

refreshBeanList.forEach(refreshBean -> {ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(refreshBean.getClass(), ConfigurationProperties.class);if (configurationProperties == null) {log.warn("refreshBeanList refreshBean configurationProperties is null, class:{}", refreshBean.getClass());return;}Binder binder = Binder.get(environment);binder.bind(configurationProperties.prefix(), Bindable.ofInstance(refreshBean));});

完整代码

@Slf4j
public class ConfigRefreshAnnotationBeanPostProcessor implements ApplicationRunner, BeanPostProcessor, BeanFactoryAware, EnvironmentAware {private Environment environment;private ConfigurableBeanFactory beanFactory;@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {if (!(beanFactory instanceof ConfigurableBeanFactory)) {log.warn("ConfigurableBeanFactory requires a ConfigurableListableBeanFactory");return;}this.beanFactory = (ConfigurableBeanFactory) beanFactory;}@Overridepublic Object postProcessBeforeInitialization(Object bean, final String beanName) throws BeansException {if (beanFactory != null) {ReflectionUtils.doWithFields(bean.getClass(), field -> {try {ConfigRefresh configRefresh = AnnotationUtils.getAnnotation(field, ConfigRefresh.class);if (configRefresh == null) {return;}Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);if (valueAnnotation == null) {return;}String value = valueAnnotation.value();String relValue = beanFactory.resolveEmbeddedValue(value);ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);configCenterClient.addRefreshFieldValue(bean, field, relValue);} catch (Exception e) {log.error("set bean field fail,beanName:{},fieldName:{}", bean.getClass().getName(), field.getName(), e);}});ConfigRefresh configRefresh = AnnotationUtils.findAnnotation(bean.getClass(), ConfigRefresh.class);if (configRefresh != null) {ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);if (configurationProperties != null) {ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);configCenterClient.addRefreshBeanList(bean);}}}return bean;}@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}@Overridepublic void run(ApplicationArguments args) {ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);configCenterClient.startSpringBootLongPolling((ConfigurableEnvironment) environment, beanFactory);}
}
 public void startSpringBootLongPolling(ConfigurableEnvironment environment, ConfigurableBeanFactory beanFactory) {if (configMap.isEmpty() || refreshFieldValueList.isEmpty()) {log.info("configMap.size:{} refreshFieldValueList.size:{}", configMap.size(), refreshFieldValueList.size());return;}MutablePropertySources propertySources = environment.getPropertySources();MapPropertySource configCenter = (MapPropertySource) propertySources.get(PROPERTY_SOURCE_NAME);if (configCenter == null) {log.warn("configCenter is null");return;}Map<String, Object> source = configCenter.getSource();Thread thread = new Thread(() -> {while (!Thread.interrupted()) {try {Map<String, Integer> configIdMap = configMap.values().stream().collect(Collectors.toMap(c -> c.getId() + "", ConfigBO::getVersion));HttpRespBO httpRespBO = HttpUtil.httpPostJson(url + "/config/change/get/long", JSON.toJSONString(configIdMap), 30000);List<ConfigVO> configList = httpResp2ConfigVOList(httpRespBO);if (configList.isEmpty()) {continue;}configList.forEach(configVO -> {Map<String, Object> result = new HashMap<>();DataTransUtil.buildFlattenedMap(result, configVO.getConfigData(), "");ConfigBO configBO = this.configMap.get(configVO.getId());configBO.setVersion(configVO.getVersion());List<ConfigDataBO> configDataList = configBO.getConfigDataList();Map<String, ConfigDataBO> configDataMap = configDataList.stream().collect(Collectors.toMap(ConfigDataBO::getKey, Function.identity()));result.forEach((key, value) -> {ConfigDataBO configDataBO = configDataMap.get(key);if (configDataBO == null) {configDataList.add(new ConfigDataBO(key, value.toString()));} else {configDataBO.setValue(value.toString());source.put(key, value);}});});refreshFieldValueList.forEach(refreshFieldBO -> {try {Field field = refreshFieldBO.getField();Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);if (valueAnnotation == null) {return;}String value = valueAnnotation.value();String relValue = beanFactory.resolveEmbeddedValue(value);if (relValue.equals(refreshFieldBO.getValue())) {return;}field.setAccessible(true);field.set(refreshFieldBO.getBean(), relValue);} catch (Exception e) {log.error("startSpringBootLongPolling set Field error", e);}});refreshBeanList.forEach(refreshBean -> {ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(refreshBean.getClass(), ConfigurationProperties.class);if (configurationProperties == null) {log.warn("refreshBeanList refreshBean configurationProperties is null, class:{}", refreshBean.getClass());return;}Binder binder = Binder.get(environment);binder.bind(configurationProperties.prefix(), Bindable.ofInstance(refreshBean));});} catch (Exception e) {log.error("startSpringBootLongPolling error", e);}}});thread.setName("startSpringBootLongPolling");thread.setDaemon(true);thread.start();}

效果

@Component
@ConfigRefresh
@ConfigurationProperties(prefix = "user")
public class ConfigTest2 {private String name;private int age;private List<String> education;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public List<String> getEducation() {return education;}public void setEducation(List<String> education) {this.education = education;}
}
   @Autowiredprivate ConfigTest2 configTest2;@Testpublic void configTest() throws InterruptedException {while (true) {System.out.println(configTest2.getName() + "-" + configTest2.getAge() + "-" + configTest2.getEducation());Thread.sleep(1000);}}

相关文章:

手写分布式配置中心(六)整合springboot(自动刷新)

对于springboot配置自动刷新&#xff0c;原理也很简单&#xff0c;就是在启动过程中用一个BeanPostProcessor去收集需要自动刷新的字段&#xff0c;然后在springboot启动后开启轮询任务即可。 不过需要对之前的代码再次做修改&#xff0c;因为springboot的配置注入value("…...

记录一次排查负载均衡不能创建的排查过程

故障现象&#xff0c;某云上&#xff0c;运维同事在创建负载均衡的时候&#xff0c;发现可以创建资源&#xff0c;但是创建完之后&#xff0c;不显示对应的负载均衡。 创建负载均衡时候&#xff0c;按f12发现console有如下报错 后来请后端网络同事排查日志发现&#xff0c;是后…...

数据推送解决方案调研

需求 文档编辑类型的需求&#xff0c;左侧是菜单栏&#xff0c;右侧是内容块&#xff0c;现在的需求时&#xff0c;如果多人同时编辑这个方案&#xff0c;当添加章节/调整章节顺序/删除章节时&#xff0c;其他用户能够及时感知到。 解决方案调研 前端轮询 最简单的方案&…...

二、NLP中的序列标注(分词、主体识别)

一般来说&#xff0c;一个序列指的是一个句子&#xff0c;而一个元素指的是句子中的一个词。在序列标注中&#xff0c;我们想对一个序列的每一个元素标注一个分类标签。比如信息提取问题可以认为是一个序列标注问题&#xff0c;如提取出会议时间、地点等。 常见的应用场景&…...

seq2seq翻译实战-Pytorch复现

&#x1f368; 本文为[&#x1f517;365天深度学习训练营学习记录博客 &#x1f366; 参考文章&#xff1a;365天深度学习训练营 &#x1f356; 原作者&#xff1a;[K同学啊 | 接辅导、项目定制]\n&#x1f680; 文章来源&#xff1a;[K同学的学习圈子](https://www.yuque.com/…...

软考69-上午题-【面向对象技术2-UML】-关系

一、关系 UML中有4种关系&#xff1a; 依赖&#xff1b;关联&#xff1b;泛化&#xff1b;实现。 1-1、依赖 行为&#xff08;参数&#xff09;&#xff0c;参数就是被依赖的事物&#xff0c;即&#xff1a;独立事物。 当独立事物发生变化时&#xff0c;依赖事务行为的语义也…...

智慧文旅|AI数字人导览:让旅游体验不再局限于传统

AI数字人导览作为一种创新的展示方式&#xff0c;已经逐渐成为了VR全景领域的一大亮点&#xff0c;不仅可以很好的嵌入在VR全景中&#xff0c;更是能够随时随地为观众提供一种声情并茂的讲解介绍&#xff0c;结合VR场景的沉浸式体验&#xff0c;让观众仿佛置身于真实场景之中&a…...

spring boot 集成 mysql ,mybatisplus多数据源

1、需要的依赖&#xff0c;版本自行控制 <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId> </dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java<…...

CLion中常用快捷键(仍适用其他编译软件)

基本编辑操作&#xff1a; 复制&#xff1a;Ctrl C粘贴&#xff1a;Ctrl V剪切&#xff1a;Ctrl X撤销&#xff1a;Ctrl Z重做&#xff1a;Ctrl Shift Z &#xff08;不小心撤销了 需要返回之前的操作 相当于下一步&#xff09;全选&#xff1a;Ctrl A 导航&#xff1…...

考研复习c语言初阶(1)

本人准备考研&#xff0c;现在开始每天更新408的内容&#xff0c;目标这个月结束C语言和数据结构&#xff0c;每天更新~ 一.再次认识c语言 C语言是一门通用计算机编程语言&#xff0c;广泛应用于底层开发。C语言的设计目标是提供一种能以简易 的方式编译、处理低级存储器、产生…...

HTML—常用标签

常用标签&#xff1a; 标题标签&#xff1a;<h1></h1>......<h6></h6>段落标签&#xff1a;<p></p>换行标签&#xff1a;<br/>列表&#xff1a;无序列表<ul><li></li></ul> 有序列表<ol>&…...

Midjourney绘图欣赏系列(七)

Midjourney介绍 Midjourney 是生成式人工智能的一个很好的例子&#xff0c;它根据文本提示创建图像。它与 Dall-E 和 Stable Diffusion 一起成为最流行的 AI 艺术创作工具之一。与竞争对手不同&#xff0c;Midjourney 是自筹资金且闭源的&#xff0c;因此确切了解其幕后内容尚不…...

深度学习应该如何入门?

深度学习是一门令人着迷的领域&#xff0c;但初学者可能会感到有些困惑。让我们从头开始&#xff0c;用通俗易懂的语言来探讨深度学习的基础知识。 1. 基础知识 深度学习需要一些数学和编程基础。首先&#xff0c;我们要掌握一些数学知识&#xff0c;如线性代数、微积分和概率…...

FreeRtos Queue(五)

本篇主要分析在中断中向队列里发消息xQueueGenericSendFromISR和在中断里从队列中读取消息xQueueReceiveFromISR。 前言: xQueueGenericSendFromISR 和 xQueueReceiveFromISR都是在中断里调用的而不是任务里调用的&#xff0c;所以队列满了或者是队列为空的时候自然就没有把当…...

解决虚拟机静态网址设置后还是变动的的问题

源头就是我的虚拟机静态网址设置好了以后但是网址还是会变动 这是我虚拟机的配置 vi /etc/sysconfig/network-scripts/ifcfg-ens33 这是出现的问题 进入这里 cd /etc/sysconfig/network-scripts/ 然后我去把多余的ens33的文件都删了 然后还不行 后来按照这个图片进行了下 然后…...

【教程】Github环境配置新手指南(超详细)

写在前面&#xff1a; 如果文章对你有帮助&#xff0c;记得点赞关注加收藏一波&#xff0c;利于以后需要的时候复习&#xff0c;多谢支持&#xff01; 文章目录 一、Github初始设置&#xff08;一&#xff09;登入Github&#xff08;二&#xff09;新建仓库 二、本地Git配置&am…...

突然发现一个很炸裂的平台!

平时小孟会开发很多的项目&#xff0c;很多项目不仅开发的功能比较齐全&#xff0c;而且效果比较炸裂。 今天给大家介绍一个我常用的平台&#xff0c;因含低代码平台&#xff0c;开发相当的快。 1&#xff0c;什么是低代码 低代码包括两种&#xff0c;一种低代码&#xff0c;…...

安卓开发面试题

安卓开发面试题 解释一下 Android 中的四大组件。 答&#xff1a;Android 中的四大组件是 Activity、Service、BroadcastReceiver 和 ContentProvider。其中&#xff0c;Activity 负责界面展示和与用户交互&#xff1b;Service 负责后台服务处理&#xff1b;BroadcastReceiver …...

es6面试题

ES6面试题 var、let、const区别 共同点&#xff1a;都是可以声明变量 区别&#xff1a; 1、var具有变量提升机制&#xff0c;let和const没有 2、var 声明的变量是函数作用域或全局作用域&#xff0c;而 const 和 let 声明的变量是块级作用域。 3、var可以多次声明同一个变量&a…...

Kafka MQ 生产者和消费者

Kafka MQ 生产者和消费者 Kafka 的客户端就是 Kafka 系统的用户&#xff0c;它们被分为两种基本类型:生产者和消费者。除 此之外&#xff0c;还有其他高级客户端 API——用于数据集成的 Kafka Connect API 和用于流式处理 的 Kafka Streams。这些高级客户端 API 使用生产者和消…...

网络六边形受到攻击

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 抽象 现代智能交通系统 &#xff08;ITS&#xff09; 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 &#xff08;…...

超短脉冲激光自聚焦效应

前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应&#xff0c;这是一种非线性光学现象&#xff0c;主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场&#xff0c;对材料产生非线性响应&#xff0c;可能…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法

树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作&#xff0c;无需更改相机配置。但是&#xff0c;一…...

黑马Mybatis

Mybatis 表现层&#xff1a;页面展示 业务层&#xff1a;逻辑处理 持久层&#xff1a;持久数据化保存 在这里插入图片描述 Mybatis快速入门 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6501c2109c4442118ceb6014725e48e4.png //logback.xml <?xml ver…...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

生成 Git SSH 证书

&#x1f511; 1. ​​生成 SSH 密钥对​​ 在终端&#xff08;Windows 使用 Git Bash&#xff0c;Mac/Linux 使用 Terminal&#xff09;执行命令&#xff1a; ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" ​​参数说明​​&#xff1a; -t rsa&#x…...

高危文件识别的常用算法:原理、应用与企业场景

高危文件识别的常用算法&#xff1a;原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件&#xff0c;如包含恶意代码、敏感数据或欺诈内容的文档&#xff0c;在企业协同办公环境中&#xff08;如Teams、Google Workspace&#xff09;尤为重要。结合大模型技术&…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)

船舶制造装配管理现状&#xff1a;装配工作依赖人工经验&#xff0c;装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书&#xff0c;但在实际执行中&#xff0c;工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

JVM虚拟机:内存结构、垃圾回收、性能优化

1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...