发布时间:2026/9/3 5:22:27
我删掉了自己写的 300 行 Future,把整个 RPC 框架改成全链异步 Jaws 系列第 6 篇。前情提要《删掉 gRPC 依赖后我用 2400 行打通了 gRPC 生态》、《HTTP/2 传输进化史》。本文代码全部出自 javahongxi/jawscommit 可查。一、一个让人不舒服的 join()故事要从 wire 模块的一次重构说起。8 月底我给 jaws-wire 加非阻塞分发时WireCallDispatcher里有一段代码让我越看越难受CompletableFutureObjectfuturemessageHandler.handleAsync(jawsRequest);// 旧版Responseresultfuture.join();// 业务线程就这么被占着等handleDispatchResult(result,ctx,serverHandler);这是 gRPC 线格式的 Provider 管线分发路径。handleAsync明明返回的是CompletableFuture——异步语义都到位了——下游却一个join()把结果等回来。业务方法快还好一旦下游是慢调用每个在途请求都占着一个业务线程干等。线程数就是并发的天花板这等于把异步框架用出了同步的损耗。改成什么样先把答案放这里CompletableFutureObjectfuturemessageHandler.handleAsync(jawsRequest);if(future.isDone()){// 快路径业务方法同步返回时直接内联处理不切线程handleDispatchResult(future.join(),ctx,serverHandler);}else{// 慢路径挂回调业务线程立刻释放future.whenComplete((result,throwable)-{handleDispatchResult(result,ctx,serverHandler);});}isDone()快路径是个值得停下来品一下的细节大多数业务方法其实跑得飞快future 返回时已经完成了这时候再挂回调纯属浪费一次线程切换。所以先探一下已完成就内联join()此时 join 不阻塞没完成才走whenComplete。一行判断换掉一次无谓的调度开销。改完 wire 这段我以为这事就结束了。直到我顺着调用链往回看了一眼客户端——才发现真正的题目有多大。二、顺着调用链往上爬RPC 框架的调用链从消费端到服务端大致是这样一条河业务代码 → 代理层 → Filter 链 → Cluster/LB → Reference → 序列化 → 传输层 ↓ (网络) 传输层 ← 序列化 ← Provider 管线 ← Filter 链 ← 业务实现wire 的join()只是河下游的一个洞。我从洞往上游走一路上看到的是这样的景象Filter 契约是同步的。jaws 的Filter接口长这样publicinterfaceFilter{Responsefilter(Caller?caller,Requestrequest);}返回Response意味着什么意味着 Filter 必须拿到最终结果才能返回。而 Filter 的下一跳caller.call(request)走到传输层传输层得等网络回包——于是每个 Filter 都被迫阻塞等一次完整的 RPC。更别提链式组装了三个 Filter 串起来就是三次「等到天荒地老」的串行阻塞。TracingFilter 已经在偷偷变形。最典型的是链路追踪 Filter改造前的消费端逻辑// 改造前同步契约下的无奈写法privateResponsehandleConsumer(...){Spanspant.nextSpan().name(spanName).start();try(Tracer.SpanInScopescopet.withSpan(span)){p.inject(span.context(),request,Request::setAttachment);Responseresponsecaller.call(request);// 阻塞等整个 RPCif(response.getException()!null){span.error(response.getException());}returnresponse;}catch(Exceptione){span.error(e);throwe;}finally{span.end();}}这段代码有个隐藏的问题span.end()在 finally 里时机是「RPC 结束」——没错但前提是线程一直停在这里等。span 的生命周期和线程的阻塞生命周期被迫绑死了。想做到「请求发出后线程就走span 等回包时再关」在同步契约下根本写不出来。DefaultResponseFuture 是一只自研怪兽。客户端等回包的核心类改造前 252 行自带状态机publicclassDefaultResponseFutureimplementsResponseFuture{protectedvolatileFutureStatestateFutureState.DOING;// 自研状态枚举protectedvolatileObjectresult;protectedvolatileExceptionexception;protectedvolatileListFutureListenerlisteners;// 自研监听器publicObjectgetValue(){synchronized(this){if(!isDoing()){returngetValueOrThrow();}longwaitTimetimeout-(System.currentTimeMillis()-createTime);if(waitTime0){for(;;){try{wait(waitTime);// 手写 wait/notify}catch(InterruptedExceptionignore){Thread.currentThread().interrupt();}if(!isDoing())break;// 重新计算剩余超时继续 wait …}}if(isDoing())cancelOnTimeout();returngetValueOrThrow();}}}手写 wait/notify 循环、手写超时补偿、手写 listener 通知、FutureState三态枚举、FutureListener回调接口……这些 JDK 在CompletableFuture里全部都有而且经过十几年生产环境锤炼。我维护这只怪兽的每一行都在重新发明 JDK 已经解决的问题。服务端管线也在同步裸奔。顺着河再看服务端一侧MessageHandler是传输层和业务之间的桥它在旧版里的签名是同步的Response handle(Object message)。Netty 的NettyChannelHandler收到请求后得等handle返回才能把响应写回 channel——事件循环线程event loop被业务调用整段占住。而事件循环是 Netty 的命根子一个 event loop 管着几十条连接它被卡 100ms这几十条连接上的所有请求全部顺延。这不是「慢一点」的问题是所有连接互相拖累的放大器。到这里问题已经从「wire 有个 join 不顺眼」升级成了框架名字叫 JAWSJava Async Wire Service异步却只做到了传输层往上一点点——Filter 拦在中间Future 是同步内核整条链是异步的躯干拖着同步的四肢。三、决策契约先行从 Filter 开刀改造顺序是这次最有意思的决策点。可选路径有三条先改传输层把 Netty/http2 内部全异步化Filter 契约不动——治标join 只是藏得更深先改 Filter 契约filter()返回CompletableFutureResponse逼着整条链跟着改先改 FutureDefaultResponseFuture 换成 CompletableFuture上层不动我选了 2契约先行。理由契约是骨架骨架定了肉才知道往哪长。Filter.filter()的返回类型一变编译器会把所有「不改就会坏」的地方精确地列出来——Filter 实现、包装器、调用方一个都逃不掉。这比人肉排查安全得多。新的Filter接口SpipublicinterfaceFilter{CompletableFutureResponsefilter(Caller?caller,Requestrequest);}配套在Caller jaws 里消费端 Reference 和服务端 Provider 的共同抽象对标 Dubbo 的 Invoker上加默认方法publicinterfaceCallerTextendsEndpoint{Responsecall(Requestrequest);defaultCompletableFutureResponsecallAsync(Requestrequest){returnCompletableFuture.completedFuture(call(request));}}callAsync()做成 default 方法是个关键的兼容设计新契约是「邀请」而不是「强拆」。老实现不改照样编译通过默认桥接到同步 call新的实现可以按自己的节奏迁到异步。整个迁移过程中测试始终是绿的。TracingFilter 的蜕变契约一换之前写不出来的代码自然就长出来了// 改造后span 生命周期不再绑死线程privateCompletableFutureResponsehandleConsumer(...){Spanspant.nextSpan().name(spanName).start();try(Tracer.SpanInScopescopet.withSpan(span)){p.inject(span.context(),request,Request::setAttachment);}returncaller.callAsync(request).whenComplete((response,throwable)-{if(throwable!null){span.error(throwable);}elseif(response!nullresponse.getThrowable()!null){span.error(response.getThrowable());}span.end();// 回包时关 span线程早就走了});}对比一下两版的本质差异注入 trace 上下文这种「出发前」的动作在调用线程完成span.end()挪进whenComplete变成「到达后」的动作由完成 future 的那个线程执行。span 的生命周期终于和 RPC 的生命周期对齐而不是和某个线程的阻塞周期对齐。这就是异步契约的价值——让代码的形状贴住事物的真实形状。AccessLog、TokenAuth、Metrics 四个内置 Filter 全部照此迁移全部改成callAsync().whenComplete()的非阻塞后处理模式。FilterProviderWrapper一个 join 也不留Filter 链的组装节点是FilterProviderWrapper它的双轨实现最能说明这次升级的彻底性OverridepublicResponsecall(Requestrequest){if(isFilterDisabled(request.getInterfaceName())){returnoriginal.call(request);}returnfilter.filter(original,request).join();// 同步门面内部一次 join}OverridepublicCompletableFutureResponsecallAsync(Requestrequest){if(isFilterDisabled(request.getInterfaceName())){returnoriginal.callAsync(request);}returnfilter.filter(original,request);// 原生异步零 join}同步入口call()保留给确实需要阻塞语义的调用方内部一次 join 封装异步入口callAsync()则全程无 join 直通。框架内部全部走callAsync同步门面只留给用户边界。四、把 252 行怪兽换成一行 extendsFilter 契约改完轮到那只怪兽了。DefaultResponseFuture的改造方向其实早在选型时就定了JDK 的CompletableFuture已经把状态机、监听器、超时、组合算子全部做好自研的意义只剩「历史包袱」。于是改造后的全部声明是——publicclassDefaultResponseFutureextendsCompletableFutureResponseimplementsResponseFuture{privatefinalRequestrequest;privatefinalinttimeout;OverridepublicvoidonSuccess(Responseresponse){complete(response);}OverridepublicvoidonFailure(Responseresponse){completeExceptionally(response.getThrowable());}}252 行瘦身到 128 行——剩余部分基本是 Javadoc 和给老 API 用的异常转换。删掉的东西列一下全是 JDK 替我保管的FutureState三态枚举 →CompletableFuture内部状态机FutureListener 手写通知循环 →whenComplete/thenApply手写 wait/notify 超时循环 → 传输层每请求定时器 completeExceptionally手写的getRawValue()/getThrowable()状态判读 →getNow(null)/isCompletedExceptionally()组合保留ResponseFuture门面接口是为了 API 稳定getValue()/getTimeout()/getRequestId()这些老签名还在但内核已经完全是 CompletableFuture 了。阻塞式用户代码getValue()委托给get()异步用户代码直接whenComplete同一只 future 两种活法。有个容易被忽略的收益藏在 commit message 里「消除双 Future 分配开销和 monitor lock 成本」。旧实现里 Reference 层为了拿到 CompletableFuture 语义要再 new 一个 CompletableFuture 把 ResponseFuture 桥接过去——每个请求两只 future、一次锁。现在DefaultResponseFuture本身就是 CompletableFutureAbstractReference 里直接whenComplete链上去一只 future 贯穿到底// AbstractReference.callAsync网络层完成 future 时顺路做统计if(responseinstanceofCompletableFuture?cf){returncf.whenComplete((r,t)-{decrActiveCount(response);if(tnull){longelapsedSystem.nanoTime()-startTime;succeededElapsed.addAndGet(elapsed);succeededCount.incrementAndGet();}}).thenApply(r-(Response)r);}调用统计activeCount、成功耗时累计本来要靠额外回调挂钩现在就是 future 链上的一环。基础设施统一之后横切逻辑的挂载方式自然就变优雅了——这是我认为比「少 300 行」更值钱的部分。五、收尾语义对齐与三个细节主战役之外这次还顺手做了三件值得单独一说的事。异常语义对齐。getException全链路改名getThrowable类型从Exception放宽到Throwable。别小看这个放宽序列化栈溢出抛的是StackOverflowError同步业务代码抛Error子类的场景也不罕见老契约里这些直接被类型系统拒收只能丢信息。改名加放宽是一起还的旧债。服务端管线 handleAsync。前面埋的伏笔在这里兑现MessageHandler的签名从同步handle()换成handleAsync()返回CompletableFutureObject由AbstractRequestHandler统一实现——Provider 查找、方法解析这些前置逻辑照旧在事件循环完成纯内存操作快且安全真正的业务调用交给doHandleAsync。NettyChannelHandler收到请求后改成挂whenComplete回调回包到达时再把响应写回 channel。事件循环线程从此只做「接请求、写响应」两件事中间漫长的业务执行与它无关。熔断计数修正。WireClient 的错误熔断要对齐 Http2Client 的whenComplete语义——区分「业务异常」和「框架错误」业务抛JawsBizException说明服务本身活着只是这次调用失败不计入熔断网络错误、超时这类框架级错误才incrErrorCount()成功后resetErrorCount()。区分的意义在于避免业务侧正常的异常流量把熔断器误打开——框架挂了才该熔断业务出错不该。用户侧异步 API 落地。契约改完了得让用户用得上。DemoService接口补了helloAsyncString 返回和getUserAsyncPOJO 返回两个异步方法NettyConsumer 里演示了从基础回调到并发组合的完整用法// 基础异步提交后线程立刻返回回调里拿结果CompletableFutureStringasyncHellodemoService.helloAsync(async-lily);asyncHello.thenAccept(result-System.out.println(callback result));// thenApply 链式变换demoService.helloAsync(chain-demo).thenApply(String::toUpperCase).thenAccept(s-System.out.println(chained s));// allOf 并发组合两个调用齐了再汇总CompletableFutureStringf1demoService.helloAsync(user-A);CompletableFutureStringf2demoService.helloAsync(user-B);CompletableFuture.allOf(f1,f2).thenRun(()-System.out.println(combined [f1.join(), f2.join()]));六、W 落定Async Wire Service 名实对齐回头看这次改造的完整地图层改造前改造后Filter 契约Response filter(...)同步拦截CompletableFutureResponse filter(...)异步管道Caller 抽象只有call()新增callAsync()default 桥接客户端 Future252 行自研 wait/notify 状态机extends CompletableFuture128 行传输层分发future.join()占线程isDone()快路径 whenComplete回调服务端管线同步handle()handleAsync()返回 CompletableFuture流式wire/http2 各自实现共享StreamPublisherFlow.Publisher 基座JAWS 这个名字立项目时就起好了——JavaAsyncWireService。但说实话改造之前那个 A 是有水分的异步只在传输层往上一点点Filter 拦腰一截同步客户端内核是 wait/notify。这次全链贯通之后从业务代理、Filter、Cluster、Reference 到传输层再到 wire 分发CompletableFuture 一竿子插到底A 字才算名副其实。复盘这次改造有三条经验值得留给读者契约先于实现。改返回类型这种「编译器帮你找全改动点」的路径永远比人肉排查安全。同步入口保留为门面、异步作为原生路径的双轨设计让迁移过程没有一天是红的。删自研代码之前先确认标准库真的覆盖了你的场景。CompletableFuture 覆盖了状态机、监听、超时、组合而 jaws 特有的每请求超时归传输层定时器管理——边界划清了删起来才不心虚。顺着调用链找下一个洞。wire 的一个 join() 只是症状从症状出发往上游走完整条链才能看到问题的全貌。单点优化容易全链对齐难难就难在要舍得把「已经能跑」的代码推翻。jaws 现在核心四模块约 2.3 万行README 里那句「可以从头读到尾」依然成立——而且这次重构之后「读」的体验更好了你顺着一条callAsync从 Filter 走到传输层看到的将是同一种异步语言而不是三种方言的翻译现场。下一篇候选是自适应负载均衡的实现剖析power of two choices 在 RPC 里的落地感兴趣的可以先去仓库读AdaptiveLoadBalance我们下篇见。项目地址github.com/javahongxi/jaws — 核心约 2.3 万行、可从头读到尾的轻量级 RPC 框架。三传输Netty 二进制 / HTTP/2 / gRPC 线格式gRPC 互通Server Streaming自适应负载均衡实测 10 万 QPS。

