契约与编解码
OpenFeign 的接口方法不是直接反射调用,而是先由 Contract 编译成请求元数据,再由编码器和解码器把 Java 对象接到 HTTP 字节流。
先给答案:Contract 和 Codec 分别解决“请求长什么样”和“数据如何变成对象”
Section titled “先给答案:Contract 和 Codec 分别解决“请求长什么样”和“数据如何变成对象””Contract 读取接口注解,把方法签名编译为 RequestTemplate 元数据;Encoder 把参数变成请求体,Decoder 把响应体还原成返回类型。契约解析与编解码分离,才能支持 Spring MVC、JAX-RS 等不同注解模型。
类型信息在响应解码时尤其重要:泛型、集合、错误响应和空响应不能只靠运行时 class 判断。排查“服务端返回正常但客户端解析失败”时,要分别看契约生成的请求、实际 Content-Type 和 decoder 获得的目标类型。
Java method + annotations │ ▼SpringMvcContract ├─ HTTP method / path ├─ query / header / path variable └─ body / return Type │ ▼Feign MethodMetadata -> RequestTemplate -> RequestSpringMvcContract 继承 Feign Contract.BaseContract,见 SpringMvcContract:98。parseAndValidateMetadata:205-207 先交给父契约处理,再处理 Spring MVC 注解;参数处理集中在 processAnnotationsOnParameter:326。
为什么复用 Spring MVC 注解
Section titled “为什么复用 Spring MVC 注解”替代方案:重新定义一套 @FeignGet、@FeignBody 等客户端注解。
为什么不行:应用已经用 Spring MVC 的参数语义描述 HTTP,重复定义会造成注解学习成本和语义漂移;复用注解还能直接共享 ConversionService、格式化和参数命名规则。
源码证据:FeignClientsConfiguration#feignContract:125-127 默认创建 SpringMvcContract,而不是 Feign 原生 Contract.Default。
request body └─► SpringEncoder#encode └─► HttpMessageConverter.write
HTTP response └─► SpringDecoder#decode └─► HttpMessageConverter.read └─► return Type默认 Decoder 在 FeignClientsConfiguration#feignDecoder:109-113 组装为 OptionalDecoder(ResponseEntityDecoder(SpringDecoder));Encoder 在 :118-121 创建 SpringEncoder。SpringEncoder#encode:90-101 根据媒体类型和表单场景选择转换器,SpringDecoder#decode:52-62 将 Feign Response 包装为 Spring ClientHttpResponse。
类型信息为什么重要
Section titled “类型信息为什么重要”Feign 方法返回值是 Java Type,不是简单的 Class。泛型集合、ResponseEntity<T> 和分页对象都需要保留参数化类型,否则解码器只能得到 LinkedHashMap 或原始集合。OpenFeign 把类型元数据保留到 Decoder,是反射契约和消息转换之间的桥。
默认重试语义
Section titled “默认重试语义”FeignClientsConfiguration#feignRetryer:138-142 默认提供 Retryer.NEVER_RETRY。这和 Feign 原生默认重试策略不同,目的是避免 Spring Cloud 在没有显式策略时对业务请求进行不可见重试;服务治理重试应由 LoadBalancer 或业务明确配置。
| 场景 | 现象 | 原因 | 规避 |
|---|---|---|---|
缺少 Content-Type |
JSON 被当作字符串或表单 | Converter 选择依赖媒体类型 | 显式声明 consumes/headers |
| 返回空体 | 解码异常或得到 null | Decoder 不知道是否允许空响应 | 明确返回 void 或处理 204 |
| 泛型被擦除 | 列表元素类型错误 | 只传入原始 Class | 保留方法返回 Type |
| 自定义 Converter 顺序不当 | 默认 JSON 解析失效 | Spring Converter 按顺序匹配 | 检查 converter 顺序和媒体类型 |
“先编译声明,再执行 IO”是 RPC、GraphQL 客户端和 SQL Mapper 的共同模式。把参数绑定编译成中间表示,可以降低每次调用的反射成本,并让校验在启动阶段提前失败。
面试锚点
SpringMvcContract和 Feign 原生 Contract 的差异是什么?- Encoder/Decoder 为什么要复用
HttpMessageConverter?- OpenFeign 默认为什么关闭 Feign 原生重试?