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 的处理流程:
- 识别传入的是
HttpEntity
类型 - 调用
getBody()
提取请求体 - 调用
getHeaders()
提取请求头 - 将 headers 设置到 HTTP 请求头
- 只序列化 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 的处理流程:
- 接收到
Object
类型的参数(实际是HttpEntity
) - 不识别这是
HttpEntity
,当成普通对象 - 使用 Jackson 序列化整个对象
- 结果包含了
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
经验教训
- 不同的 HTTP 客户端对同一个对象的处理可能不同
- 从 RestTemplate 迁移到 Feign 时,不能简单替换
- 遇到接口调用问题,要对比实际发送的请求体
- 理解框架的设计原理,而不是死记 API
注意事项
使用 Feign 时:
- 直接传业务对象,不要用
HttpEntity
包装 - 需要自定义 headers 时,使用
@RequestHeader
注解