相关新闻

2026/9/3 5:22:27

Git常用命令1

1、本地Git命令 1.1、Git Commit Git Commit 项目快照(Snapshot) 每次 git commit 并不是粗暴地复制整个目录,而是对已跟踪文件当前状态的轻量级快照记录。Git 通过对比当前版本与上一次提交的差异(diff),…

2026/9/3 5:17:26

TRAE 接入 SenseNova 大模型 API 教程:配置方法与使用步骤

TRAE 接入 SenseNova 大模型 API 教程:配置方法与使用步骤TRAE 自定义模型配置教程:接入商汤日日新 SenseNova API,填写 Base URL、API Key 与模型 ID 的完整步骤关键词 TRAE 接入 SenseNova、TRAE 自定义模型、TRAE 配置教程、SenseNova API…

2026/9/3 5:17:26

商汤日日新 SenseNova U1 Fast 使用教程:信息图生成 API 调用方法

商汤日日新 SenseNova U1 Fast 使用教程:信息图生成 API 调用方法SenseNova U1 Fast 使用教程:独立图像生成接口调用方法、11 种宽高比与信息图 prompt 写法关键词 SenseNova U1 Fast、sensenova-u1-fast、商汤日日新、信息图生成、AI 生成信息图、文生图…

2026/9/3 5:27:27

