RestTemplate 和 Feign 传参差异导致的接口调用失败

RestTemplate 和 Feign 传参差异导致的接口调用失败

问题背景

项目中有一个试驾结束后推送 TDA 的功能,原来使用 RestTemplate 调用正常,后来改用 Feign 重构重推功能时,发现调用失败。

问题现象

原始代码(正常)

java 复制代码
// 构建请求
HttpEntity<Object> adaptTDA = adapterTDARequest.stopTestDriveSendTda(
    getTestDriveSheetResult,
    queryUscMdmEmpResult.get(0)
);

// 使用 RestTemplate 调用
String tdaResult = postData(lookUpResult.get(0).getLookUpValueName(), adaptTDA);

重构代码(失败)

java 复制代码
// 构建请求(同样的方法)
HttpEntity<Object> adaptTDA = adapterTDARequest.stopTestDriveSendTda(
    sheetBO,
    queryUscMdmEmpResult.get(0)
);

// 改用 Feign 调用
tdaResult = tdaFeign.stopTestDriveSendTda(adaptTDA);

Feign 接口定义

java 复制代码
@FeignClient(name = "${feign.client.config.tda.url}", 
             url = "${feign.client.config.tda.url}")
public interface TDAFeign {
    @PostMapping(value = "/sca/saletool/smart/drive", 
                 consumes = "application/json")
    String stopTestDriveSendTda(@RequestBody Object param);
}

排查过程

1. 对比日志

开启 Feign 和 RestTemplate 的日志后,发现请求体不一样:

RestTemplate 发送的请求体:

csharp 复制代码
Writing [{"reception_ed":1760322532648,"client_info":{"client_phone":"18085741555","client_name":"曾兴宇","client_id":"77bd2f6c7c0f4288a08129663a574fcb"},"user_id":"106903","drive_id":"SCSJSS85102251012003","reception_bg":1760259783000,"drive_info":{"drive_ed":1760320732649,"drive_route":[],"drive_car":"HX11","drive_bg":1760261585000}}] as "application/json;charset=UTF-8"

Feign 发送的请求体:

csharp 复制代码
Writing [<{"reception_ed":1760322332033,"client_info":{"client_phone":"18602994468","client_name":"武宇青老公","client_id":"03679b8750404f738b81006bcb4aa203"},"user_id":"110094","drive_id":"SCSJSS91001251009001","reception_bg":1759978703000,"drive_info":{"drive_ed":1760320532033,"drive_route":[],"drive_car":"HX11","drive_bg":1759980505000}},[Content-Type:"application/json; charset=UTF-8"]>] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@511a307e]

注意 Feign 的请求体多了 <...,[Content-Type:"application/json; charset=UTF-8"]> 这部分。

2. 分析差异

RestTemplate 发送的是纯 JSON:

json 复制代码
{"reception_ed":...,"client_info":{...}}

Feign 发送的包含了 HttpEntity 的结构:

json 复制代码
{
  "body": {"reception_ed":...,"client_info":{...}},
  "headers": {"Content-Type": ["application/json; charset=UTF-8"]}
}

显然,Feign 把整个 HttpEntity 对象序列化了。

原因分析

HttpEntity 的结构

java 复制代码
public class HttpEntity<T> {
    private final HttpHeaders headers;  // 请求头
    private final T body;               // 请求体
    
    public HttpHeaders getHeaders() { return headers; }
    public T getBody() { return body; }
}

HttpEntity 是 Spring 提供的一个包装类,用于同时携带请求体和请求头。

RestTemplate 的处理方式

查看 RestTemplate 源码:

java 复制代码
// RestTemplate.java
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
        @Nullable HttpEntity<?> requestEntity, Class<T> responseType, ...) {
    
    RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);
    // ...
}

protected <T> RequestCallback httpEntityCallback(@Nullable HttpEntity<?> requestEntity, Type responseType) {
    return new HttpEntityRequestCallback(requestEntity, responseType);
}

关键在 HttpEntityRequestCallback 类:

java 复制代码
private class HttpEntityRequestCallback extends AcceptHeaderRequestCallback {
    
    private final HttpEntity<?> requestEntity;
    
    @Override
    public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
        super.doWithRequest(httpRequest);
        
        // 提取 body
        Object requestBody = this.requestEntity.getBody();
        
