不想引入MQ?不妨试试 Debezium

Debezium是一个捕获数据更改(CDC)平台,并且利用Kafka和Kafka Connect实现了自己的持久性、可靠性和容错性。常见的数据更改捕获都是通过数据库比如mysql的binlog来达到目的。
「这样的好处是,只需导入依赖,不额外引入组件,同时无需改动之前的代码。两边完全解耦,互不干扰。」

为什么是debezium

这么多技术框架,为什么选debezium?

看起来很多。但一一排除下来就debezium和canal。

sqoop,kettle,datax之类的工具,属于前大数据时代的产物,地位类似于web领域的structs2。而且,它们基于查询而非binlog日志,其实不属于CDC。首先排除。

flink cdc是大数据领域的框架,一般web项目的数据量属于大材小用了。

同时databus,maxwell相对比较冷门,用得比较少。

「最后不用canal的原因有以下几点:」

  • canal需要安装,这违背了“如非必要,勿增实体”的原则。
  • canal只能对MYSQL进行CDC监控。有很大的局限性。
  • 大数据领域非常流行的flink cdc(阿里团队主导)底层使用的也是debezium,而非同是阿里出品的canal。
  • debezium可借助kafka组件,将变动的数据发到kafka topic,后续的读取操作只需读取kafka,可有效减少数据库的读取压力。可保证一次语义,至少一次语义。
  • 同时,也可基于内嵌部署模式,无需我们手动部署kafka集群,可满足”如非必要,勿增实体“的原则。

springboot 整合 Debezium之Mysql

依赖
<debezium.version>1.7.0.Final</debezium.version><mysql.connector.version>8.0.26</mysql.connector.version><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.connector.version}</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.debezium</groupId><artifactId>debezium-api</artifactId><version>${debezium.version}</version>
</dependency>
<dependency><groupId>io.debezium</groupId><artifactId>debezium-embedded</artifactId><version>${debezium.version}</version>
</dependency>
<dependency><groupId>io.debezium</groupId><artifactId>debezium-connector-mysql</artifactId><version>${debezium.version}</version><exclusions><exclusion><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></exclusion></exclusions>
</dependency>

注意debezium版本为1.7.0.Final,对应mysql驱动为8.0.26,低于这个版本会报兼容错误。

配置

相应的配置

debezium.datasource.hostname = localhost
debezium.datasource.port = 3306
debezium.datasource.user = root
debezium.datasource.password = 123456
debezium.datasource.tableWhitelist = test.test
debezium.datasource.storageFile = E:/debezium/test/offsets/offset.dat
debezium.datasource.historyFile = E:/debezium/test/history/custom-file-db-history.dat
debezium.datasource.flushInterval = 10000
debezium.datasource.serverId = 1
debezium.datasource.serverName = name-1

然后进行配置初始化。

主要的配置项:

  • connector.class 监控的数据库类型,这里选mysql。
  • offset.storage 选择FileOffsetBackingStore时,意思把读取进度存到本地文件,因为我们不用kafka,当使用kafka时,选KafkaOffsetBackingStore
  • offset.storage.file.filename 存放读取进度的本地文件地址。
  • offset.flush.interval.ms 读取进度刷新保存频率,默认1分钟。如果不依赖kafka的话,应该就没有exactly once只读取一次语义,应该是至少读取一次。意味着可能重复读取。如果web容器挂了,最新的读取进度没有刷新到文件里,下次重启时,就会重复读取binlog。
  • table.whitelist 监控的表名白名单,建议设置此值,只监控这些表的binlog。
  • database.whitelist 监控的数据库白名单,如果选此值,会忽略table.whitelist,然后监控此db下所有表的binlog。