Python实现6S大气校正:遥感影像自动化处理与多卫星支持

简介:本资源是面向遥感科研与工程应用开发者的6S大气校正Python实现工具包,专为解决GF-1/2、Landsat-8、Sentinel-2等主流卫星影像的大气影响去除问题而设计,适用于地表反射率反演、植被指数计算、水体识别等定量遥感分析场景,兼顾…

2026/9/3 5:27:27

线性调频Chirp信号工程落地:从数学公式到嵌入式实现

简介:本资源是一份面向通信工程专业学生与初阶信号处理实践者的MATLAB教学脚本,聚焦Chirp调制原理及其在扩频BOK(Binary Offset Keying)中的实现与验证。资源解决的核心问题是:如何在MATLAB中从零构建Chirp信号、完成相…

2026/9/3 5:27:27

基于分形理论的粗糙表面接触刚度MATLAB计算与工程应用

简介:本资源是一套面向机械工程、材料科学及接触力学研究者的MATLAB计算工具,聚焦于粗糙表面法向接触刚度的分形理论建模与数值求解,解决传统平滑表面假设在微纳尺度接触分析中的局限性问题。压缩包为RAR格式,共含2个MATLAB脚本文…

2026/9/3 5:27:27

C语言贪吃蛇实战手记:链表+状态机实现高分期末作业

