资讯动态

Spring Boot手写JSON-RPC Server:轻量协议解析与请求分发实战

发布时间:2026/10/20 7:33:34 来源:尧图企业网站定制
简介面向Java开发者的Spring Boot JSON-RPC 2.0服务端实现资源适合需要在项目中快速搭建轻量级远程调用接口的团队或正在学习Spring Boot与JSON-RPC协议的学生。资源通过标准JSON-RPC 2.0协议实现方法调用适用于接口联调、微服务内部通信及教学演示等多种场景开发者可根据自身技术层级直接复用或改造。压缩包内共26个文件核心为5个Java源文件及对应class文件用于定义服务端方法和请求处理逻辑另有4个properties文件管理运行配置2个xml文件支撑构建或映射并附带Maven包装器与IDE工程标识整体仅55KB结构紧凑导入开发工具即可运行调试。示例中客户端以application/json发送请求包含id、jsonrpc、method、params字段服务端返回对应的result清晰呈现了JSON-RPC 2.0的请求-响应模型便于对照实现自定义业务方法。已有299人学习下载适合作为快速上手Spring Boot服务端接口开发的参考模板。1. Spring Boot 里直接起一个 JSON-RPC Server为什么值得自己搭而不是引重型框架先给结论如果你的项目只是需要在 Spring Boot 服务里暴露一组 JSON-RPC 接口给前端、给脚本、给另一套系统调用真没必要引入 XML-RPC 那套老古董也不用为了一个 RPC 调用把 Dubbo 或者 gRPC 全家桶搬进来。JSON-RPC 2.0 的协议体极其简单请求就是一个带jsonrpc、method、params、id字段的 JSON 对象响应也是固定结构整个协议规范几千字就能讲完。用 Spring Boot 现有的 MVC 能力加上一个RequestBody和一个路由分发器就能实现一个干净、可控、能调试的 JSON-RPC Server而且能和你的 Spring 容器无缝整合——Service里写业务逻辑RPC 层只负责协议解析和调用。这套资源适合正在做前后端分离、需要给第三方系统提供轻量 API、或者想把内部工具平台的接口统一成 RPC 风格的开发者。它解决的核心问题是不引入额外注册中心和复杂序列化配置用最朴素的 Spring 方式把 JSON-RPC 跑起来并且能处理批量请求、异常码、参数校验这些容易被忽略的细节。2. 先搞清楚 JSON-RPC 2.0 协议细节请求、响应、错误码与批量调用2.1 请求对象里每个字段的语义和默认值陷阱JSON-RPC 2.0 的请求对象有四个关键字段jsonrpc必须是字符串2.0大小写和类型都不能错否则服务端应该返回Invalid Requestmethod是字符串对应你要调用的方法名params是结构化参数可以是数组按位置传参也可以是对象按名称传参id用于关联请求和响应可以是字符串、数字或null。这里最容易踩的坑是id为null的情况——规范里说如果id不存在或为null表示这是一个通知notification服务端不需要返回响应。很多人在实现时容易把id: null和id: 0搞混0是合法 id必须有响应null才是通知。另外params可以省略但省略和传null在部分客户端里行为不一致所以服务端解析时最好把params为空的情况统一当成无参数处理。2.2 响应结构、错误码与标准化异常体响应对象只有三种情况成功时返回{jsonrpc:2.0,result:...,id:...}调用出错时返回{jsonrpc:2.0,error:{code:...,message:...,data:...},id:...}通知则完全不返回。错误码有规范约定-32700表示解析错误-32600表示无效请求-32601表示方法不存在-32602表示参数无效-32603表示内部错误-32000到-32099是服务端自定义错误的保留区间。这里要特别留意的坑是message字段必须是字符串data字段可以省略但如果你要把后端异常堆栈传出去一定要控制长度否则响应体可能过大。错误码的正负号也容易写错比如参数无效是-32602不是32602客户端判断时通常用error.code ! undefined而不是if (error)因为error对象本身存在但可能没 code。2.3 批量请求的并发与原子性JSON-RPC 2.0 支持客户端一次提交一个请求数组服务端必须返回一个响应数组响应数组的顺序可以和请求数组不一致。这意味着实现时就遇到一个选择每个请求是串行执行还是并发执行如果串行简单但慢如果并发响应顺序就会乱需要靠id关联。另一个坑是如果批量请求里的某个元素不是合法请求对象规范要求服务端返回一个错误响应而不是整个批量请求全部失败。我一般在实现时会把批量请求拆成单个任务丢进线程池然后用CompletableFuture汇总最后按请求顺序排列响应。但如果业务上有依赖关系比如第二个请求依赖第一个请求的结果那就不能并发处理这时候应该由客户端拆成多次调用而不是在服务端做状态管理。2.4 一个零依赖的协议解析与校验逻辑在动手写 Spring Boot 接口之前我习惯先写一个纯 Java 的协议校验类不依赖 Spring方便单元测试。这个类只做三件事判断请求是不是合法 JSON、判断是不是 JSON-RPC 2.0 对象、把params转成特定类型。常见做法是先用Jackson把请求体转成JsonNode然后逐字段校验。这里要注意一个细节jsonrpc字段值必须是2.0但有些客户端会传2.0 带空格或者传数字2.0服务端要做严格字符串比较不能equalsIgnoreCase。public class RpcProtocolValidator { private static final ObjectMapper MAPPER new ObjectMapper(); public static boolean isValidRequest(String body) { try { JsonNode node MAPPER.readTree(body); if (node.isArray()) { // 批量请求每个元素都要合法才算整体合法 for (JsonNode item : node) { if (!isValidRequestObject(item)) return false; } return true; } return isValidRequestObject(node); } catch (JsonProcessingException e) { return false; } } private static boolean isValidRequestObject(JsonNode node) { if (!node.isObject()) return false; JsonNode version node.get(jsonrpc); if (version null || !2.0.equals(version.asText())) return false; JsonNode method node.get(method); if (method null || method.asText().isEmpty()) return false; JsonNode id node.get(id); // id 可以缺失或为 null代表通知但如果存在必须是 string/number/null if (id ! null !id.isValueNode()) return false; JsonNode params node.get(params); if (params ! null !params.isArray() !params.isObject()) return false; return true; } }这段逻辑里isArray()判断把批量请求和单请求统一处理了。id必须是 value node 这一点容易被忽略——有些客户端会把id传成布尔值true这在 JSON-RPC 里不合法必须拦截。params只允许数组或对象传字符串或者数字进来都直接判非法。校验通过后再进入业务分发而不是在 Spring 的RequestBody阶段就绑定成强类型这样协议层的错误能被统一捕获并返回-32600。参数说明MAPPER这里用默认配置即可不需要开启FAIL_ON_UNKNOWN_PROPERTIES因为我们直接操作JsonNode不反序列化成 POJO。3. Spring Boot 集成 JSON-RPC路由分发、参数绑定与业务解耦3.1 用一个统一入口 Controller 接收所有 RPC 请求JSON-RPC 的接口地址通常只有一个比如/api/rpc所有方法都通过method字段区分而不是像 REST 那样一个资源一个 URL。这意味着你的 Controller 层只需要一个PostMapping方法签名固定为接收String body然后交给后端的 dispatcher 处理。这里有个设计选择是把 body 直接声明为String再手动解析还是声明为JsonNode我推荐String因为协议校验的失败信息需要区分“JSON 解析错误”和“协议错误”如果让 Spring 的反序列化提前失败你只能拿到 400 状态码拿不到结构化的-32700响应。手动接收字符串你就能完全控制异常路径。RestController RequestMapping(/api/rpc) public class RpcController { private final RpcDispatcher dispatcher; public RpcController(RpcDispatcher dispatcher) { this.dispatcher dispatcher; } PostMapping(produces application/json) public String handle(RequestBody String body) { return dispatcher.dispatch(body); } }这个 Controller 没有任何业务逻辑它只需要做两件事接收原始请求体返回 JSON 字符串响应。produces application/json是必要的有些客户端会严格检查响应头的 Content-Type如果返回text/plain可能解析失败。dispatcher是核心组件它负责解析、路由、执行业务方法并生成响应。这里的参数说明RequestBody String body在处理超大请求体时需要注意Spring Boot 默认限制 1MB如果 RPC 方法要传大对象需要在配置里调大spring.servlet.multipart.max-request-size或者干脆用RequestBody(required false)配合流读取但一般 RPC 场景不会传大文件1MB 足够。3.2 注册表设计如何把 method 字符串映射到 Spring Bean 方法JSON-RPC 的method是字符串比如user.getInfo你要能定位到某个类某个方法。最简单的方式是用一个MapString, FunctionJsonNode, Object把所有方法注册进去但这样写起来像面条代码而且没法利用 Spring 的参数注入。更好的方案是自定义注解 反射把方法标注为 RPC 方法然后扫描注册。我一般会定义一个RpcMethod(user.getInfo)注解放在 Service 的方法上启动时用一个ApplicationRunner扫描容器里所有带这个注解的方法把method映射到一个MethodInvoker对象这个对象持有目标 bean 和Method以及参数类型信息。Component public class RpcMethodRegistry { private final MapString, RpcMethodInvoker invokers new ConcurrentHashMap(); public RpcMethodRegistry(ApplicationContext context) { MapString, Object beans context.getBeansWithAnnotation(RpcService.class); for (Object bean : beans.values()) { for (Method method : bean.getClass().getMethods()) { RpcMethod annotation method.getAnnotation(RpcMethod.class); if (annotation ! null) { String name annotation.value(); invokers.put(name, new RpcMethodInvoker(bean, method)); } } } } public RpcMethodInvoker get(String methodName) { return invokers.get(methodName); } }这里用了getBeansWithAnnotation(RpcService.class)来圈定扫描范围避免把所有 Spring Bean 的方法都翻一遍。RpcMethodInvoker内部保存bean和Method调用时通过反射invoke并且把方法的参数类型暴露给上面的参数解析器。这个设计的优点是新增一个 RPC 方法只需要在已有的Service类里加一个带RpcMethod的方法不需要改任何路由代码。要注意的坑是getMethods()会拿到继承来的public方法如果父类也有RpcMethod注解会重复注册所以我一般会在注册时检查method.getDeclaringClass()是不是当前类或者用getDeclaredMethods()只拿本类的方法。3.3 参数解析位置参数与命名参数双模式支持JSON-RPC 的params可能是数组也可能是对象为了兼容不同客户端服务端最好同时支持两种模式。数组模式对应方法参数顺序对象模式对应参数名称。Java 方法在运行时默认拿不到参数名除非编译时加了-parameters参数或者用Param注解声明。我一般用Param(userId)这种显式注解避免依赖编译配置。参数解析的核心逻辑是先判断params是数组还是对象如果是数组按顺序逐个转换如果是对象按注解里的名称去params里取值。public Object[] resolveParameters(JsonNode params, RpcMethodInvoker invoker) { Method method invoker.getMethod(); Parameter[] parameters method.getParameters(); Object[] args new Object[parameters.length]; if (params null || params.isNull()) { for (int i 0; i parameters.length; i) { args[i] resolveDefaultValue(parameters[i].getType()); } return args; } if (params.isArray()) { for (int i 0; i parameters.length; i) { JsonNode value params.get(i); args[i] convertValue(value, parameters[i].getType()); } } else if (params.isObject()) { for (int i 0; i parameters.length; i) { RpcParam paramAnnotation parameters[i].getAnnotation(RpcParam.class); String name paramAnnotation ! null ? paramAnnotation.value() : parameters[i].getName(); JsonNode value params.get(name); args[i] convertValue(value, parameters[i].getType()); } } return args; }这段代码的一个注意点是如果params数组长度小于方法参数个数params.get(i)返回nullconvertValue要能处理null并给一个默认值如果长度大于参数个数多余的元素直接忽略。命名参数模式下如果客户端传了参数对象但没有对应方法参数名称的键params.get(name)也是null建议对基础类型做一次hasNonNull检查如果缺参直接抛出-32602参数错误而不是传null进去让业务方法空指针。3.4 异常拦截与统一响应把异常码映射到 HTTP 200JSON-RPC 协议基于 HTTP 时一个常见争议是业务失败到底应该返回 HTTP 500 还是 HTTP 200规范本身没有强制要求但绝大多数 JSON-RPC 实现倾向于返回 HTTP 200把错误信息放在响应体的error字段里因为这样才能让客户端统一处理传输层异常和业务异常。在 Spring Boot 里实现这个就需要捕获所有业务异常转换为RpcError对象最终封装成成功响应结构返回。我一般用一个ExceptionHandler处理所有异常但更可控的做法是在 dispatcher 层 catch 每一个请求的异常这样可以做到批量请求里某一个失败不影响其他元素。public RpcResponse invokeSingle(JsonNode request) { String id request.hasNonNull(id) ? request.get(id).asText() : null; try { String methodName request.get(method).asText(); RpcMethodInvoker invoker registry.get(methodName); if (invoker null) { return RpcResponse.error(-32601, Method not found, id); } Object[] args paramResolver.resolveParameters(request.get(params), invoker); Object result invoker.invoke(args); return RpcResponse.success(result, id); } catch (IllegalArgumentException e) { return RpcResponse.error(-32602, Invalid params: e.getMessage(), id); } catch (Exception e) { return RpcResponse.error(-32603, Internal error, id); } }这里的invokeSingle方法负责单条请求的完整生命周期。我故意把IllegalArgumentException单独处理因为参数解析失败时抛出的就是它映射到-32602比笼统返回内部错误更友好。注意id的处理如果请求是通知没有 id这个invokeSingle仍然会执行业务逻辑但返回的响应会被外层判断丢弃不写回 HTTP。还有一种情况是 JSON 解析失败这发生在进入invokeSingle之前应该在 dispatcher 的最外层捕获返回id: null的错误响应。4. 手写 Dispatcher 与完整请求链路从原始字符串到结构化响应4.1 用ObjectMapper把请求体拆成单条或批量模式Dispatcher 是整个 JSON-RPC Server 的心脏它要处理四类输入合法单请求、合法批量请求、可解析但协议非法的 JSON、完全不可解析的字符串。我习惯把处理流程拆成四步先尝试readTree如果失败返回-32700成功后再判断是数组还是对象数组则进入批量逻辑对象则进入单请求逻辑单请求如果校验失败返回-32600。这里要特别注意批量请求里即使某一个元素是非法对象其他合法元素仍然要执行不能因为一个坏了就整体返回错误。public String dispatch(String body) { JsonNode node; try { node objectMapper.readTree(body); } catch (JsonProcessingException e) { return buildErrorResponse(-32700, Parse error, null); } if (node.isArray()) { ListJsonNode requests new ArrayList(); for (JsonNode item : node) { if (!RpcProtocolValidator.isValidRequestObject(item)) { requests.add(createInvalidRequestNode()); } else { requests.add(item); } } ListString responses new ArrayList(); for (JsonNode request : requests) { RpcResponse result invokeSingle(request); if (request.hasNonNull(id) || request.isObject()) { // 通知请求不返回响应 if (request.hasNonNull(id)) { responses.add(objectMapper.writeValueAsString(result)); } } } return [ String.join(,, responses) ]; } else if (node.isObject()) { if (!RpcProtocolValidator.isValidRequestObject(node)) { return buildErrorResponse(-32600, Invalid Request, null); } RpcResponse result invokeSingle(node); if (node.hasNonNull(id)) { return objectMapper.writeValueAsString(result); } else { return ; } } else { return buildErrorResponse(-32600, Invalid Request, null); } }这里有一个细节批量请求中如果某个元素不是合法请求对象规范要求返回一个错误响应并且这个错误响应的id是null。我在代码里用createInvalidRequestNode构造一个假的请求节点让它走进invokeSingle但实际上是伪造了一个id: null的非法请求这样invokeSingle会返回-32600错误。另一个细节是批量请求的响应数组中通知元素不产生响应所以responses列表的长度可能小于请求数组长度。如果整个批量请求所有元素都是通知最终返回的是空数组[]而不是空字符串这一点要和单通知的“返回空字符串”区分开客户端要注意这种差异。4.2 把业务方法返回值规范成result或errorRpcResponse是内部封装它持有jsonrpc、result、error、id四个字段。但写回客户端时不能直接序列化因为result和error是互斥的不能同时出现。我一般用两个静态工厂方法success(Object result, String id)和error(int code, String message, String id)。注意id的类型要保持原样——如果客户端传来的是数字1你返回的id也应该是数字1而不是字符串1否则客户端用严格 JSON-RPC 客户端库校验时会报 id 类型不匹配。所以RpcResponse里的id字段类型应该用JsonNode存原始节点序列化时原样输出。public class RpcResponse { private final String jsonrpc 2.0; private JsonNode result; private RpcError error; private JsonNode id; public static RpcResponse success(JsonNode result, JsonNode id) { RpcResponse response new RpcResponse(); response.result result; response.id id; return response; } public static RpcResponse error(int code, String message, JsonNode id) { RpcResponse response new RpcResponse(); response.error new RpcError(code, message); response.id id; return response; } }id用JsonNode存储的好处是ObjectMapper序列化时会保留原始 JSON 类型不会出现数字变字符串的问题。如果id是null也要区分是通知导致的 null 还是错误响应里的 null——错误响应里的id为null是合法的表示这个错误不是在某个具体请求上发生的。RpcError内部包含code、message还可以加一个可选的data字段但我不建议默认把异常堆栈放进去因为堆栈可能包含内部类名和行号泄露实现细节也容易被安全扫描器盯上。如果想调试可以只在message里带异常类名或者通过Profile(dev)控制在开发环境输出堆栈。4.3 事务与线程模型批量请求是否该共用一个事务在真实的业务场景里JSON-RPC 方法可能涉及数据库操作。如果客户端发了一个批量请求里面有两个方法第一个方法插入数据第二个方法依赖第一个方法的结果那必须保证执行顺序。但如果两个方法互不依赖是独立的写操作服务端要不要把整个批量包在一个事务里我建议不要。JSON-RPC 规范里批量请求是“一组调用”没有事务语义。如果真的需要事务应该让客户端显式调用一个transaction.begin、transaction.commit这样的方法服务端用TransactionTemplate包住。另外如果批量请求里某个方法抛了异常不能回滚其他已经成功的方法因为每个方法都是独立调用有自己的id。我曾经见过一个团队在 dispatcher 里加了全局事务拦截器结果批量请求里一个方法失败把其他成功方法的数据库操作也回滚了客户端收到混合错误响应排查了很久。4.4 响应顺序与迟到响应用CompletableFuture保持原始请求顺序如果服务端用线程池并发处理批量请求响应顺序会乱。解决方式有两种一种是复用单线程串行处理简单但性能差另一种是先并发执行再按请求顺序收集结果。我一般用第二种具体做法是把请求列表和CompletableFuture关联每个请求提交到线程池返回 future最后遍历原始请求列表逐个future.get()收集结果这样响应数组的顺序就和请求数组一致了。注意future.get()要捕获ExecutionException然后把里面的真实异常转成-32603错误响应。线程池的拒绝策略也要想清楚如果请求量瞬间超出线程池队列应该返回-32603表示服务端过载而不是抛RejectedExecutionException让整个 HTTP 请求变 500。ExecutorService executor Executors.newFixedThreadPool(8); ListCompletableFutureJsonNode futures new ArrayList(); for (JsonNode request : requests) { CompletableFutureJsonNode future CompletableFuture.supplyAsync( () - handleSingle(request), executor); futures.add(future); } ListString orderedResponses new ArrayList(); for (int i 0; i requests.size(); i) { if (requests.get(i).hasNonNull(id)) { try { JsonNode resp futures.get(i).get(5, TimeUnit.SECONDS); orderedResponses.add(objectMapper.writeValueAsString(resp)); } catch (Exception e) { orderedResponses.add(buildErrorResponse(-32603, Async execution error, null)); } } }这段实现的关键是超时时间不能设太长否则一个方法卡死会拖住整个请求。5 秒是我的默认值具体业务可以调。handleSingle方法内部就是invokeSingle的封装返回JsonNode这样CompletableFuture的泛型是JsonNode而不是RpcResponse统一在最后用ObjectMapper序列化。参数说明supplyAsync是核心它保证每个请求在独立线程里跑但业务方法里的Transactional要想生效必须保证代理调用链完整如果方法被反射调用事务注解不生效这是个隐藏陷阱下一节细说。5. 避坑指南JSON-RPC Server 落地时的六个高频翻车现场5.1 反射调用导致Transactional事务失效现象RPC 方法上标注了Transactional前端调用后数据库确实写了但如果第二个操作抛异常第一个操作不会回滚。原因Spring 事务是通过 AOP 代理实现的代理只对“从外部通过代理对象调用”的方法生效。而 RPC dispatcher 通过Method.invoke(bean, args)直接调用目标对象的方法根本没走代理事务拦截器自然不工作。解决不要直接调用原始 bean而是调用从ApplicationContext.getBean(Class)拿到的代理对象。在注册RpcMethodInvoker时用beanFactory.getBean(bean.getClass())获取代理或者更简单——在注册表里保存ObjectProviderT每次调用时getObject()。从那以后我每次写反射调用 Spring 方法前都会先确认 target 是不是代理对象而不是原始实例。5.2 批量请求中其中一个请求是非法 JSON导致整个批量请求 500现象客户端发送[{jsonrpc:2.0,method:a,id:1}, not a json object]服务端直接返回 HTTP 500或者返回一个空数组。原因dispatcher 里只检查了最外层的readTree成功没有对批量请求的每一个元素重新做协议校验导致第二个元素被当成普通对象传入invokeSingle里面request.get(method)抛 NPE。解决在批量逻辑里对每个元素先调用RpcProtocolValidator.isValidRequestObject(item)不合法就构造一个-32600错误响应占位而不是把它当成正常请求。注意校验函数要处理item不是对象的情况比如是字符串或数字isObject()要放在最前面判断。5.3id类型被序列化成字符串现象客户端传{id: 1}数字服务端响应变成了{id: 1}一些严格客户端直接报错。原因RpcResponse里的id字段类型是String序列化时自然是字符串。或者用了JSONObjectget(id)拿到的可能被转成字符串。解决RpcResponse的id字段声明为JsonNode从原始请求里直接request.get(id)拿出来序列化时原样输出。如果用的是旧的HashMap实现也要注意存进去之前不要手动toString()。5.4 方法名带点号Spring MVC 把它当路径切割现象客户端调用user.getInfo但服务端方法注册表里有这个 key却始终提示-32601 Method not found。原因注册表扫描时用了bean.getClass().getMethods()如果目标方法名是getInfo但注解里写的是user.getInfo这个没问题。真正的问题可能是你用RequestMapping的嵌套路径形式把方法名写成了user/getInfo或者注册表的 key 拼写错误多了一个空格。解决注册时对annotation.value()做trim()并且统一用点号分隔不要用斜杠。另外注意 Spring 的RpcMethod注解如果和GetMapping混用Servlet 路由会优先匹配 HTTP 路径user.getInfo这个 method 名和 URL 无关不要被路径干扰。5.5 通知请求返回了响应体现象客户端发了一个没有id的请求服务端仍然写回了{jsonrpc:2.0,result:...}导致客户端解析阻塞。原因dispatcher 里没有判断hasNonNull(id)直接把invokeSingle的结果写回。规范明确通知不能有响应。解决在单请求和批量请求两处都判断hasNonNull(id)没有 id 就跳过响应。注意id: null和没有 id 字段一样算通知hasNonNull正好能覆盖这两种情况。批量请求里如果全是通知响应是空数组[]这个也要让客户端知道否则客户端以为服务端没处理。5.6 自定义异常无法被客户端区分现象业务里抛了自定义异常比如BusinessException(余额不足)客户端收到的error.message却是Internal error无法针对业务错误做分支处理。原因dispatcher 的catch (Exception e)把所有异常都吞成-32603自定义异常的信息没有传到 response 里。解决先 catch 业务异常再 catch 通用异常。我习惯定义RpcBusinessException携带code和data在invokeSingle里单独 catch把code放进-32000到-32099的自定义区间message就用异常里的信息。这样客户端就能区分“余额不足”和“系统内部错误”。6. 验证与进阶搭一个命令行客户端跑通单请求、批量与错误码6.1 用curl做第一轮协议验证写完服务端后先用curl验证基本链路不要急着写客户端。启动 Spring Boot 应用后执行下面的命令检查返回结构是否符合预期。curl -X POST http://localhost:8080/api/rpc \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:ping,params:{},id:1}如果一切正常应该返回{jsonrpc:2.0,result:pong,id:1}。这里要特别检查id是不是数字1而不是字符串1。接着验证错误码curl -X POST http://localhost:8080/api/rpc \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:nonexistent,params:[],id:2}期望响应是{jsonrpc:2.0,error:{code:-32601,message:Method not found},id:2}。如果返回的是-32603说明你的 dispatcher 把方法不存在的异常吞成了通用异常需要检查注册表逻辑。6.2 用 Python 写一个高覆盖的测试脚本Python 的requests库足以验证所有协议分支。我习惯把测试用例写成一个脚本来回跑每次改动后执行一遍确保回归。import requests import json BASE_URL http://localhost:8080/api/rpc def rpc_call(method, paramsNone, rpc_id1, request_idTrue): payload {jsonrpc: 2.0, method: method} if params is not None: payload[params] params if request_id: payload[id] rpc_id resp requests.post(BASE_URL, jsonpayload) return resp.status_code, resp.json() # 正常调用 status, body rpc_call(ping, {}) assert status 200 assert body[result] pong assert body[id] 1 # 参数无效 status, body rpc_call(user.getInfo, {userId: -1}) assert body[error][code] -32602 # 方法不存在 status, body rpc_call(no.such.method, {}) assert body[error][code] -32601 # 通知请求无响应 status, body rpc_call(ping, {}, request_idFalse) assert body # 通知不返回任何内容 # 批量请求 batch [ {jsonrpc: 2.0, method: ping, params: {}, id: 1}, {jsonrpc: 2.0, method: no.such.method, params: {}, id: 2}, invalid json object, ] resp requests.post(BASE_URL, jsonbatch) result resp.json() assert result[0][result] pong assert result[1][error][code] -32601 assert result[2][error][code] -32600这个脚本里最值得注意的断言是“通知请求”。我实际开发中就翻过车——服务端忘了判空返回了{jsonrpc:2.0,result:pong,id:null}而requests.post会把这个响应体返回导致断言失败。另一个注意点批量请求里的非法元素会得到一个id: null的错误响应但如果服务端实现有 bug可能返回id: 2因为第二个请求的 id 被复用给了错误响应这是不对的每个错误响应必须用自己那条请求的 id 或 null。6.3 压测视角单请求和批量请求的吞吐差异如果你把大量独立调用拼接成一个批量请求发送服务端如果用线程池并发处理吞吐会明显上升。我的习惯是先测单请求的 QPS再测同样数量请求拼成批量后的总耗时。这里有个容易误用的点批量请求不是越大越好如果一次塞 1000 个方法调用服务端的线程池会瞬间被打满后续请求排队反而比串行更慢。需要根据方法平均耗时设置批量大小上限。比如每个方法 10ms线程池 8 个线程批量 100 个的总耗时理论上是100/8 * 10 125ms但如果是串行则是1000ms这个收益很可观但批量 1000 个时理论耗时变成1250ms已经超过了单发 100 次的网络开销。所以批量大小在 50 到 200 之间通常比较合理。这个资源里的核心实现就够你直接使用了剩下的可以根据你的业务方法耗时去调线程池参数和批量上限。希望这份笔记能帮你把 JSON-RPC 在 Spring Boot 里跑得顺遇到问题时也能快速定位。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