ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FeignClient 传 body 的底层逻辑与最佳实践:彻底解决 Required request body is missing 等报错

FeignClient 传 body 的底层逻辑与最佳实践:彻底解决 Required request body is missing 等报错 最近又在好几个技术群里看到有人问“FeignClient 参数传 body 应该怎么写”一问就是报错截图不是required request body is missing就是failed to deserialize the json body into the target type还有人干脆收到一整个 HTML 错误页。说实话这个问题看起来简单实际上坑挺多的尤其是刚接触 Spring Cloud 的同事很容易在注解、参数类型、请求格式上绕晕。这篇就把 FeignClient 传 body 这件事彻底讲清楚。我会从 Feign 处理请求体的底层逻辑讲起再给几种实际可用的写法然后带一个完整的前后端调用示例最后把常见报错的排查思路整理出来。适合正在做微服务接口对接、或者被 Feign 各种参数问题折磨过的 Java 开发朋友看完基本能直接抄作业。1. 先理清 Feign 传 body 的底层逻辑1.1 一个接口为什么只能有一个 RequestBody先说很多人报错的根源Spring MVC 和 Feign 对方法参数的处理方式其实和普通 Java 方法完全不一样。普通方法里你爱写几个参数写几个但 HTTP 请求只有一个 body。Spring 解析 Controller 或者 Feign Client 方法的时候遇到RequestBody注解的参数就会把请求体反序列化绑到这个参数上。如果你在同一个方法里写了两个RequestBodySpring 启动时就会直接报错提示 body 只能被绑定一次。就算你只写了一个但调用方那边没有把参数序列化进 body服务端解析时拿不到内容就会出现required request body is missing。很多刚接触 Feign 的同事以为“我传了个对象过去Spring 会自动帮我处理”但实际上注解没写对、Content-Type 不对、参数类型不匹配都会让 body 传不过去。1.2 Feign 是怎么把对象变成请求体的Feign 本身是一个声明式 HTTP 客户端核心逻辑就是根据接口方法上的注解把方法调用转换成一个 HTTP 请求。在 Spring Cloud OpenFeign 里默认使用的编码器是SpringEncoder。它会检查方法参数上有没有RequestBody如果有就会用 Spring 容器里的HttpMessageConverter把参数对象序列化成 JSON写入请求体。这里有个容易被忽略的点Feign 对RequestBody的处理依赖 Content-Type。正常走默认配置时SpringEncoder会自动设置Content-Type: application/json服务端RequestBody也按 JSON 解析两边就通了。但如果有人手动塞了 Header或者自定义了 Encoder把 Content-Type 改成了别的服务端就可能解析失败或者收到空 body。另外还有一点Feign 方法参数上如果没有加任何注解比如直接写void create(User user)那SpringEncoder不会把它当 body 处理而是把它当作 query 参数拼到 URL 上。这也是很多人明明传了对象、服务端却收不到 body 的原因之一。1.3 不是所有请求都能带 bodyGET 的边界HTTP 协议本身没有禁止 GET 请求带 body但实际用起来非常尴尬很多框架、网关、浏览器实现默认不会读取 GET 的 body或者干脆忽略掉。Spring MVC 虽然某些版本允许 GET RequestBody但 Feign 在生成 GET 请求时默认不会把请求体写入配置复杂不说还会有各种兼容问题。所以实践中建议只要是需要传 body 的接口一律用 POST、PUT、PATCH别去挑战 GET 传 body。这不是 Feign 的锅是 HTTP 语义和中间件生态决定的。真要传少量参数就用RequestParam拼 query要传结构化数据就老老实实 POST RequestBody。2. 传 body 的几种主流写法2.1 最推荐RequestBody DTO 对象先给最常见的写法。比如服务端有个创建用户的接口接收 JSON bodyFeign Client 这边可以直接定义一个 DTO然后用RequestBody标注FeignClient(name user-service, url ${user.service.url}) public interface UserClient { PostMapping(/user/create) ResultUser createUser(RequestBody UserCreateDTO dto); }public class UserCreateDTO { private String name; private Integer age; private String email; // 省略 getter / setter }关键是 DTO 的字段名要和服务端接收的 JSON 字段名保持一致。比如服务端 JSON 用的name你 DTO 里写userName那序列化出来的 JSON 就是{userName:xx}服务端按name取就是null。这种方式的好处是类型安全、可读性好、方便加校验也是我平时用得最多的。字段变多了直接在 DTO 上加属性就行不会像 Map 那样越写越乱。2.2 图省事直接传 Map能行但别长期用有的同事喜欢这么写PostMapping(/user/create) ResultUser createUser(RequestBody MapString, Object params);调用的时候往里塞几个 key确实灵活。比如字段是动态的、接口参数不固定的时候Map 可以少建几个类。但我不建议把它作为默认方案原因有三个第一Map 丢掉了类型信息。value 是 Integer 还是 StringJackson 序列化时可能会猜错比如数字被序列化成字符串服务端强转就报错。第二Map 没法做参数校验和文档化。别人看接口签名只知道你传了个 Map不知道具体要哪些字段维护成本很高。第三Map 里如果塞了null默认情况下 Jackson 也会把null序列化进去服务端如果没做容错可能触发 NPE 或者反序列化异常。所以 Map 只适合快速验证、临时调试、或者对接非常不规范的第三方接口。正式业务还是建议建 DTO。2.3 传 JSON 字符串的特殊情况和坑还有一种写法是直接传 JSON 字符串PostMapping(/user/create) ResultUser createUser(RequestBody String jsonBody);这里的坑比想象中大。RequestBody String在 Spring MVC 里是直接用StringHttpMessageConverter读取原始请求体也就是说调用方传入的字符串会被“原样发送”。但在 Feign 的SpringEncoder里事情就没那么简单了它拿到参数是 String 之后还是会走对象序列化的逻辑把字符串再当作 JSON 序列化一次。结果就是你本来想发的是{name:zhang,age:18}实际发出去却变成了{\name\:\zhang\,\age\:18}服务端拿到手的是一个带转义的 JSON 字符串而不是真正的 JSON 对象解析自然就挂了。那什么时候适合用RequestBody String只有当你配置了自定义 Encoder让 Feign 直接把 String 写入 body 的时候才建议这么用。后面 2.4 会说到。2.4 需要原生 body 时怎么自定义 Encoder有些场景是真的需要发送原始字符串 body比如对接某些硬件接口、第三方平台它们的签名逻辑是直接把参数拼成 JSON 字符串做加密body 不是标准 DTO 能表达的。这时候可以自定义一个简单的 Encoderpublic class RawStringEncoder implements Encoder { Override public void encode(Object object, Type bodyType, RequestTemplate template) { if (object instanceof String) { template.body((String) object, StandardCharsets.UTF_8); } else { throw new EncodeException(RawStringEncoder only supports String body); } } }然后在 Feign 配置类里把 Encoder 换掉Configuration public class FeignConfig { Bean public Encoder feignEncoder() { return new RawStringEncoder(); } }这样接口方法就能放心写PostMapping(/third-party/api) String callThirdParty(RequestBody String rawJsonBody);但要注意换了 Encoder 之后这个 Feign Client 下的所有接口都会走新的序列化逻辑如果其它接口还在用 DTO 对象就会报错。所以一般建议单独建一个 Feign Client 接口给特殊对接用别和普通业务接口混在一起。3. 一个完整的 Feign 调用究竟长什么样3.1 服务提供方的 Controller 怎么接很多问题其实是服务端和客户端两边约定不一致导致的。服务端先定义一个标准的 POST 接口RestController RequestMapping(/user) public class UserController { PostMapping(/create) public ResultUser createUser(RequestBody UserCreateDTO dto) { User user userService.create(dto); return Result.ok(user); } }注意RequestBody注解一定要写在参数前面这样 Spring 才会从请求体里反序列化。如果服务端这里忘了加RequestBody那 Spring 会尝试把dto当作 form 表单参数去绑定客户端发来的 JSON body 就可能解析失败或者全是 null。3.2 调用方 Feign Client 怎么定义再看调用方。定义一个 Feign Client 接口方法签名尽量和服务端保持对称FeignClient(name userService, url ${user.service.url}, configuration FeignConfig.class) public interface UserClient { PostMapping(/user/create) ResultUser createUser(RequestBody UserCreateDTO dto); }调用的时候注入接口直接当普通 Service 用Service public class UserServiceImpl { Resource private UserClient userClient; public void register(UserCreateDTO dto) { ResultUser result userClient.createUser(dto); if (result.getCode() ! 0) { throw new BizException(result.getMessage()); } } }从客户端到服务端整个链路就是调用方方法参数被 Feign 序列化成 JSON bodyPOST 到服务端服务端 Spring MVC 把 body 反序列化成UserCreateDTO然后执行业务逻辑。3.3 实操中的请求体设计建议传 body 不只是写个注解那么简单请求体设计直接影响后期维护体验。我的习惯是每个接口单独建 DTO不要直接拿数据库实体类当请求体避免暴露多余字段也防止循环引用导致序列化死循环。DTO 字段根据接口语义命名比如创建用户用UserCreateDTO更新用UserUpdateDTO字段尽量精简。有日期字段时在 DTO 上标注JsonFormat(pattern yyyy-MM-dd HH:mm:ss)否则序列化出来可能是时间戳服务端解析可能对不上。如果个别字段可能为 null建议用包装类型如Integer、Long不要用int、long防止反序列化时空指针。传 body 最怕的就是“不知道服务端要什么字段”。所以我一般先去抓包看看浏览器里真实发的请求体长什么样再照着写 DTO。3.4 调试技巧用浏览器 Network 面板构造请求这里分享一个很实用的调试方法。比如你是在做页面某个功能对接可以先把浏览器开发者工具打开 Network 面板选中那个 ajax 请求找到Request Payload或者Request Body里的 JSON 内容。这就是服务端实际接收到的 body 格式。然后打开 Postman 或者 Apifox新建一个 POST 请求把 URL、Header、Body 原样复制进去先跑通一遍。如果 Postman 能通Feign 大概率也能通如果 Postman 都通不了就先别折腾代码说明接口本身就是这个入参格式照着改 DTO 就行。我经常遇到同事说“Feign 调不通”结果我在浏览器 Network 面板里一对比发现 URL 路径少了个前缀或者 Content-Type 不对。这类问题工具一抓就出来了。4. 经典报错与排查实录4.1 required request body is missing 到底是什么意思这个报错完整信息一般是required request body is missing: public org.jeecg.common.api.vo.Result com.example.controller.UserController.createUser(com.example.dto.UserCreateDTO)翻译过来就是服务端接口要读 body但是请求打过来的时候 body 是空的。常见原因有三个一是 Feign 接口方法参数上根本没有加RequestBody对象被当成了 query 参数拼进 URL。服务端当然读不到 body。二是 Feign 接口写的是 GET 请求比如GetMapping然后还想传 body。前面说了Feign 对 GET 传 body 支持很差大概率发不出 body。三是自定义了 Encoder 或者拦截器把请求体干掉了。这种情况先检查 Feign 配置类里有没有奇怪的 Encoder、RequestInterceptor把 body 里的内容替换或者清空。排查思路很简单先抓包看实际 HTTP 请求有没有 body没有就往前查 Feign 配置如果有 body 但服务端还是报缺 body再对比 Content-Type 是否匹配。4.2 failed to deserialize the json body into the target type这个报错的意思是服务端收到了 body但反序列化到目标类型时失败了。它的完整提示经常带一段 JSON 内容比如failed to deserialize the json body into the target type: input: missing field age说明服务端在把 JSON 转成 DTO 时发现缺少age字段或者字段类型对不上。这种情况十有八九是调用方传的 JSON 和服务端 DTO 定义不一致。举个例子服务端 DTO 有age属性调用方 DTO 写成了yearsOld序列化出来的 JSON 就没有age这个 key。还有一种是类型不匹配比如服务端定义Long id调用方传了字符串abcJackson 转换失败。再补充一个隐蔽的坑如果服务端 DTO 里加了一个新字段但又没配JsonIgnoreProperties(ignoreUnknown true)旧版调用方发来的 JSON 完全能通过但新版调用方多传了新字段服务端可能因为无法识别该字段而反序列化失败。Spring Boot 2.x 默认是忽略未知字段的但如果你改过 Jackson 配置或者用的是旧版本 Spring就可能会踩到。遇到这类问题把错误信息里的字段名和 DTO 对比一下基本能定位。4.3 收到一页 HTML请求打到哪去了这个特别经典。Feign 接口方法返回类型可能定义成了ResultUser但实际返回的是一段!doctype htmlbody stylebackground:#e8f4ff;text-align:center;margin-...看到 HTML基本可以确定请求没有打到预期的 API 接口上。要么是 URL 配置错了比如url少了个/api请求打到网关默认页面要么是服务端有 Spring Security、登录拦截器把没有鉴权的请求重定向到了登录页还有一种可能是服务端全局异常处理兜底返回了一个错误页。排查步骤用 Postman 或 Apifox 直接请求 Feign 里配的完整 URL看返回是不是也是 HTML。如果是把服务端日志翻出来看请求到底被谁拦截了。检查 Feign 的url配置是否带了正确的网关前缀、服务端口。看看是不是需要额外传递 Token 之类的 Header被拦截器挡了。我也遇到过一种情况服务端接口本身返回的是 JSON但 Feign 调用时没有加Accept: application/jsonHeader被网关降级处理返回了一个未授权页面。这种情况在浏览器的Request Headers里抄一下默认 Header加到 Feign 配置里就好了。4.4 传不同参数结果一样参数名对不上还有个很诡异的现象同时访问一个页面传不同参数但结果都一样。这种问题经常出在 Feign 传 body 时字段名对不上。比如服务端接口要的是userId调用方 DTO 里写的是id结果服务端每次拿到的都是 null默认走了一个兜底逻辑所以结果一样。另一个可能是服务端接口并没有用RequestBody接参数而是从 query 里取。传不同参数当然没反应。这种情况把 Postman 抓到的请求对比一下看参数是放在 query 还是 body 里再决定服务端和 Feign 怎么改。5. 进阶超时、重试与表单类型的“body”5.1 别忽略超时和线程配置传 body 传对了也不代表接口就一定能稳定调通。Feign 默认的超时时间很短如果服务端处理慢一点就可能超时。尤其是一些大报文 body 的接口序列化、传输、反序列化都需要时间。可以在配置里显式指定超时时间feign: client: config: default: connectTimeout: 5000 readTimeout: 15000connectTimeout是建立连接的超时readTimeout是从服务端读取数据的超时。如果接口经常处理大 JSONreadTimeout可以适当调大但也不要无脑调到几十秒最好配合慢 SQL、大文件上传等场景做压测。另外如果 Feign 调用量很大建议为 Feign 配置独立的线程池避免“线程饥饿”。不过这属于性能调优范畴了刚入门的朋友先保证功能通就行后面再优化。5.2 表单格式的 bodycontent-type 是 urlencoded最后再说一种容易被忽略的“body 形式”。Feign 默认传 JSON但有些老接口或者某些第三方服务要求的是application/x-www-form-urlencoded。虽然它也是 body但编码方式和 JSON 不一样需要借助feign-form库的SpringFormEncoder。引入依赖后Configuration public class FeignFormConfig { Bean public Encoder feignFormEncoder() { return new SpringFormEncoder(); } }Feign Client 方法就可以这么写FeignClient(name formService, url ${form.service.url}, configuration FeignFormConfig.class) public interface FormClient { PostMapping(value /oauth/token, consumes MediaType.APPLICATION_FORM_URLENCODED_VALUE) TokenResponse getToken(RequestParam(grant_type) String grantType, RequestParam(code) String code); }这种情况下参数以表单形式编码在 body 里服务端用RequestParam接收。它和 JSON body 的区别在于 Content-Type 和编码方式理解了这一点以后遇到 XML、二进制等其它 body 格式思路也是一样的选择合适的 Encoder设置正确的 Content-Type两边对齐就行。Feign 传 body 这块真的不算难但特别考验细心。注解、Content-Type、参数类型、服务端 DTO一个环节没对上就报错。我个人这几年踩坑下来最深刻的体会就是遇到问题先别急着改代码用抓包工具把真实的 HTTP 请求看清楚比瞎猜有效得多。像required request body is missing、反序列化失败、返回 HTML 这类问题只要对照实际请求一分就能定位。希望这篇能帮你少走点弯路有不同看法欢迎讨论。
返回列表