简介:本资源是一份高质量的C语言期末大作业项目——贪吃蛇大作战完整源码包,面向计算机相关专业本科生及C语言初学者,解决课程设计、期末考核中缺乏可运行、可展示、可讲解的综合性实践项目问题。压缩包共29个文件,包含核心源码文…

2026/9/1 16:02:17

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/2 9:00:32

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/2 8:41:06

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/3 0:02:06

零基础装 OpenClaw 小龙虾 AI:Windows 一键部署教程与避坑要点

Windows 部署 OpenClaw 完整教程|本地 AI 智能体 5 分钟落地,环境配置一次搞定 版本说明:Windows 3.1.0 / Mac 2.7.9 写在前面 近两年开源 AI 领域有一款被称作「数字员工」的工具持续走热,它就是 OpenClaw,圈内人更习…

2026/9/3 0:02:06

Hermes Agent 本地部署新方案:Windows 整合包减少依赖报错

Windows 本地部署 Hermes 太麻烦?这版一键包 5 分钟快速跑通 很多人想体验 Hermes Agent,但真正开始部署时,往往会卡在环境配置这一步。 需要安装各类依赖、调试运行环境、处理路径问题,还容易遇到命令行报错、系统拦截、文件缺…

2026/9/3 0:02:06

实测 OpenClaw 一键包,5 分钟完成本地自动化环境搭建

OpenClaw 本地 AI 自动化工具部署指南|使用一键包规避环境配置难题 痛点:部署 AI 自动化工具常常要处理 Python、Node.js 各类依赖,版本冲突、环境配置耗费大量时间,OpenClaw 提供一键安装包,降低部署门槛。 适配系统&…

2026/9/2 1:15:22

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/2 1:15:22

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/2 1:15:20

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…