        if (requestBody != null) {
            // 提取 headers
            HttpHeaders requestHeaders = this.requestEntity.getHeaders();
            
            // 设置 headers
            if (!requestHeaders.isEmpty()) {
                httpRequest.getHeaders().putAll(requestHeaders);
            }
            
            // 序列化 body(只序列化 body,不包含 headers)
            // 使用 HttpMessageConverter 写入
            writeWithMessageConverters(requestBody, ...);
        }
    }
}

RestTemplate 的处理流程:

  1. 识别传入的是 HttpEntity 类型
  2. 调用 getBody() 提取请求体
  3. 调用 getHeaders() 提取请求头
  4. 将 headers 设置到 HTTP 请求头
  5. 只序列化 body 部分

Feign 的处理方式

Feign 接口定义:

java 复制代码
@PostMapping(consumes = "application/json")
String stopTestDriveSendTda(@RequestBody Object param);

Feign 的序列化逻辑:

java 复制代码
// SpringEncoder.java
public void encode(Object requestBody, Type bodyType, RequestTemplate template) {
    
    // Feign 不会特殊处理 HttpEntity
    // 直接把传入的对象当成普通 Java 对象序列化
    
    for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
        if (messageConverter.canWrite(requestBody.getClass(), contentType)) {
            // 序列化整个 requestBody
            messageConverter.write(requestBody, contentType, outputMessage);
            return;
        }
    }
}

Feign 的处理流程:

  1. 接收到 Object 类型的参数(实际是 HttpEntity)
  2. 不识别这是 HttpEntity,当成普通对象
  3. 使用 Jackson 序列化整个对象
  4. 结果包含了 body 和 headers 两个字段

序列化结果对比

传入的对象:

java 复制代码
HttpEntity<Map<String, Object>> adaptTDA = new HttpEntity<>(body, headers);

RestTemplate 序列化:

java 复制代码
// 只序列化 body
{"reception_ed": 1760322532648, "client_info": {...}}

Feign 序列化:

java 复制代码
// 序列化整个 HttpEntity 对象
{
  "body": {"reception_ed": 1760322532648, "client_info": {...}},
  "headers": {"Content-Type": ["application/json; charset=UTF-8"]}
}

为什么会这样?

RestTemplate 的设计:

  • exchange() 方法的参数类型明确是 HttpEntity<?>
  • 内部有专门的 HttpEntityRequestCallback 处理
  • 知道如何提取 body 和 headers

Feign 的设计:

  • 接口方法参数类型是 @RequestBody Object
  • 是一个通用的序列化框架
  • 不知道传入的是 HttpEntity,当成普通 POJO 处理

总结

核心问题

RestTemplate 和 Feign 对 HttpEntity 的处理方式不同:

  • RestTemplate 会自动提取 body 和 headers
  • Feign 会把整个 HttpEntity 当成普通对象序列化

根本原因

  • RestTemplate 的 exchange() 方法参数类型是 HttpEntity<?>,有专门的处理逻辑
  • Feign 的接口方法参数类型是 @RequestBody Object,是通用序列化,不识别 HttpEntity

经验教训

  1. 不同的 HTTP 客户端对同一个对象的处理可能不同
  2. 从 RestTemplate 迁移到 Feign 时,不能简单替换
  3. 遇到接口调用问题,要对比实际发送的请求体
  4. 理解框架的设计原理,而不是死记 API

注意事项

使用 Feign 时:

  • 直接传业务对象,不要用 HttpEntity 包装
  • 需要自定义 headers 时,使用 @RequestHeader 注解
相关推荐
_Evan_Yao6 小时前
Redis 缓存穿透、击穿、雪崩,一次讲清!
java·spring·mybatis
梓豪csd6 小时前
消息订阅中心:谁该收到哪条通知
java·开发语言·人工智能·低代码·自动化·1024程序员节
秋饼6 小时前
Spring AI 2.1 Message Parts 深度实战:推理签名回放与跨轮记忆持久化的源码级落地
java·ai·技术分享·后端开发
thefool1122667 小时前
TCP 1
java·1024程序员节
金銀銅鐵9 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
AI人工智能+电脑小能手9 小时前
一次请求打几个下游,这个Java并行任务工具类6个方法按序聚合
java·线程池·并发编程·1024程序员节·批量处理·并行任务
paopaokaka_luck11 小时前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节
2301_322414280411 小时前
springboot农产品交易平台的设计与实现08055-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·spring boot·redis·mysql·tomcat·maven·mybatis
考虑考虑12 小时前
JDK26的JEP 500属性
java·后端·java ee
LMT流氓兔12 小时前
第五单元——第十二课:把脚本整理成项目
java·数据库·python·langchain·1024程序员节