OpenFeign 源码阅读 —— 请求编码与参数解析
一次 Feign 调用,方法参数如何变成 HTTP 请求?本文从源码拆解三层:Contract 把注解解析为元数据、RequestTemplate 把元数据与参数合成为模板、Encoder 把对象编码进请求体。
Contract:协议解析
接口与职责
java
// feign.Contract
public interface Contract {
List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}接口注解(@GetMapping/@PathVariable/@RequestHeader...)
│
▼ Contract(SpringMvcContract)
│
▼ MethodMetadata(方法元数据:URL 模板、参数索引、类型映射)SpringMvcContract
Spring Cloud 默认使用 SpringMvcContract 解析 Spring MVC 注解:
java
// spring-cloud-openfeign-core
public class SpringMvcContract extends Contract.BaseContract {
@Override
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
// 1. 解析方法级注解
// @RequestMapping/@GetMapping → 收集 path、method、headers、produces...
// 2. 解析参数级注解
// @PathVariable → 路径变量
// @RequestParam → 查询参数
// @RequestHeader → 请求头
// @RequestBody → 请求体
// 3. 生成 configKey(类名#方法名(参数类型))作为唯一标识
return metadata;
}
}解析的注解类型
| 注解 | 元数据落点 |
|---|---|
| @GetMapping / @PostMapping / @RequestMapping | URL 模板、HTTP 方法、Headers、Consumes |
| @PathVariable | 路径变量({id} 占位符) |
| @RequestParam | 查询参数 |
| @RequestHeader | 请求头 |
| @RequestBody | 请求体(触发 Encoder) |
| @RequestPart / @ModelAttribute | 表单/文件 |
MethodMetadata:方法元数据
数据结构
java
// feign.MethodMetadata
public final class MethodMetadata {
private final String configKey; // 唯一标识:类#方法(参数)
private String returnType; // 返回类型描述
private transient Type returnTypeClass; // 返回类型 Class
private RequestTemplate template; // URL/方法/头 模板
private final Map<Integer, Param> indexToName = new LinkedHashMap<>(); // 参数索引 → 名称
private Map<Integer, Collection<String>> indexToExpanderClass; // 参数索引 → 展开器
private boolean bodyIndex; // 是否有 @RequestBody
private int bodyIndexValue; // 请求体参数索引
private boolean queryMapIndex; // @QueryMap
private boolean formParams; // 表单参数
}关键字段
- template:URL 模板(含
{pathVar}占位符)与请求方法 - indexToName:参数位置 → 参数名(
@PathVariable("id") Long id→ index 0 → "id") - bodyIndex:哪个参数是请求体(
@RequestBody) - returnTypeClass:解码目标类型
RequestTemplate:模板构建
模板工厂
java
// feign.RequestTemplate.Factory
static final class Factory {
// 参数 → 模板
RequestTemplate create(Object[] argv) {
RequestTemplate template = this.template; // 克隆方法级模板
// 遍历所有参数,按索引填充
for (int i = 0; i < argv.length; i++) {
Object arg = argv[i];
// 1. 路径变量 {id} → 值
// 2. 查询参数 → 追加
// 3. 请求头 → 填充
// 4. @RequestBody → 编码为 body
}
return template;
}
}URL 解析流程
接口方法:@GetMapping("/api/order/{id}")
Order getOrder(@PathVariable("id") Long id);
调用:proxy.getOrder(1001L)
│
▼
Template: {id} 占位符 + 参数 [0 → 1001L]
│
▼
/api/order/1001L ← 占位符替换RequestTemplate 结构
java
public class RequestTemplate {
private String method; // GET/POST...
private URI uri; // 目标地址
private final Map<String, Collection<String>> queries; // 查询参数
private final Map<String, Collection<String>> headers; // 请求头
private byte[] body; // 请求体(编码后)
private final Request.Body bodyData;
}参数展开器
java
// 复杂参数类型需要展开器
// 例:@RequestParam Map<String, String> → 每个 entry 变为查询参数
Map<Integer, Collection<String>> indexToExpanderClass;
// @RequestParam 默认用 toString 展开SynchronousMethodHandler 构建模板
java
// SynchronousMethodHandler.invoke()
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv); // 1. 模板工厂
...
}完整链路:
buildTemplateFromArgs.create(argv)
├─ 1. 克隆方法级模板(含 URL 模式、headers)
├─ 2. 遍历参数:
│ ├─ @PathVariable(i) → 模板占位符替换
│ ├─ @RequestParam(i) → queries 追加
│ ├─ @RequestHeader(i) → headers 追加
│ └─ @RequestBody(i) → 标记 bodyIndex
├─ 3. 编码请求体(bodyIndex 参数 → Encoder)
└─ 4. 返回完整 RequestTemplateEncoder:请求体编码
接口
java
// feign.codec.Encoder
public interface Encoder {
void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
// 默认实现:只支持 byte[] / String / InputStream
class Default implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
if (object instanceof byte[]) {
template.body((byte[]) object, null);
} else if (object instanceof String) {
template.body((String) object);
} else if (object instanceof Iterable) {
// 迭代展开
} else {
throw new EncodeException("不是支持的请求体类型: " + object.getClass());
}
}
}
}SpringEncoder(默认)
java
// spring-cloud-openfeign-core
public class SpringEncoder implements Encoder {
private final HttpMessageConverter messageConverter; // Spring 消息转换器
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
// 委托给 Spring 的 HttpMessageConverter(默认 Jackson)
HttpMessageConverter converter = getConverter(...);
HttpOutputMessage outputMessage = new RequestTemplateOutputMessage(template);
converter.write(object, parameterType, outputMessage); // 序列化为 JSON
}
}对象(OrderRequest)→ SpringEncoder → Jackson 序列化 → JSON → RequestTemplate.body编码器链
编码选择:
├─ @RequestBody 对象 → SpringEncoder(Jackson JSON)
├─ 表单(@ModelAttribute)→ 表单编码器
├─ 文件上传 → MultipartFormEncoder
└─ 原生 byte[]/String → 默认编码器完整请求构建时序
proxy.createOrder(orderRequest)
│
▼
FeignInvocationHandler.invoke()
│
▼
SynchronousMethodHandler.invoke(argv)
│
├─ buildTemplateFromArgs.create(argv)
│ ├─ 方法级模板(/api/order POST, Content-Type: application/json)
│ ├─ @RequestBody 参数 → SpringEncoder → JSON
│ └─ @RequestParam / @PathVariable → 填充
│
├─ targetRequest(template) → Request(含编码后的 body)
│
└─ client.execute(request) → 发送常见问题
- 参数名为 arg0/arg1? SpringMvcContract 依赖真实参数名(
-parameters编译参数),缺失时注解必须显式写 name。 - @RequestBody 与 @RequestParam 混用? 一个方法只能有一个 body 参数(bodyIndex 唯一),其他参数必须是 path/query/header。
- 复杂对象作为 @RequestParam? 需自定义展开器(Expander),否则 toString 编码。
- 编码失败(400)? 检查 Encoder 是否支持该类型(Jackson 是否能序列化)、Content-Type 是否正确。