Spring Boot集成JSON-RPC:统一端点远程调用服务端搭建
简介一套基于Spring Boot的JSON-RPC服务端实现面向需要快速构建轻量级远程调用接口的Java后端开发人员适用于微服务内部服务间通信、前后端数据交换等对请求格式有明确约定的场景。工程演示了标准JSON-RPC 2.0协议的集成方式客户端以application/json格式发送包含id、method、params的请求服务端解析后返回对应result示例中multiplier方法接收参数[5,8]并正确返回40方便读者直接验证调用链路请求与响应字段结构简洁便于二次扩展。压缩包共26个文件整体约55KB以5个Java源码和5个class编译文件为主另有properties、xml、prefs等配置用于工程与构建参数配合jar依赖与Maven Wrapper可快速导入IDE运行。已有297人学习下载。通过该示例可掌握Spring Boot下自定义JSON-RPC处理、参数解析与响应封装的基本思路同时借鉴其目录分层和Maven配置方便在自有项目中落地或改造成更通用的远程调用模块。1. 项目概述为什么用 Spring Boot 搭 JSON-RPC 服务端我最早把 JSON-RPC 塞进 Spring Boot纯粹是被现场对接逼出来的。对方系统只给了一个内网地址和一份接口文档文档里全是{jsonrpc:2.0,method:xxx,params:[...],id:1}这种请求格式要求所有接口统一走这一个地址不能在地址上区分业务。用 REST 的话对方得为每个 URL 做配置和权限折腾一圈下来最后还是选了 JSON-RPC 一个端点打天下。JSON-RPC 是一种非常轻量的远程调用协议基于 JSON 格式传输和 REST 最大的区别在于REST 用 URL 和 HTTP 方法来表达资源 动作JSON-RPC 则把方法名 参数 调用编号统统放进请求体里服务地址永远只有一个。Spring Boot 做 Web 服务端的生态非常成熟接 JSON-RPC 无非就是找一个合适的库或者自己写几十行代码把协议解析出来。这篇文章适合这几类人一是被外部系统协议限制必须在 Spring Boot 项目里暴露 JSON-RPC 接口的二是想在微服务内部做轻量级远程调用不想上重型 RPC 框架的三是对 REST 接口管理感到繁琐想了解另一种接口组织方式的。整个搭建过程不复杂核心就三件事搞清楚 JSON-RPC 2.0 的报文格式选一个顺手的实现方案把 Spring Boot 的 Bean 和协议层打通。2. 核心思路拆解先看懂协议再选实现方案2.1 JSON-RPC 2.0 协议要点速览JSON-RPC 2.0 的请求体长这样{ jsonrpc: 2.0, method: getUserById, params: [1001], id: 7 }四个字段的含义我拆开说。jsonrpc固定写2.0用来告诉服务端协议版本method是要调用的方法名params有两种写法数组表示按位置传参对象表示按名称传参id是客户端生成的调用编号服务端返回的响应里必须带上同一个id这样客户端才能把请求和响应一一对应起来。如果客户端不想关心响应可以把id设为null这种请求叫做通知Notification服务端处理完不会返回任何内容。响应体分两种。成功时返回result字段失败时返回error字段。error是一个对象包含code、message、data三个属性其中data是可选的。规范里几个固定的错误码需要记一下错误码含义-32700解析错误服务端收到的不是有效 JSON-32600无效请求请求体结构不符合规范-32601方法不存在-32602参数无效参数数量或类型不匹配-32603内部错误服务端运行时异常-32000 到 -32099服务端自定义错误码有个细节很多人刚接触时会忽略JSON-RPC 支持批量请求。客户端可以发送一个数组数组里每个元素都是一个独立的请求对象服务端处理完成后也要按数组顺序返回一个响应数组。这个特性好用但也容易被滥用后面我会专门讲怎么防。2.2 jsonrpc4j 和自研路由怎么选Spring Boot 里接 JSON-RPC主流方案有两个。第一个是直接用开源库 jsonrpc4j它和历史悠久的 JsonRpcServer 是一脉相承的后来才做的 Spring Boot 支持。第二个是自研一个 Controller 做协议解析原理不复杂代码量大概一百多行。我一开始倾向于自研因为可控性强毕竟只是一个method分发。但用了 jsonrpc4j 之后发现它比我预想的完整得多方法自动导出、参数绑定、异常映射、批量请求这些都处理好了而且是基于 Spring MVC 的HttpRequestHandler机制实现性能上就是一次普通请求的损耗。唯一要注意的是它的文档不算丰富很多配置项得看源码或者实测这也是我写这篇文章的原因之一。自研方案适用于什么情况呢比如项目中已经有一个统一网关只需要暴露一个 JSON-RPC 端点而且方法数量很少那么手写一个 Controller 完全够用。但是只要方法一多、异常处理一复杂手写就容易漏所以我最终还是推荐 jsonrpc4j。下面的实操部分我用 jsonrpc4j 为主线来讲代码示例都以它能跑通为准。3. 实操过程从零搭建 Spring Boot JSON-RPC 服务端3.1 项目初始化与依赖引入先建一个标准的 Spring Boot 项目我用的版本是 2.7.xJava 8 以上都行。引入 JSON-RPC 依赖前先去仓库确认最新版本目前比较稳定的是 1.6.0。dependency groupIdcom.github.briandilley.jsonrpc4j/groupId artifactIdjsonrpc4j/artifactId version1.6.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencyjsonrpc4j 的传递依赖里带了 Jackson所以不需要额外引入 Jackson。如果你的项目里已经用了 Spring Boot 的 web starterspring-boot-starter-web里自带的 Jackson 版本和 jsonrpc4j 有可能会冲突遇到的话可以在 pom 里排除 jsonrpc4j 自带的 jackson 依赖这个问题在后面的常见问题部分会细说。3.2 服务接口与实现类编写先定义一个业务服务接口然后写实现类。这里给一个用户查询的例子public interface UserService { User getUserById(Integer id); User getUserByNameAndPage(String name, int page, int size); }接口不高明但足够演示位置参数和命名参数两种传法。接下来是重点实现类上要加两个注解一个让 Spring 管理 Bean一个告诉 jsonrpc4j 这个类要作为 JSON-RPC 服务导出。Service JsonRpcService(/userService) public class UserServiceImpl implements UserService { Override public User getUserById(Integer id) { // 实际业务里这里会查数据库 return new User(id, 张三, 28); } Override public User getUserByNameAndPage(String name, int page, int size) { return new User(1, name - page - size, 0); } }JsonRpcService(/userService)里的路径是相对路径最终服务地址的实际前缀由配置决定默认是/rpc所以完整调用地址就是http://localhost:8080/rpc/userService。这个映射关系我一开始也搞错过以为是注解里的路径直接生效后来抓包才发现前面多了一个/rpc这个前缀在配置类里是可以改的。3.3 自动导出配置配置类是整个集成过程里最核心的一步。jsonrpc4j 提供了一个叫AutoJsonRpcServiceImplExporter的类它会扫描 Spring 容器里所有带JsonRpcService注解的 Bean自动为每个 Bean 生成一个 HTTP 处理器并注册到 Spring MVC 路由表。Configuration public class JsonRpcConfig { Bean public AutoJsonRpcServiceImplExporter autoJsonRpcServiceImplExporter() { AutoJsonRpcServiceImplExporter exporter new AutoJsonRpcServiceImplExporter(); exporter.setContentType(application/json;charsetUTF-8); exporter.setAllowExtraParams(false); exporter.setAllowLessParams(false); return exporter; } }几个参数的说明setAllowExtraParams(false)请求里的 params 方法签名多出了参数就直接报参数错误避免某些客户端顺手传了一堆没用的字段导致排查困难。setAllowLessParams(false)要求方法签名里的每个参数都必须出现在请求里缺一个就报错。setContentType(application/json;charsetUTF-8)这个很重要不设置的话中文可能乱码或者返回的 Content-Type 不带 charset。配置类写完后直接启动项目如果控制台没有报错服务端就已经可以接 JSON-RPC 请求了。你可以从启动日志里看到/rpc/userService这条路由被注册到了 Spring MVC。3.4 启动与联调测试服务启动后用 curl 做一次完整的调用curl -X POST \ http://localhost:8080/rpc/userService \ -H Content-Type: application/json;charsetUTF-8 \ -d {jsonrpc:2.0,method:getUserById,params:[1001],id:7}响应{ jsonrpc: 2.0, result: { id: 1001, name: 张三, age: 28 }, id: 7 }再看一下方法不存在时返回什么{ jsonrpc: 2.0, error: { code: -32601, message: Method not found }, id: 7 }注意观察id完美地回传了。这个id是排查问题的关键如果客户端收到的响应里的id对不上那就说明请求和响应错位了基本就是并发场景下的处理顺序问题。如果用 Java 客户端调用直接用 JDK 自带的HttpClient就行不需要额外依赖String body {\jsonrpc\:\2.0\,\method\:\getUserByNameAndPage\, \params\:{\name\:\李四\,\page\:1,\size\:10},\id\:1}; HttpRequest request HttpRequest.newBuilder() .uri(URI.create(http://localhost:8080/rpc/userService)) .header(Content-Type, application/json;charsetUTF-8) .POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpResponseString response HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString());这里 params 传的是对象jsonrpc4j 会根据方法参数名去匹配所以要求编译时开启了-parameters参数否则方法参数名得不到匹配会失败。如果不想依赖参数名就用数组传参数组顺序和 Java 方法签名顺序一致即可。这个坑很多人踩过我后面单独列一条。4. 进阶配置异常处理、安全加固与性能调优4.1 统一异常处理与错误码映射业务代码里抛异常是很常见的事如果不做处理jsonrpc4j 会返回一个通用的内部错误码 -32603但原因往往是天书。我想让调用方拿到更有意义的错误信息就用到了JsonRpcError注解。假设我有一个BusinessException想让它映射到自定义错误码 -32001JsonRpcError(exception BusinessException.class, code -32001) public class BusinessException extends RuntimeException { public BusinessException(String message) { super(message); } }然后在服务实现类的方法上放开这个异常Override public User getUserById(Integer id) { if (id null || id 0) { throw new BusinessException(用户ID必须大于0); } return new User(id, 张三, 28); }调用方收到的是{ jsonrpc: 2.0, error: { code: -32001, message: 用户ID必须大于0 }, id: 7 }这样排查问题就直观多了。多个异常可以用逗号分隔配置多个JsonRpcError注意不要配置继承关系的异常否则匹配顺序不是很好控制。4.2 安全加固的几点建议JSON-RPC 没有内置身份认证所以在 Spring Boot 里暴露服务时安全是必须考虑的。我的做法是加一个HandlerInterceptor或者Filter校验请求头里的 token。token 校验逻辑放在业务之前不合法直接返回 401响应体按 JSON-RPC 错误格式来返回。Component public class JsonRpcTokenFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token request.getHeader(X-Auth-Token); if (!expected-token.equals(token)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write( {\jsonrpc\:\2.0\,\error\:{\code\:-32000,\message\:\Unauthorized\},\id\:null} ); return; } chain.doFilter(request, response); } }除了鉴权还要考虑请求体的合法性问题。所有能接收 JSON 的入口都值得被重视不要让攻击者传一个巨大的params数组来消耗服务端内存。可以在过滤器里判断Content-Length超过设定阈值直接拒绝。这个方法虽粗暴但够用。我还在过滤器里干过一件事如果请求体第一个非空白字符是[直接拦掉。这就是我在 2.1 里提到的批量请求隐患。批量请求在业务上要慎用因为我见过有的客户端循环往数组里塞几千个请求服务端瞬间打满线程池。业务上真需要批量能力时由服务端专门提供一个批量业务方法永远更可控。4.3 性能与请求体限制JSON-RPC 在 Spring Boot 里本质上走的还是内嵌 Tomcat 的线程池所以常规调优方案依然适用。但有一个容易被忽略的点服务端缓存响应体。如果某个方法返回的数据量比较大且变化不频繁可以在服务层做缓存相比每次靠 JSON 序列化去扛效果明显得多。还有一点是调大内嵌 Tomcat 的线程池上限。生产环境的默认线程数对 JSON-RPC 这类同步阻塞型接口来说可能不够尤其是内部系统大量短请求进来的时候可以在application.yml里调整server: tomcat: threads: max: 400 min-spare: 50日志方面建议在开发阶段打印完整的请求体和响应体。jsonrpc4j 支持通过LoggingTeeInputStream和LoggingTeeOutputStream来读取原始数据但配置起来比较繁琐。我一般直接在 Filter 里实现用一个重写过的HttpServletRequestWrapper把 body 缓冲一份打印完再放回去让服务方法还能正常读取。生产环境记日志一定要打脱敏邮箱、手机号这些字段别打全量。5. 常见问题与排查技巧实录5.1 请求路径 404 / 服务不生效这是最频繁出现的问题。服务起来了但一调用返回 404。排查步骤就三步第一确认访问路径里带上了/rpc前缀即http://localhost:8080/rpc/userService如果配置过server.servlet.context-path记得前缀要在它后面。第二检查实现类上有没有同时加Service和JsonRpcService缺一个都不会被自动导出。第三看启动日志里有没有Mapped {[/rpc/userService]}这条记录如果没有多半是配置类的AutoJsonRpcServiceImplExporter没被扫描到。5.2 params 传参顺序与类型不匹配jsonrpc4j 对参数类型比较敏感。我用数组传参时曾经因为 Integer 和 int 的差异导致服务端一直报 -32602。后来把接口方法的参数类型改成包装类型Integer问题就解决了。这个和 Jackson 反序列化时的处理方式有关实际碰到莫名参数错误时优先做这个改动。用命名参数传参时要确认项目编译时开启了-parameters参数否则方法参数名会变成arg0、arg1。在 Maven 的pom.xml里加上plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration parameterstrue/parameters /configuration /plugin5.3 中文乱码与序列化问题中文乱码一般有两个原因。第一个是服务端返回时Content-Type里没带charsetUTF-8在AutoJsonRpcServiceImplExporter里设置即可。第二个是客户端发请求时没有在 Header 里指定charsetUTF-8请求体里的中文在传输层就坏了这个只能从客户端解决。另一个常见问题是日期格式。启动项目后如果发现LocalDateTime字段在响应里序列化格式不对需要在 ObjectMapper 上做全局配置。jsonrpc4j 默认使用 Spring Boot 的JacksonAutoConfiguration自动创建的那个 ObjectMapper所以只要在 Spring Boot 里正常配置即可Configuration public class JacksonConfig { Bean Primary public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); return mapper; } }5.4 调度线程池被打满的问题有一次在压测时发现 JSON-RPC 服务端整体响应变慢查看线程 dump大量线程卡在序列化和网络写。原因是我在一个接口方法里用了同步的数据库查询但没做超时控制底层连接池一旦被占满所有请求就全部排队。后来我的处理方式是把耗时操作拆出去用线程池执行然后回调同时给数据库连接池加上max-lifetime和connection-timeout。如果你用的是微服务架构还能考虑给 JSON-RPC 服务单独开一个实例避免和其他 REST 接口抢线程资源。排查线程问题有个小技巧在application.yml里把 Tomcat 的线程池参数开到足够大再开启 Spring Boot 的 actuator 端点通过/actuator/metrics/tomcat.threads.busy观察繁忙线程数能直观看到服务端的健康度。没有引入 actuator 也别急用 jconsole 连上生产进程看平台线程数也行。6. 结语一些实践体会这套 JSON-RPC 服务端搭起来之后我在几个内部项目里验证过稳定性和可维护性都比想象中好。最大的收获是它让接口管理变得非常集中一个端点、一套协议、一份文档就能覆盖所有方法调用。如果要说个人心得那就是不要为了用 JSON-RPC 而用 JSON-RPC。REST 在资源建模上有天然优势适合面向外部开放 APIJSON-RPC 更适合内部系统之间、或者异构语言之间做方法调用式的协作。如果项目里已经有了成熟的注册中心和 RPC 框架也没有外部协议约束完全没必要多引入一层 JSON-RPC。另外一个小建议服务暴露出去之前先写一份精简的协议说明文档把method命名规范、参数类型、返回格式、错误码含义都写清楚发给接入方一份能减少大量无意义的来回沟通。毕竟 JSON-RPC 好处是端点集中代价是方法多了以后文档跟不上就会变成暗号大全。这份文档我一般是和接口定义放在一起维护接口改了文档顺手就更新了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →