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

58 阅读3分钟

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

问题背景

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

问题现象

原始代码(正常)

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

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

重构代码(失败)

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

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

Feign 接口定义

@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 发送的请求体:

Writing [{"reception_ed":xxxx,"client_info":{"client_phone":"xxxx","client_name":"xxx","client_id":"xxxx"},"user_id":"xxxx","drive_id":"xxxx","reception_bg":xxxx,"drive_info":{"drive_ed":xxxx,"drive_route":[],"drive_car":"xxxx","drive_bg":xxxx}}] as "application/json;charset=UTF-8"

Feign 发送的请求体:

Writing [<{"reception_ed":xxxx,"client_info":{"client_phone":"xxxx","client_name":"xxxx","client_id":"xxxx"},"user_id":"xxxx","drive_id":"xxxx","reception_bg":xxxx,"drive_info":{"drive_ed":xxxx,"drive_route":[],"drive_car":"xxxx","drive_bg":xxxx}},[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:

{"reception_ed":...,"client_info":{...}}

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

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

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

原因分析

HttpEntity 的结构

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 源码:

// 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 类:

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 接口定义:

@PostMapping(consumes = "application/json")
String stopTestDriveSendTda(@RequestBody Object param);

Feign 的序列化逻辑:

// 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 两个字段

序列化结果对比

传入的对象:

HttpEntity<Map<String, Object>> adaptTDA = new HttpEntity<>(body, headers);

RestTemplate 序列化:

// 只序列化 body
{"reception_ed": 1760322532648, "client_info": {...}}

Feign 序列化:

// 序列化整个 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 注解