import io.debezium.connector.mysql.MySqlConnector;
import io.debezium.relational.history.FileDatabaseHistory;
import lombok.Data;
import org.apache.kafka.connect.storage.FileOffsetBackingStore;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.File;
import java.io.IOException;/*** @className: MysqlConfig* @author: nyp* @description: TODO* @date: 2023/8/7 13:53* @version: 1.0*/
@Configuration
@ConfigurationProperties(prefix ="debezium.datasource")
@Data
public class MysqlBinlogConfig {private String hostname;private String port;private String user;private String password;private String tableWhitelist;private String storageFile;private String historyFile;private Long flushInterval;private String serverId;private String serverName;@Beanpublic io.debezium.config.Configuration MysqlBinlogConfig () throws Exception {checkFile();io.debezium.config.Configuration configuration = io.debezium.config.Configuration.create().with("name", "mysql_connector").with("connector.class", MySqlConnector.class)// .with("offset.storage", KafkaOffsetBackingStore.class).with("offset.storage", FileOffsetBackingStore.class).with("offset.storage.file.filename", storageFile).with("offset.flush.interval.ms", flushInterval).with("database.history", FileDatabaseHistory.class.getName()).with("database.history.file.filename", historyFile).with("snapshot.mode", "Schema_only").with("database.server.id", serverId).with("database.server.name", serverName).with("database.hostname", hostname)
//                .with("database.dbname", dbname).with("database.port", port).with("database.user", user).with("database.password", password)
//                .with("database.whitelist", "test").with("table.whitelist", tableWhitelist).build();return configuration;}private void checkFile() throws IOException {String dir = storageFile.substring(0, storageFile.lastIndexOf("/"));File dirFile = new File(dir);if(!dirFile.exists()){dirFile.mkdirs();}File file = new File(storageFile);if(!file.exists()){file.createNewFile();}}
}

snapshot.mode 快照模式,指定连接器启动时运行快照的条件。可能的设置有:

  • initial只有在没有为逻辑服务器名记录偏移量时,连接器才运行快照。
  • When_needed当连接器认为有必要时,它会在启动时运行快照。也就是说,当没有可用的偏移量时,或者当先前记录的偏移量指定了服务器中不可用的binlog位置或GTID时。
  • Never连接器从不使用快照。在第一次使用逻辑服务器名启动时,连接器从binlog的开头读取。谨慎配置此行为。只有当binlog保证包含数据库的整个历史记录时,它才有效。
  • Schema_only连接器运行模式而不是数据的快照。当您不需要主题包含数据的一致快照,而只需要主题包含自连接器启动以来的更改时,此设置非常有用。
  • Schema_only_recovery这是已经捕获更改的连接器的恢复设置。当您重新启动连接器时,此设置允许恢复损坏或丢失的数据库历史主题。您可以定期将其设置为“清理”意外增长的数据库历史主题。数据库历史主题需要无限保留。
  • database.server.id

伪装成slave的Debezium服务的id,自定义,有多个Debezium服务不能重复,如果重复的话会报以下异常。

io.debezium.DebeziumException: A slave with the same server_uuid/server_id as this slave has connected to the master; the first event 'binlog.000013' at 46647257, the last event read from './binlog.000013' at 125, the last byte read from './binlog.000013' at 46647257. Error code: 1236; SQLSTATE: HY000.at io.debezium.connector.mysql.MySqlStreamingChangeEventSource.wrap(MySqlStreamingChangeEventSource.java:1167)at io.debezium.connector.mysql.MySqlStreamingChangeEventSource$ReaderThreadLifecycleListener.onCommunicationFailure(MySqlStreamingChangeEventSource.java:1212)at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:980)at com.github.shyiko.mysql.binlog.BinaryLogClient.connect(BinaryLogClient.java:599)at com.github.shyiko.mysql.binlog.BinaryLogClient$7.run(BinaryLogClient.java:857)at java.lang.Thread.run(Thread.java:750)
Caused by: com.github.shyiko.mysql.binlog.network.ServerException: A slave with the same server_uuid/server_id as this slave has connected to the master; the first event 'binlog.000013' at 46647257, the last event read from './binlog.000013' at 125, the last byte read from './binlog.000013' at 46647257.at com.github.shyiko.mysql.binlog.BinaryLogClient.listenForEventPackets(BinaryLogClient.java:944)... 3 common frames omitted
监听

