用这4招优雅的实现Spring Boot 异步线程间数据传递
Spring Boot 自定义线程池实现异步开发相信大家都了解,但是在实际开发中需要在父子线程之间传递一些数据,比如用户信息,链路信息等等
比如用户登录信息使用ThreadLocal存放保证线程隔离,代码如下:
/*** @author 公众号:码猿技术专栏* @description 用户上下文信息*/
public class OauthContext {private static final ThreadLocal<LoginVal> loginValThreadLocal=new ThreadLocal<>();public static LoginVal get(){return loginValThreadLocal.get();}public static void set(LoginVal loginVal){loginValThreadLocal.set(loginVal);}public static void clear(){loginValThreadLocal.remove();}
}
那么子线程想要获取这个LoginVal如何做呢?
今天就来介绍几种优雅的方式实现Spring Boot 内部的父子线程的数据传递。
1. 手动设置
每执行一次异步线程都要分为两步:
- 获取父线程的LoginVal
- 将LoginVal设置到子线程,达到复用
代码如下:
public void handlerAsync() {//1\. 获取父线程的loginValLoginVal loginVal = OauthContext.get();log.info("父线程的值:{}",OauthContext.get());CompletableFuture.runAsync(()->{//2\. 设置子线程的值,复用OauthContext.set(loginVal);log.info("子线程的值:{}",OauthContext.get());});}
虽然能够实现目的,但是每次开异步线程都需要手动设置,重复代码太多,看了头疼,你认为优雅吗?
2. 线程池设置TaskDecorator
TaskDecorator是什么?官方api的大致意思:这是一个执行回调方法的装饰器,主要应用于传递上下文,或者提供任务的监控/统计信息。
知道有这么一个东西,如何去使用?
TaskDecorator是一个接口,首先需要去实现它,代码如下:
/*** @author 公众号:码猿技术专栏* @description 上下文装饰器*/
public class ContextTaskDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {//获取父线程的loginValLoginVal loginVal = OauthContext.get();return () -> {try {// 将主线程的请求信息,设置到子线程中OauthContext.set(loginVal);// 执行子线程,这一步不要忘了runnable.run();} finally {// 线程结束,清空这些信息,否则可能造成内存泄漏OauthContext.clear();}};}
}
这里我只是设置了LoginVal,实际开发中其他的共享数据,比如SecurityContext,RequestAttributes....
TaskDecorator需要结合线程池使用,实际开发中异步线程建议使用线程池,只需要在对应的线程池配置一下,代码如下:
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();poolTaskExecutor.setCorePoolSize(xx);poolTaskExecutor.setMaxPoolSize(xx);// 设置线程活跃时间(秒)poolTaskExecutor.setKeepAliveSeconds(xx);// 设置队列容量poolTaskExecutor.setQueueCapacity(xx);//设置TaskDecorator,用于解决父子线程间的数据复用poolTaskExecutor.setTaskDecorator(new ContextTaskDecorator());poolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());// 等待所有任务结束后再关闭线程池poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);return poolTaskExecutor;}
此时业务代码就不需要去设置子线程的值,直接使用即可,代码如下:
public void handlerAsync() {log.info("父线程的用户信息:{}", OauthContext.get());//执行异步任务,需要指定的线程池CompletableFuture.runAsync(()-> log.info("子线程的用户信息:{}", OauthContext.get()),taskExecutor);}
来看一下结果,如下图:
这里使用的是CompletableFuture执行异步任务,使用@Async这个注解同样是可行的。
注意:无论使用何种方式,都需要指定线程池
3. InheritableThreadLocal
这种方案不建议使用,InheritableThreadLocal虽然能够实现父子线程间的复用,但是在线程池中使用会存在复用的问题,具体的可以看陈某之前的文章:微服务中使用阿里开源的TTL,优雅的实现身份信息的线程间复用
这种方案使用也是非常简单,直接用InheritableThreadLocal替换ThreadLocal即可,代码如下:
/*** @author 公众号:码猿技术专栏* @description 用户上下文信息*/
public class OauthContext {private static final InheritableThreadLocal<LoginVal> loginValThreadLocal=new InheritableThreadLocal<>();public static LoginVal get(){return loginValThreadLocal.get();}public static void set(LoginVal loginVal){loginValThreadLocal.set(loginVal);}public static void clear(){loginValThreadLocal.remove();}
}
4. TransmittableThreadLocal
TransmittableThreadLocal是阿里开源的工具,弥补了InheritableThreadLocal的缺陷,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
使用起来也是非常简单,添加依赖如下:
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.14.2</version>
</dependency>
OauthContext改造代码如下:
/*** @author 公众号:码猿技术专栏* @description 用户上下文信息*/
public class OauthContext {private static final TransmittableThreadLocal<LoginVal> loginValThreadLocal=new TransmittableThreadLocal<>();public static LoginVal get(){return loginValThreadLocal.get();}public static void set(LoginVal loginVal){loginValThreadLocal.set(loginVal);}public static void clear(){loginValThreadLocal.remove();}
}
关于TransmittableThreadLocal想深入了解其原理可以看陈某之前的文章:微服务中使用阿里开源的TTL,优雅的实现身份信息的线程间复用,应用还是非常广泛的
TransmittableThreadLocal原理
从定义来看,TransimittableThreadLocal继承于InheritableThreadLocal,并实现TtlCopier接口,它里面只有一个copy方法。所以主要是对InheritableThreadLocal的扩展。
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T>
在TransimittableThreadLocal中添加holder属性。这个属性的作用就是被标记为具备线程传递资格的对象都会被添加到这个对象中。
要标记一个类,比较容易想到的方式,就是给这个类新增一个Type字段,还有一个方法就是将具备这种类型的的对象都添加到一个静态全局集合中。之后使用时,这个集合里的所有值都具备这个标记。
// 1\. holder本身是一个InheritableThreadLocal对象
// 2\. 这个holder对象的value是WeakHashMap<TransmittableThreadLocal<Object>, ?>
// 2.1 WeekHashMap的value总是null,且不可能被使用。
// 2.2 WeekHasshMap支持value=null
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();}/*** 重写了childValue方法,实现上直接将父线程的属性作为子线程的本地变量对象。*/@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);}
};
应用代码是通过TtlExecutors工具类对线程池对象进行包装。工具类只是简单的判断,输入的线程池是否已经被包装过、非空校验等,然后返回包装类ExecutorServiceTtlWrapper。根据不同的线程池类型,有不同和的包装类。
@Nullable
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {return executorService;}return new ExecutorServiceTtlWrapper(executorService);
}
进入包装类ExecutorServiceTtlWrapper。可以注意到不论是通过ExecutorServiceTtlWrapper#submit方法或者是ExecutorTtlWrapper#execute方法,都会将线程对象包装成TtlCallable或者TtlRunnable,用于在真正执行run方法前做一些业务逻辑。
/*** 在ExecutorServiceTtlWrapper实现submit方法*/
@NonNull
@Override
public <T> Future<T> submit(@NonNull Callable<T> task) {return executorService.submit(TtlCallable.get(task));
}/*** 在ExecutorTtlWrapper实现execute方法*/
@Override
public void execute(@NonNull Runnable command) {executor.execute(TtlRunnable.get(command));
}
所以,重点的核心逻辑应该是在TtlCallable#call()或者TtlRunnable#run()中。以下以TtlCallable为例,TtlRunnable同理类似。在分析call()方法之前,先看一个类Transmitter
public static class Transmitter {/*** 捕获当前线程中的是所有TransimittableThreadLocal和注册ThreadLocal的值。*/@NonNullpublic static Object capture() {return new Snapshot(captureTtlValues(), captureThreadLocalValues());}/*** 捕获TransimittableThreadLocal的值,将holder中的所有值都添加到HashMap后返回。*/private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {ttl2Value.put(threadLocal, threadLocal.copyValue());}return ttl2Value;}/*** 捕获注册的ThreadLocal的值,也就是原本线程中的ThreadLocal,可以注册到TTL中,在* 进行线程池本地变量传递时也会被传递。*/private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<Object>, Object>();for(Map.Entry<ThreadLocal<Object>,TtlCopier<Object>>entry:threadLocalHolder.entrySet()){final ThreadLocal<Object> threadLocal = entry.getKey();final TtlCopier<Object> copier = entry.getValue();threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));}return threadLocal2Value;}/*** 将捕获到的本地变量进行替换子线程的本地变量,并且返回子线程现有的本地变量副本backup。* 用于在执行run/call方法之后,将本地变量副本恢复。*/@NonNullpublic static Object replay(@NonNull Object captured) {final Snapshot capturedSnapshot = (Snapshot) captured;return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));}/*** 替换TransmittableThreadLocal*/@NonNullprivate static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {// 创建副本backupHashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<TransmittableThreadLocal<Object>, Object>();for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {TransmittableThreadLocal<Object> threadLocal = iterator.next();// 对当前线程的本地变量进行副本拷贝backup.put(threadLocal, threadLocal.get());// 若出现调用线程中不存在某个线程变量,而线程池中线程有,则删除线程池中对应的本地变量if (!captured.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 将捕获的TTL值打入线程池获取到的线程TTL中。setTtlValuesTo(captured);// 是一个扩展点,调用TTL的beforeExecute方法。默认实现为空doExecuteCallback(true);return backup;}private static HashMap<ThreadLocal<Object>, Object> replayThreadLocalValues(@NonNull HashMap<ThreadLocal<Object>, Object> captured) {final HashMap<ThreadLocal<Object>, Object> backup = new HashMap<ThreadLocal<Object>, Object>();for (Map.Entry<ThreadLocal<Object>, Object> entry : captured.entrySet()) {final ThreadLocal<Object> threadLocal = entry.getKey();backup.put(threadLocal, threadLocal.get());final Object value = entry.getValue();if (value == threadLocalClearMark) threadLocal.remove();else threadLocal.set(value);}return backup;}/*** 清除单线线程的所有TTL和TL,并返回清除之气的backup*/@NonNullpublic static Object clear() {final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<Object>, Object>();for(Map.Entry<ThreadLocal<Object>,TtlCopier<Object>>entry:threadLocalHolder.entrySet()){final ThreadLocal<Object> threadLocal = entry.getKey();threadLocal2Value.put(threadLocal, threadLocalClearMark);}return replay(new Snapshot(ttl2Value, threadLocal2Value));}/*** 还原*/public static void restore(@NonNull Object backup) {final Snapshot backupSnapshot = (Snapshot) backup;restoreTtlValues(backupSnapshot.ttl2Value);restoreThreadLocalValues(backupSnapshot.threadLocal2Value);}private static void restoreTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {// 扩展点,调用TTL的afterExecutedoExecuteCallback(false);for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {TransmittableThreadLocal<Object> threadLocal = iterator.next();if (!backup.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 将本地变量恢复成备份版本setTtlValuesTo(backup);}private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {TransmittableThreadLocal<Object> threadLocal = entry.getKey();threadLocal.set(entry.getValue());}}private static void restoreThreadLocalValues(@NonNull HashMap<ThreadLocal<Object>, Object> backup) {for (Map.Entry<ThreadLocal<Object>, Object> entry : backup.entrySet()) {final ThreadLocal<Object> threadLocal = entry.getKey();threadLocal.set(entry.getValue());}}/*** 快照类,保存TTL和TL*/private static class Snapshot {final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value,HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {this.ttl2Value = ttl2Value;this.threadLocal2Value = threadLocal2Value;}}
进入TtlCallable#call()方法。
@Override
public V call() throws Exception {Object captured = capturedRef.get();if (captured == null || releaseTtlValueReferenceAfterCall && !capturedRef.compareAndSet(captured, null)) {throw new IllegalStateException("TTL value reference is released after call!");}// 调用replay方法将捕获到的当前线程的本地变量,传递给线程池线程的本地变量,// 并且获取到线程池线程覆盖之前的本地变量副本。Object backup = replay(captured);try {// 线程方法调用return callable.call();} finally {// 使用副本进行恢复。restore(backup);}
}
到这基本上线程池方式传递本地变量的核心代码已经大概看完了。总的来说在创建TtlCallable对象是,调用capture()方法捕获调用方的本地线程变量,在call()执行时,将捕获到的线程变量,替换到线程池所对应获取到的线程的本地变量中,并且在执行完成之后,将其本地变量恢复到调用之前。
总结
上述列举了4种方案,这里推荐方案2和方案4,其中两种方案的缺点非常明显,实际开发中也是采用的方案2或者方案4
相关文章:

用这4招优雅的实现Spring Boot 异步线程间数据传递
Spring Boot 自定义线程池实现异步开发相信大家都了解,但是在实际开发中需要在父子线程之间传递一些数据,比如用户信息,链路信息等等 比如用户登录信息使用ThreadLocal存放保证线程隔离,代码如下: /*** author 公众号…...
RocketMQ源码分析之NameServer
1、RocketMQ组件概述 NameServer NameServer相当于配置中心,维护Broker集群、Broker信息、Broker存活信息、主题与队列信息等。NameServer彼此之间不通信,每个Broker与集群内所有的Nameserver保持长连接。 2、源码分析NameServer 本文不对 NameServer 与…...

如何优化认知配比
战略可以归结为三种要素的合理配比。我们对战略的一个定义是:在终局处的判断。这其实来自于一个宗教的命题——面死而生。死是终局,生是过程,当你想做一个思想实验,或者是你真的有缘能够直面死亡,你所有关于生的认知就…...
WuThreat身份安全云-TVD每日漏洞情报-2023-02-15
漏洞名称:TOTOLINK A7100RU 安全漏洞 漏洞级别:严重 漏洞编号:CVE-2023-24276,CNNVD-202302-367 相关涉及:TOTOlink A7100RU(V7.4cu.2313_B20191024)路由器 漏洞状态:POC 参考链接:https://tvd.wuthreat.com/#/listDetail?TVD_IDTVD-2023-02977 漏洞名称:Windows NTLM 特权提…...

Unreal Engine角色涌现行为开发教程
在本文中,我将讨论如何使用虚幻引擎、强化学习和免费的机器学习插件 MindMaker 在 AI 角色中生成涌现行为。 目的是感兴趣的读者可以使用它作为在他们自己的游戏项目或具体的 AI 角色中创建涌现行为的指南。 推荐:使用 NSDT场景设计器 快速搭建 3D场景。…...

vue处理一千张图片进行分页加载
vue处理一千张图片进行分页加载 开发过程中,如果后端一次性返回你1000多条图片或数据,那我们前端应该怎么用什么思路去更好的渲染呢? 第一种:我们可以使用分页加载 第二种:我们可以进行懒加载那我们用第一种方法使用…...
(三十三)Vue之消息订阅与发布
文章目录消息订阅与发布理解使用步骤改造TodoList为消息订阅与发布上一篇:(三十二)Vue之全局事件总线 消息订阅与发布 理解 这种方式的思想与全局事件总线很相似,一种组件间通信的方式,适用于任意组件间通信。 它包…...

Http中你必须知道那点事
1, HTTP 1.1 简介 HTTP概念 HyperText Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则。 数据传输的规则指的是请求数据和响应数据需要按照指定的格式进行传输。如果想知道具体的格式,可以打开浏览器…...

ViewBinding使用入门
ViewBinding 参考资料: 新技术 ViewBinding 最佳实践 & 原理击穿 更多 ViewBinding 的封装思路 1. kotlin-android-extensions(KAE) 的问题 根据Google官方的说法, KAE存在以下问题: 污染全局命名空间不能暴露可空性信息仅支持Kotlin代码 kotlin在1.4.20中 开始废弃这…...
Retrofit源码分析实践(七)【Retrofit ConvertFactory的功能实现】
Retrofit源码分析&实践系列文章目录 Retrofit源码分析&实践(一)【从使用入手分析源码】Retrofit源码分析&实践(二)【Retrofit 免费的api测试工具引入】Retrofit源码分析&实践(三)【Retrofit 代码框架搭建】Retrofit源码分析&实践(四)【Retrofit 实…...

介电常数常用测量方法综述
张扬1,徐尚志1,赵文晖2,龚增2,赵晓群1 1同济大学,上海 2上海市计量测试技术研究院,上海 在设计电路、天线、电容器等过程中经常会涉及所用材料的介电常数, 所以深入了解介电常数的相关概念对实际工作有重…...

重生之我是赏金猎人(三)-SRC漏洞挖掘-强行多次FUZZ发现某厂商SSRF到redis密码喷洒批量反弹Shell
0x00 前言 https://github.com/J0o1ey/BountyHunterInChina 欢迎大佬们点个star 最近BugBounty挖了不少,但大多数都是有手就行的漏洞,需要动脑子的实属罕见 而今天就遇到了一个非常好的案例,故作此文 0x01 对目录批量FUZZ,发…...

学会分享,学会生活,分享5款简单易用的软件。
分享是一种博爱的心境,学会分享,就学会了生活。 1.在线图片编辑工具——佐糖 佐糖是一款在线免费图片编辑工具,采用AI人工智能技术,自动识别图片,支持一键抠图,更换背景,移除水印等。另外还提…...
Redux 与 Vuex 的区别总结
Redux 工作流程 首先,用户(通过 view)发送 Action,发送方式就用到了 dispatch 方法。然后,Store 自动调用 Reducer,并传入俩个参数,当前 State 和 收到的 Action。而 Reducer 会返回新的 State。最后,Store 根据接收到的 State,判断是否发生变化。一旦发生变化,Store…...
QT(15)- QFile
1 函数 1.1 copy bool QFile::copy(const QString &newName) QFile::copy(const QString &newName) 是 Qt 中用于复制文件的函数,将 fileName() 所指向的文件复制到 newName 指定的新位置。 在复制文件之前,源文件会先被关闭。如果被复制的文…...

GaiaX开源解读 | 表达式作为逻辑动态化的基础,我们是如何设计的
GaiaX跨端模板引擎,是在阿里优酷、淘票票、大麦内广泛使用的Native动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX开源解读》,带大家看看过去三年GaiaX的发展过程。 前言 GaiaX【https://github.com/alibaba/GaiaX】是由优酷应…...
python中adb shell相关操作
1、python 实现adb交互,进入adb shell后,如何再发送消息 os.popen("adb -s 192.168.1.100:5555 shell \"dmesg > /data/dmesg.log\"")进入进入adb shell,在执行 dmesg > /data/dmesg.log 2、python(或BAT脚本)自…...
python 之 资源使用与控制 resource模块
一、背景 2021年做可信计算时,由于甲方给的CPU利用率不能不能超过20%; 目的:我们的程序部署甲方服务器上不能阻碍其甲方服务的正常运行 我们的程序在CPU超过20%时,可以休眠几秒后继续运行 此时需要检测控制服务器的CPU资源使用信…...

蓝库云|8项关键让你看透企业「数字转型」,零代码是惊喜
各行各业都要面对的多方竞争力及不断上涨的经营成本,以及随着时代的发展,有不少企业纷纷推动数字化转型,考虑藉着应用现代化的数据和科技工具的结合,协助企业创造新的营运模式及收入来源,以提升自动化效率、优化客户体…...

(五)、编辑页面-发布长文-富文本编辑【uniapp+uinicloud多用户社区博客实战项目(完整开发文档-从零到完整项目)】
1,edit页面 1.1 新建edit页面 1.2 从本地相册选择图片或使用相机拍照。 uni.chooseImage(OBJECT) 1.3 直接上传文件到云存储。 uploadFile(Object object) 1.4 从富文本编辑器获取编辑器内容 editorContext.getContents(OBJECT) 首页富文本编辑器初始化完成时…...

docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...

【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

DBAPI如何优雅的获取单条数据
API如何优雅的获取单条数据 案例一 对于查询类API,查询的是单条数据,比如根据主键ID查询用户信息,sql如下: select id, name, age from user where id #{id}API默认返回的数据格式是多条的,如下: {&qu…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。
1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...