简介一份面向 Java Web 开发者和后端初学者的 JSON-RPC 入门案例包围绕 JSON-RPC 轻量级远程调用协议演示客户端通过 HTTP 发送请求、服务器解析并执行方法、再以 JSON 返回结果的完整流程可帮助读者快速理解分布式服务通信与前后端交互的实现方式。压缩包共 35 个文件大小约 18.86MB以 25 个 jar 依赖库为主包含 Spring、Jackson、jsonrpc4j 等常用组件同时还有 class 编译产物、xml 配置、jsp 页面和 properties 文件并留有 index.jsp 主入口与 WEB-INF 配置目录整体呈现了一个可运行的 Java Web 项目结构。资源已有 223 人学习内容覆盖 JSON-RPC 2.0 的请求响应字段、参数组织、唯一标识匹配、错误返回格式以及服务端方法如何封装和暴露。通过学习包内案例读者可掌握基于 jsonrpc4j 和 Spring 搭建服务端与客户端的方法了解在 Tomcat 等环境中部署与调试的常见思路并能迁移到实际项目中的分布式通信、API 对接等场景是一份兼顾原理与操作的入门资料。 说实话我第一眼看到这个名为 jsonRPC.rar 的压缩包时脑子里冒出来的不是协议文档而是一句老话好东西往往都藏在朴素的文件名里。干这行久了你会发现项目命名越随意里面的东西往往越硬核——没有花里胡哨的营销词文件名就是内容本身。这个压缩包里装的是一套基于 jsonRPC 协议的远程调用通信框架的完整工程源码和部署文档。如果你正在做前后端分离项目、微服务拆分或者需要在多个系统之间做轻量级接口调用jsonRPC 绝对是个被低估的选项。它不是 RESTful API 的替代品而是另一种更纯粹、更适合内部服务间通信的解决方案。这套东西适合谁适合那些被庞大框架折腾得够呛、想回归通信本质的开发者也适合正在设计服务间调用协议、需要一份可参考的落地范例的架构师。1. 项目整体设计与思路拆解1.1 为什么是 jsonRPC 而不是 RESTful API先说一个很多人都会问的问题现在不都流行 RESTful 吗为什么还要用 RPCRESTful API 面向资源强调的是资源的状态转移一个 URL 代表一个资源HTTP 动词代表操作。这设计本身没毛病特别适合对外提供 API 的场景——语义清晰、便于缓存、易于调试。但当你的场景变成内部服务间调用RESTful 就开始显得有点笨重了。我给你打个比方。RESTful 像是你通过政务大厅的窗口办事拿号、填表、每个窗口对应一种业务流程清晰但步骤繁琐。jsonRPC 则像是你直接拨内线电话找对应的人拨通、报需求、拿结果直达目标没有中间环节。在服务间通信这种“自己人”的场景里jsonRPC 的轻量直连优势非常明显。这个压缩包里的项目选 jsonRPC 而不是 RESTful我认为有以下几个核心考量协议简洁jsonRPC 2.0 规范全文就一页纸请求和响应结构极其简单没有 RESTful 那种资源命名、状态码语义、版本策略的各种纠结。面向方法而非资源服务间调用天然是帮我执行个操作而不是操作某个资源。比如计算订单金额、校验用户权限这类操作型接口用 RPC 表达更自然。传输层无关jsonRPC 规范本身不绑定传输方式你可以跑在 HTTP、WebSocket、TCP 甚至消息队列上。这个项目里默认实现是 HTTP 传输但底层做了抽象换传输层不需要动业务代码。排错简单所有错误都通过统一的错误对象返回配合错误码定位问题比 RESTful 那种200 里包着错误信息的常见做法清晰得多。1.2 工程模块划分的讲究我解压这个 rar 包看了一下工程结构它的模块划分很有代表性基本上是一个生产级 RPC 框架该有的样子。整个工程分为四个核心模块协议层、传输层、序列化层、业务接入层。协议层负责 jsonRPC 2.0 规范的消息解析和构造比如请求对象里必须有jsonrpc、method、params、id这四个字段协议层就是对这些字段做严格校验和封装。传输层负责网络通信这个项目是用 HTTP 做的但接口设计上做了抽象也就是说将来想换成 WebSocket 或者直接走 TCP传输层的替换成本很低。序列化层处理 JSON 的编码解码看起来简单但里面有大文章——后面我会详细讲。业务接入层面向调用方提供类似本地调用的编程接口让调用方不需要关心网络细节。这个分层的思路最值得借鉴的地方在于它把稳定和变化分开了。协议规范是稳定的传输方式是可变的JSON 序列化细节是可优化的业务接入是随时要扩展的。分层之后每一层的调整都不会波及其他层。2. 核心细节解析与实操要点2.1 协议规范里的几个隐藏细节jsonRPC 2.0 规范看起来非常简单但我实际用下来才发现细节决定成败。这个项目里对协议细节的处理值得拿出来单独说说。先看标准请求的结构{ jsonrpc: 2.0, method: orderService.calculateAmount, params: {orderId: 10086, couponCode: SAVE50}, id: 1 }响应结构{ jsonrpc: 2.0, result: {amount: 99.50, discounted: true}, id: 1 }这里有几个坑需要注意。第一个是id字段。规范里说id用于关联请求和响应但没有规定必须是整数。这个项目里建议统一使用单调递增的整数而且要保证在同一连接内不重复。为什么因为如果你用的是异步请求模式同一时间可能有多个请求在网络上飞行响应回来之后全靠id匹配是哪个请求的结果。我用过字符串做id的也能工作但排查问题的时候还是整数最直观。第二个是通知类型的请求。jsonRPC 规范里有一种特殊消息——通知它没有id字段意味着发送方不需要接收响应。这个项目里把通知用在日志上报、指标收集这类场景效果极好。还是同样的理由这让发送方节省了等待时间接收方也不需要回复吞吐量能上一个台阶。第三个是批量请求。规范允许客户端把多个请求放在一个 JSON 数组里一次性发送服务端必须按顺序处理并返回结果数组。这个功能在需要并发调用多个服务方法时很实用可以减少连接建立的开销。不过我要提醒你批量请求的响应顺序不保证和请求顺序一致因为服务端可以并发处理各个请求然后按完成时间返回。2.2 服务注册与调用的编程模型这个项目里最让我觉得舒服的设计是服务注册和调用的编程模型。服务端只需要在启动时注册一个普通函数这个函数就自动变成了一个可远程调用的方法# server_example.py import jsonrpc_server def calculate_amount(order_id, coupon_codeNone): 计算订单金额这是普通业务函数 base_price 199.00 discount 0.0 if coupon_code SAVE50: discount 50.0 return { amount: base_price - discount, discounted: discount 0 } server jsonrpc_server.create_server(host0.0.0.0, port8080) server.register(orderService.calculateAmount, calculate_amount) server.start()注意看calculate_amount就是一个普普通通的 Python 函数没有继承任何基类没有贴任何魔法注解只是通过register方法注册到了服务端。这种设计对业务代码零入侵老项目想接入 RPC不需要改业务逻辑代码只需要在启动时多写几行注册代码。客户端调用更简单# client_example.py import jsonrpc_client client jsonrpc_client.create_client(http://192.168.1.100:8080/api/rpc) # 同步调用 result client.call(orderService.calculateAmount, order_id10086, coupon_codeSAVE50) print(result) # 输出: {amount: 149.0, discounted: True} # 异步调用不阻塞拿到结果后走回调 future client.call_async(orderService.calculateAmount, order_id10086, coupon_codeSAVE50) future.add_done_callback(lambda fut: print(fut.result()))用起来感觉不像在做远程调用更像在调一个本地的函数。这种透明化 RPC的体验大大降低了使用门槛。但对于新接触 RPC 的开发者这里有个隐藏的心理陷阱——远程调用和本地调用的失败模式是完全不同的。本地函数挂了会立刻抛异常而远程调用挂了可能是网络超时、服务端崩溃、参数序列化失败这些都不是立即能感知的。所以你写调用代码的时候必须对异常有充分预期。3. 实操过程与核心环节实现3.1 从零搭建一个可用的 jsonRPC 服务我来带你完整走一遍实操流程。我是用 Python 实现的因为 Python 的 HTTP 处理能力加上这个项目本身的轻量特性最适合演示。如果你用 Java、Go 或者其他语言思路完全一样只是语法不同。第一步环境准备这个项目依赖极简只需要 Python 3.6不需要安装任何第三方库因为 HTTP 部分用的是标准库http.serverJSON 解析用的是标准库json。这点我觉得很良心没有依赖地狱压缩包解开就能跑。第二步定义消息编解码器先把 jsonRPC 协议的消息编解码做出来。协议要求请求对象有三个必填字段和一个可选字段jsonrpc固定为 2.0、method方法名、params参数可以是结构体也可以是数组、id用于匹配请求和响应通知消息可省略。响应对象也有三字段jsonrpc、result或error二选一、id。# message.py import json class RPCRequest: def __init__(self, method, paramsNone, request_idNone): self.jsonrpc 2.0 self.method method self.params params self.id request_id def to_dict(self): data { jsonrpc: self.jsonrpc, method: self.method } if self.params is not None: data[params] self.params if self.id is not None: data[id] self.id return data class RPCResponse: def __init__(self, resultNone, errorNone, request_idNone): self.jsonrpc 2.0 self.result result self.error error self.id request_id def to_dict(self): data {jsonrpc: self.jsonrpc} if self.error is not None: data[error] self.error else: data[result] self.result data[id] self.id return data def parse_request(json_str): data json.loads(json_str) return RPCRequest( methoddata.get(method), paramsdata.get(params), request_iddata.get(id) )这段代码看起来很基础但有个关键设计点to_dict()方法里对id做了is not None判断这是为了支持通知消息——通知没有id序列化的时候就不能带id字段否则协议校验会失败。第三步实现服务端核心分发逻辑服务端的核心是方法查找和参数绑定。方法注册到一个字典里收到请求后按method名查字典找到了就调用对应函数没找到就返回统一的方法未找到错误。参数绑定支持两种形式按位置传递的数组和按名称传递的对象。这个项目里两种都支持实际使用中发现按名称传递是绝对主流因为可读性好、不容易错位。# server_core.py import json from http.server import HTTPServer, BaseHTTPRequestHandler from message import RPCRequest, RPCResponse class RPCDispatcher: def __init__(self): self.methods {} def register(self, method_name, func): self.methods[method_name] func def dispatch(self, method_name, params): if method_name not in self.methods: raise MethodNotFoundException( fMethod {method_name} not found ) func self.methods[method_name] if isinstance(params, dict): return func(**params) elif isinstance(params, list): return func(*params) else: return func() class RPCRequestHandler(BaseHTTPRequestHandler): def do_POST(self): if self.path ! /api/rpc: self._send_error_response(None, -32601, Method not found) return content_length int(self.headers.get(Content-Length, 0)) request_body self.rfile.read(content_length).decode(utf-8) try: req RPCRequest.from_json(request_body) result self.server.dispatcher.dispatch( req.method, req.params ) resp RPCResponse(resultresult, request_idreq.id) except Exception as e: resp RPCResponse( error{code: -32603, message: str(e)}, request_idgetattr(req, id, None) ) self._send_json_response(resp.to_dict()) def _send_json_response(self, data): body json.dumps(data).encode(utf-8) self.send_response(200) self.send_header(Content-Type, application/json) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, format, *args): pass # 禁止默认日志输出 class JsonRpcServer: def __init__(self, host0.0.0.0, port8080): self.host host self.port port self.dispatcher RPCDispatcher() self.http_server None def register(self, method_name, func): self.dispatcher.register(method_name, func) def start(self): self.http_server HTTPServer( (self.host, self.port), RPCRequestHandler ) self.http_server.dispatcher self.dispatcher self.http_server.serve_forever() def stop(self): if self.http_server: self.http_server.shutdown()这里有个值得说的设计细节——RPCRequestHandler里的do_POST方法。为什么只处理 POST因为 jsonRPC 的请求体是 JSON可能会比较大用 GET 会把参数塞在 URL 里既受长度限制又不安全。强制 POST 让整个实现简化了好多不需要考虑 URL 解析。第四步实现客户端调用逻辑客户端的核心是构造请求并解析响应。同步调用用 Python 的urllib就够了异步调用需要用到线程池或者绑定一个事件循环。这个项目里实现了一个简洁的异步封装基于concurrent.futures。# client_core.py import json import urllib.request from concurrent.futures import ThreadPoolExecutor from message import RPCRequest, RPCResponse class JsonRpcClient: def __init__(self, endpoint, timeout30): self.endpoint endpoint self.timeout timeout self.request_id 0 self.executor ThreadPoolExecutor(max_workers10) def _next_id(self): self.request_id 1 return self.request_id def call(self, method, *args, **kwargs): 同步调用 req RPCRequest( methodmethod, paramskwargs if kwargs else (args if args else None), request_idself._next_id() ) return self._invoke(req) def call_async(self, method, *args, **kwargs): 异步调用返回 Future 对象 return self.executor.submit( self.call, method, *args, **kwargs ) def notify(self, method, *args, **kwargs): 发送通知消息不等待响应 req RPCRequest( methodmethod, paramskwargs if kwargs else (args if args else None), request_idNone # 关键没有 id 就是通知 ) self._send(req) def _invoke(self, req): resp_data self._send(req) if error in resp_data: err resp_data[error] raise RpcError(err.get(code), err.get(message)) return resp_data.get(result) def _send(self, req): payload json.dumps(req.to_dict()).encode(utf-8) http_req urllib.request.Request( self.endpoint, datapayload, headers{Content-Type: application/json}, methodPOST ) with urllib.request.urlopen(http_req, timeoutself.timeout) as resp: return json.loads(resp.read().decode(utf-8))客户端这块最关键的参数是timeout。我见过太多生产事故都是因为没设置超时服务端假死之后客户端线程全部卡住最后整个调用方系统线程池耗尽崩溃。记住一句话任何网络调用都必须设置超时没有例外。3.2 跑通一个完整的调用链按照上面的代码我们启动服务端再跑客户端# 终端1启动服务端 python server_example.py # 终端2启动客户端 python client_example.py如果一切正常你应该能在终端2看到输出结果——计算后的订单金额和折扣状态。我第一次跑通这个全流程的时候最大的感受是这也太平静了。没有几十兆的依赖包下载没有复杂的配置文件没有启动失败需要排查的环境问题。这让我意识到RPC 协议本身的复杂度真的不高高的是那些为了适配各种企业级场景而叠加的框架层。当年 Dubbo、gRPC 那些重框架把服务发现、负载均衡、熔断、限流全部缝进去单是理解它们的配置体系就够学一两个月的。而 jsonRPC 这种轻量方案恰恰让你先理解了远程调用这件事本身再去考虑那些附加能力的必要性。4. 常见问题与排查技巧实录4.1 错误码速查表jsonRPC 2.0 规范定义了标准错误码但实际项目中很多人只会用一种。这个压缩包里的项目对错误码做了完整映射我强烈建议你把它当作开发规范用起来。错误码含义常见触发场景排查建议-32700解析错误Parse error请求体不是合法 JSON检查客户端序列化是否有特殊字符导致 JSON 格式损坏-32600无效请求Invalid Request报文结构不符合 jsonRPC 规范检查是否缺少jsonrpc或method字段-32601方法未找到Method not found调用了服务端未注册的方法比对方法名拼写和服务端注册名注意大小写-32602无效参数Invalid params参数类型不对或缺少必填参数打印服务端收到的原始 params 检查结构-32603内部错误Internal error被调函数抛了未捕获异常查服务端日志中的异常堆栈-32000 至 -32099服务端自定义错误业务逻辑校验失败等看错误对象的data字段里是否附带详情我在实际使用中还发现错误码映射这件事看似简单但很多项目都会在服务端自定义错误这个区间做出各种花样。有些团队把整个业务错误体系映射到 -32000 附近的编码段有些则沿用 HTTP 的 400 系列错误码逻辑。我倾向于在 jsonRPC 错误对象里加入一个data字段用来携带业务错误码和错误详情——这比把业务错误语义全都揉进 message 字符串强得多程序可以精确判断错误类型不必做字符串匹配这种脆弱逻辑。4.2 我踩过的三个真实坑第一个坑是Content-Type 设置错误。客户端发的请求头没设Content-Type: application/json服务端那边如果用的严格模式会直接解析失败返回 -32700。这个问题在浏览器环境下出现的最多因为浏览器对 Content-Type 有自动处理有时候你不设它甚至会加一个默认的text/plain;charsetUTF-8直接导致解析错误。第二个坑是批量请求的响应顺序。jsonRPC 规范明确说了服务端可以并发执行批量请求响应按任意顺序返回。我最初写代码时下意识假设响应数组的顺序和请求数组一致结果在高并发下间歇性出现数据错配排查了很久才发现是这个假设导致的问题。所以如果你的代码依赖批量请求的响应顺序一定要用id字段重新匹配绝对不能靠数组位置对应。第三个坑是超大 JSON 请求体的性能问题。如果你在 params 里传了一段 base64 编码的文件内容或者大文本字符串序列化和反序列化的开销会直线上升。实测数据是这样的当单个 JSON 报文超过 1MB 时Python 标准库的json模块解析耗时开始变得不可忽略超过 5MB 时可能成为系统的性能瓶颈。后来我在传输层加了一层 Gzip 压缩报文体积能压缩到原来的十分之一左右性能问题才缓解。4.3 调试技巧分享调试 jsonRPC 服务最直接的工具有两个curl和浏览器的控制台。用 curl 调试请求curl -X POST http://localhost:8080/api/rpc \ -H Content-Type: application/json \ -d {jsonrpc: 2.0, method: orderService.calculateAmount, params: {orderId: 10086}, id: 1}服务端开启调试模式后可以把每一个请求和响应的原始报文打印到日志里。这个能力我强烈建议你放在框架基础设施里而不是调试完就删掉。因为生产环境排查问题的时候原始报文日志就是故障现场监控录像没有它全靠猜和碰运气。当然生产环境日志要做好脱敏不能把敏感信息直接打出来。5. 项目背后的扩展思考5.1 这个方法还能用在哪些地方jsonRPC 的适用场景比多数人想象的要宽。我在实际项目里用过几种比较有意思的变形这里分享出来供你参考。浏览器插件与本地服务通信本地监听一个 HTTP 端口用 jsonRPC 报文暴露操作接口浏览器插件通过 fetch 调用。因为协议简单调试方便这套方案比自定义 WebSocket 协议稳定得多。游戏服务器之间的战斗结算战斗服务只负责发消息结算服务只负责算结果两边用 jsonRPC 调。参数校验严格错误信息明确对日志监控非常友好。移动端弱网环境下的业务操作弱网下报文越小越好jsonRPC 报文天然比同等业务含义的 RESTful 请求要短因为根本没那么多冗余字段。加上批量请求可以把多次操作合并成一次往返效果更明显。这些场景都有一个共同特征调用关系明确参数结构清晰对语义标准化的要求远高于对生态完善度的要求。这就是 jsonRPC 能站住脚的根本原因。5.2 关于传输方式的更多可能性这个项目默认用 HTTP 做传输层但我前面提到过协议本身不绑定传输方式。我再补充一个思路把 jsonRPC 消息放在消息队列里传输可以利用消息队列的持久化、重试、延迟队列能力做到异步 RPC。适用于那种不需要实时响应、但需要保证最终一致性的场景比如订单状态变更后的通知类操作。另一个方向是 WebSocket 传输。浏览器环境下用 WebSocket 跑 jsonRPC 可以实现真正的服务端主动推送——这在 RESTful 时代还需要轮询或者 SSE 才能勉强实现在 jsonRPC 里不过是个消息类型的问题。我个人在实际操作中的体会是别急着用重型框架先想清楚你要解决的问题本质是什么。如果你只是想让两个服务之间能互相调用方法、能传参、能拿到返回值、能报告错误一个 jsonRPC 就足够了。它轻量到你把整套源码读完、改写好、部署上线可能比研究一个微服务框架的配置项还要快。这个压缩包里的实现就是这种小工具解决大问题思路的典型代表。最后再分享一个小技巧如果你要在自己的项目里引入 jsonRPC别把整个框架代码复制过去然后开始改。你先原样跑通再按需剪裁。你会发现真正需要留下的核心代码可能不到两百行但这两百行是你读透了原理之后留下来的而不是从网上抄来的——这两者的差别在你后面排查问题时会体会得特别深。本文还有配套的精品资源点击获取