配置监听服务

import com.alibaba.fastjson.JSON;
import io.debezium.config.Configuration;
import io.debezium.data.Envelope;
import io.debezium.engine.ChangeEvent;
import io.debezium.engine.DebeziumEngine;
import io.debezium.engine.format.Json;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;/*** @projectName: test* @package: com.test.config* @className: MysqlBinlogListener* @author: nyp* @description: TODO* @date: 2023/8/7 13:56* @version: 1.0*/
@Component
@Slf4j
public class MysqlBinlogListener {@Resourceprivate Executor taskExecutor;private final List<DebeziumEngine<ChangeEvent<String, String>>> engineList = new ArrayList<>();private MysqlBinlogListener (@Qualifier("mysqlConnector") Configuration configuration) {this.engineList.add(DebeziumEngine.create(Json.class).using(configuration.asProperties()).notifying(record -> receiveChangeEvent(record.value())).build());}private void receiveChangeEvent(String value) {if (Objects.nonNull(value)) {Map<String, Object> payload = getPayload(value);String op = JSON.parseObject(JSON.toJSONString(payload.get("op")), String.class);if (!(StringUtils.isBlank(op) || Envelope.Operation.READ.equals(op))) {ChangeData changeData = getChangeData(payload);// 这里抛出异常会导致后面的日志监听失败try {mysqlBinlogService.service(changeData);}catch (Exception e){log.error("binlog处理异常,原数据: " + changeData, e);}}}}@PostConstructprivate void start() {for (DebeziumEngine<ChangeEvent<String, String>> engine : engineList) {taskExecutor.execute(engine);}}@PreDestroyprivate void stop() {for (DebeziumEngine<ChangeEvent<String, String>> engine : engineList) {if (engine != null) {try {engine.close();} catch (IOException e) {log.error("", e);}}}}public static Map<String, Object> getPayload(String value) {Map<String, Object> map = JSON.parseObject(value, Map.class);Map<String, Object> payload = JSON.parseObject(JSON.toJSONString(map.get("payload")), Map.class);return payload;}public static ChangeData getChangeData(Map<String, Object> payload) {Map<String, Object> source = JSON.parseObject(JSON.toJSONString(payload.get("source")), Map.class);return ChangeData.builder().op(payload.get("op").toString()).table(source.get("table").toString()).after(JSON.parseObject(JSON.toJSONString(payload.get("after")), Map.class)).source(JSON.parseObject(JSON.toJSONString(payload.get("source")), Map.class)).before(JSON.parseObject(JSON.toJSONString(payload.get("before")), Map.class)).build();}@Data@Builderpublic static class ChangeData {/*** 更改前数据*/private Map<String, Object> after;private Map<String, Object> source;/*** 更改后数据*/private Map<String, Object> before;/*** 更改的表名*/private String table;/*** 操作类型, 枚举 Envelope.Operation*/private String op;}}

将监听到的binlog日志封装为ChangeData对象,包括表名,更改前后的数据,

以及操作类型

READ("r"),
CREATE("c"),
UPDATE("u"),
DELETE("d"),
TRUNCATE("t");

springboot 整合 Debezium之MongoDB

添加依赖
<debezium.version>1.6.1.Final</debezium.version>
<dependency><groupId>io.debezium</groupId><artifactId>debezium-api</artifactId><version>${debezium.version}</version></dependency><dependency><groupId>io.debezium</groupId><artifactId>debezium-embedded</artifactId><version>${debezium.version}</version></dependency><dependency><groupId>io.debezium</groupId><artifactId>debezium-connector-mongodb</artifactId><version>${debezium.version}</version></dependency><dependency><groupId>io.debezium</groupId><artifactId>debezium-scripting</artifactId><version>${debezium.version}</version></dependency>
配置项目启动加载
package net.commchina.task.config;import io.debezium.engine.DebeziumEngine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;@Slf4j
public class DebeziumServerBootstrap  implements ApplicationRunner {private final Executor executor = Executors.newSingleThreadExecutor();private DebeziumEngine<?> debeziumEngine;public Executor getExecutor(){return executor;}public DebeziumEngine<?> getDebeziumEngine(){return debeziumEngine;}public void setDebeziumEngine(DebeziumEngine<?> debeziumEngine){this.debeziumEngine = debeziumEngine;}@Overridepublic void run(ApplicationArguments args) throws Exception {executor.execute(debeziumEngine);Runtime.getRuntime().addShutdownHook(new Thread(()->{try {debeziumEngine.close();} catch (IOException e) {log.error("addShutdownHook error:{}",e);}}));}
}
配置类Debezium
package net.commchina.task.config;import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.debezium.engine.ChangeEvent;
import io.debezium.engine.DebeziumEngine;
import io.debezium.engine.format.Json;
import lombok.extern.slf4j.Slf4j;
import net.commchina.task.listener.TaskEventListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Map;@Slf4j
@Configuration
@ConditionalOnProperty(name = "mongodb.sync.data", havingValue = "true")
public class DebeziumConfiguration {@Value("${mongodb.hosts}")private String mongodbHosts;@Value("${mongodb.name}")private String mongodbName;@Value("${mongodb.user}")private String mongodbUser;@Value("${mongodb.password}")private String mongodbPassword;@Value("${mongodb.authsource}")private String mongodbAuthsource;@Value("${database.include.list}")private String databaseIncludeList;@Value("${collection.include.list}")private String collectionIncludeList;private static final ObjectMapper objectMapper = new ObjectMapper();@Autowiredprivate TaskEventListener taskEventListener;/*** Debezium 配置.** @return configuration*/@Beanio.debezium.config.Configuration debeziumConfig(){return io.debezium.config.Configuration.create()//连接器的唯一名称.with("name", "inventory-connector")//连接器的Java类名称.with("connector.class", "io.debezium.connector.mongodb.MongoDbConnector")//副本集中 MongoDB 服务器的主机名和端口对(以“主机”或“主机:端口”的形式)的逗号分隔列表//.with("mongodb.hosts", "Replica-Set/192.168.188.152:27017,Replica-Set/192.168.188.152:27018,Replica-Set/192.168.188.152:27019").with("mongodb.hosts", mongodbHosts).with("mongodb.name",mongodbName).with("mongodb.user",mongodbUser).with("mongodb.password",mongodbPassword).with("mongodb.authsource",mongodbAuthsource)//包含的数据库列表.with("database.include.list", databaseIncludeList).with("collection.include.list",collectionIncludeList)//偏移量持久化,用来容错 默认值.with("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore")//偏移量持久化文件路径 默认/tmp/offsets.dat  如果路径配置不正确可能导致无法存储偏移量 可能会导致重复消费变更//如果连接器重新启动,它将使用最后记录的偏移量来知道它应该恢复读取源信息中的哪个位置。.with("offset.storage.file.filename", "/logs/offsets.dat")//捕获偏移量的周期.with("offset.flush.interval.ms", "6000")//是否包含数据库表结构层面的变更,建议使用默认值true.with("include.schema.changes", "false")//历史变更记录.with("database.history", "io.debezium.relational.history.FileDatabaseHistory")//历史变更记录存储位置,存储DDL.with("database.history.file.filename", "/logs/dbhistory.dat").with("transforms","filter").with("transforms.filter.type","io.debezium.transforms.Filter").with("transforms.filter.language","jsr223.groovy").with("transforms.filter.condition","value.op == 'u' || value.op == 'c' || value.op == 'd'").build();}/*** Debezium server bootstrap debezium server bootstrap.** @param configuration the configuration* @return the debezium server bootstrap*/@BeanDebeziumServerBootstrap debeziumServerBootstrap(io.debezium.config.Configuration configuration){DebeziumServerBootstrap debeziumServerBootstrap = new DebeziumServerBootstrap();DebeziumEngine<ChangeEvent<String,String>> debeziumEngine = DebeziumEngine.create(Json.class).using(configuration.asProperties()).notifying((records, committer)->{records.forEach(r->{try {log.debug("key:{}---value:{}",r.key(),r.value());if(null!=r.value()){Map<String, Object> map = objectMapper.readValue(r.value(), new TypeReference<Map<String, Object>>() {});Object payload = map.get("payload");taskEventListener.syncData(JSON.toJSONString(payload));}committer.markProcessed(r);} catch (Exception e) {log.error("e:{}",e);}});}).build();debeziumServerBootstrap.setDebeziumEngine(debeziumEngine);return debeziumServerBootstrap;}}
测试

更改数据库数据进行测试监听

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/88010.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

seata的启动与使用

1 下载seata 下载地址&#xff1a;https://github.com/seata/seata/releases/v0.9.0/ 1.1 修改配置文件 将下载得到的压缩包进行解压&#xff0c;进入conf目录&#xff0c;调整下面的配置文件&#xff1a; registry.conf registry {type "nacos"nacos {serverA…

Spring 学习(八)事务管理

1. 事务 1.1 事务的 ACID 原则 数据库事务&#xff08;transaction&#xff09;是访问并可能操作各种数据项的一个数据库操作序列。事务必须满足 ACID 原则——即原子性&#xff08;Atomicity&#xff09;、一致性&#xff08;Consistency&#xff09;、隔离性&#xff08;Iso…

uniapp:tabBar点击后设置动画效果

APP端不支持dom操作&#xff0c;也不支持active伪类&#xff0c;绞尽脑汁也没办法给uniapp原生的tabBar点击加动画效果&#xff0c;所以最终只能舍弃原生tabBar&#xff0c;改用自定义tabBar。 自定义tabBar的原理是&#xff0c;页面的上部分分别是tabBar对应的页面组件&#…

Matlab绘图函数subplot、tiledlayout、plot和scatter

一、绘图函数subplot subplot(m,n,p)将当前图窗划分为 mn 网格&#xff0c;并在 p 指定的位置创建坐标区。MATLAB按行号对子图位置进行编号。第一个子图是第一行的第一列&#xff0c;第二个子图是第一行的第二列&#xff0c;依此类推。如果指定的位置已存在坐标区&#xff0c;…

【.net core】yisha框架使用nginx代理swagger接口无法访问问题

后端代码配置 #在StartUp.cs文件中Configure方法中增加以下代码 app.UseSwagger(c >{//代理路径访问c.PreSerializeFilters.Add((doc, item) >{//根据代理服务器提供的协议、地址和路由&#xff0c;生成api文档服务地址doc.Servers new List<OpenApiServer>{ new…

计算物理专题----随机游走实战

计算物理专题----随机游走实战 Problem 1 Implement the 3D random walk 拟合线 自旋的 拟合函数&#xff08;没有数学意义&#xff09; 参数&#xff1a;0.627,3.336,0.603&#xff0c;-3.234 自由程满足在一定范围内的均匀分布以标准自由程为单位长度&#xff0c;…

node的服务端对接科大讯飞-火星ai解决方案

序&#xff1a; 官方给的node对接火星的demo其实只适用于node开发的web应用&#xff0c;但是对于纯node 作为服务端&#xff0c;也就是作为webapi来调用&#xff0c;你会发现&#xff0c;location.host直接是获取不到location的。这个时候&#xff0c;其实要单独起个wss的服务的…

C++: stack 与 queue

目录 1.stack与queue stack queue 2.priority_queue 2.1相关介绍 2.2模拟实现priority_queue --仿函数: --push --pop --top --size --empty --迭代器区间构造 2.3仿函数 3.容器适配器 stack模拟实现 queue模拟实现 学习目标: 1.stack和queue介绍与使用 2.pri…

PHP8中伪变量“$this->”和操作符“::”的使用-PHP8知识详解

对象不仅可以调用自己的变量和方法&#xff0c;也可以调用类中的变量和方法。PHP8通过伪变量“$this->”和操作符“::”来实现这些功能。 1.伪变量“$this->” 在通过对象名->方法调用对象的方法时&#xff0c;如果不知道对象的名称&#xff0c;而又想调用类中的方法…

基于微信小程序的校园代送跑腿系统(源码+lw+部署文档+讲解等)

文章目录 前言系统主要功能&#xff1a;具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计…

Mooctest

开发者 测试框架junit 1.字符串不能除 2.a给了c 3. 4. 5.输入是否>0 6.注释

Python中的用法与常见问题解析

装饰器是Python语言中一种强大且常用的概念。通过装饰器&#xff0c;我们可以在不修改原始函数代码的情况下&#xff0c;给函数添加额外的功能&#xff0c;比如日志记录、性能分析、输入验证等。在本文中&#xff0c;我们将深入探讨Python中装饰器的用法和常见问题&#xff0c;…

Leetcode刷题笔记--Hot51-60

1--环形链表II 主要思路&#xff1a; 快慢指针&#xff0c;快指针每次走两步&#xff0c;慢指针每次走一步&#xff1b; 第一次相遇时&#xff0c;假设慢指针共走了 f 步&#xff0c;则快指针走了 2f 步&#xff1b; 假设起点到环入口结点的长度为 a&#xff08;不包括入口结点…

【7.Vue 利用Heatmap.js 制作自定义热力图】

1.效果 2.背景 需要根据后端检测的设备的数值显示设备周围的清洁度,用户希望用热力图的方式来显示,于是在网上找了资料,发现可以用Heatmap.js来实现。 Heatmap.js 官网:https://www.patrick-wied.at/static/heatmapjs/ 3.引入组件 安装Heatmap.js npm install Heatmap.…

Nginx之带宽限制解读

目录 基本介绍 指令配置 limit_rate limit_rate_after 实战测试 原理&#xff1a; 令牌桶算法 基本介绍 在高负载的网络环境下&#xff0c;为了保持服务的稳定性&#xff0c;限速 (download rate) 是一种必要的操控拜访量的手法。Nginx 是一款高性能的 Web 服务器和反向代…

踩中AIGC 美图看清自己“工具”本职

日前&#xff0c;美图公司发布 2023 年中期业绩&#xff0c;实现总收入 12.61 亿元&#xff0c;同比增长 29.8%&#xff1b;实现经调整后归母净利润 1.51 亿元&#xff0c;同比增长 320.4%&#xff0c;利润增速是收入增速的十倍。同时&#xff0c;在 AIGC 的加持下&#xff0c;…

JDK21新特性Record Patterns记录模式详解

1 摘要 通过使用记录模式来增强Java编程语言&#xff0c;以解构记录值。记录模式和类型模式可嵌套使用&#xff0c;从而实现强大、声明式和可组合的数据导航和处理形式。 2 发展史 由 JEP 405 提出的预览功能&#xff0c;并在JDK 19发布&#xff0c;然后由 JEP 432 再次预览…

GitLab数据迁移后出现500错误

一、背景 去年做GitLab数据迁移时&#xff0c;写过一篇文章《GitLab的备份与还原》。后来发现新创建的项目没问题&#xff0c;但对于迁移过来的项目&#xff0c;修改名称等信息&#xff0c;或者删除该项目时&#xff0c;会出现500错误&#xff0c;以为是系统问题&#…

websocket请求通过IteratorAggregate实现流式输出

对接国内讯飞星火模型&#xff0c;官方文档接口采用的是websocket跟国外chatgpt有些差异。 虽然官网给出一个简单demo通过while(true)&#xff0c;websocket的receive()可以实现逐条接受并输出给前端&#xff0c;但是通用和灵活度不高。不能兼容现有项目框架的流式输出。故模仿…

安卓Compose(一)

为什么学习安卓Compose&#xff1f; 安卓Compose是一个相对新的UI工具包&#xff0c;它的出现为安卓应用程序开发带来了一系列的好处。下面是一些学习Compose的理由&#xff1a; 声明式UI 与传统的安卓XML布局相比&#xff0c;Compose使用了声明式的UI编程范例。这意味着你可以…