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. 结果包含了 bodyheaders 两个字段

序列化结果对比

传入的对象:

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 注解
相关推荐
momo_via3 小时前
maven下载与安装及在IDEA中配置maven
java·maven·intellij-idea
Deschen3 小时前
设计模式-适配器模式
java·设计模式·适配器模式
开发游戏的老王3 小时前
虚幻引擎虚拟制片入门教程 之 模型资源的导入
java·游戏引擎·虚幻
编啊编程啊程4 小时前
【004】生菜阅读平台
java·spring boot·spring cloud·dubbo·nio
Craaaayon4 小时前
【数据结构】二叉树-图解广度优先搜索
java·数据结构·后端·算法·宽度优先
岁岁岁平安4 小时前
Java+SpringBoot+Dubbo+Nacos快速入门
java·spring boot·nacos·rpc·dubbo
学习编程的Kitty5 小时前
算法——位运算
java·前端·算法
用户904706683575 小时前
如何使用 Spring MVC 实现 RESTful API 接口
java·后端
刘某某.5 小时前
数组和小于等于k的最长子数组长度b
java·数据结构·算法