简单聊聊Feign框架源码
本文以 OpenFeign + Spring Cloud OpenFeign 源码为主线,拆解声明式 HTTP 客户端的完整实现:动态代理、接口元数据解析、请求模板、Client 执行、负载均衡与 Spring 整合。面向 Java 后端面试复习,源码均为"忠于原版的简化版",面试可现场画。与 简单聊聊SpringCloud 微服务调用链路源码(Feign 在链路中的位置)、简单聊聊MyBatis源码(同为"接口 + 动态代理"套路)互补阅读。
一句话总览
Feign 的本质是"接口 + 注解 → 动态代理 → HTTP 调用":把接口方法解析成元数据,调用时代理对象把参数填进请求模板,交给 Client 发 HTTP,响应解码成返回值。 源码主线只有一条:target(接口) 造代理 → invoke 构建请求 → Client.execute 发请求 → Decoder 解码。
一、Feign 是什么:先分清三个概念
| 概念 | 说明 |
|---|---|
| Feign | Netflix 开源的声明式 HTTP 客户端(已停维护) |
| OpenFeign | 社区接手的活跃版本,我们现在说的"Feign 源码"都是它 |
| Spring Cloud OpenFeign | Spring 的整合层:@FeignClient + @EnableFeignClients,接注册中心/负载均衡/熔断 |
使用方式(面试从这讲起):
java
@FeignClient(name = "pay-service", fallbackFactory = PayFallback.class)
public interface PayServiceClient {
@GetMapping("/pay/{orderId}")
PayResult query(@PathVariable("orderId") Long orderId);
}
注入 PayServiceClient 直接调用,一行 HTTP 代码都不用写------这就是"声明式"。
二、整体架构:六大件
@FeignClient 接口
│
▼
┌─────────────┐ 解析接口 ┌──────────────┐
│ Contract │──────────▶│ MethodMetadata │ 每个方法一份元数据
└─────────────┘ └──────┬───────┘
│
┌─────────────┐ 填参数 ┌────▼───────┐ 拦截器加 header
│ Encoder │──▶ body ───▶│ RequestTemplate │──▶ Request
└─────────────┘ └────┬───────┘
│
┌─────────────┐ 发请求 ┌────▼───────┐ 负载均衡挑实例
│ Client │◀────────────│ FeignBlocking │
└─────────────┘ │LoadBalancerClient│
│ └──────────────┘
▼
┌─────────────┐ 解响应 ┌──────────────┐
│ Decoder │──────────▶│ ErrorDecoder │ 非 2xx → FeignException
└─────────────┘ └──────────────┘
六件套:Contract(解析)、Encoder/Decoder(编解码)、Client(执行)、Retryer(重试)、ErrorDecoder(错误)、RequestInterceptor(拦截)。
三、源码主线:一次方法调用的完整旅程
① 启动:@EnableFeignClients → 扫描 @FeignClient 接口 → 注册 FeignClientFactoryBean
② 注入:getObject() → Feign.builder() 装配六件套 → target(接口) → ReflectiveFeign.newInstance
③ 代理:Proxy.newProxyInstance 生成 JDK 动态代理(FeignInvocationHandler + 方法→MethodHandler 映射)
④ 调用:payClient.query(1L) → handler.invoke(args)
⑤ 构建:MethodMetadata 指导,把参数填进 RequestTemplate(路径/query/body)
⑥ 拦截:RequestInterceptor 链处理(加 Token、TraceId 等公共 header)
⑦ 寻址:FeignBlockingLoadBalancerClient 通过注册中心 + LoadBalancer 挑一个实例
⑧ 执行:真实 Client(OkHttp/Apache)发 HTTP
⑨ 解码:2xx → Decoder 转对象返回;非 2xx → ErrorDecoder 转 FeignException
⑩ 重试:RetryableException 时按 Retryer 策略重试 ⑥~⑨
⑪ 降级:熔断开启时,异常走 fallback/fallbackFactory
四、动态代理:Feign 的骨架(核心源码)
4.1 ReflectiveFeign.newInstance
java
public <T> T newInstance(Target<T> target) {
// 1. 按接口名解析出 方法名 → MethodHandler 的映射
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
// 2. 转成 Method → MethodHandler(注意 key 是 Method 本身,所以 Feign 接口不支持方法重载)
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<>();
for (Method method : target.type().getMethods()) {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
// 3. 造 InvocationHandler
InvocationHandler handler = factory.create(target, methodToHandler);
// 4. JDK 动态代理生成实现类
return (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[]{target.type()}, handler);
}
4.2 FeignInvocationHandler.invoke
java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Object 的方法本地直接处理,不走 HTTP
if ("equals".equals(method.getName())) { ... }
else if ("hashCode".equals(method.getName())) { ... }
else if ("toString".equals(method.getName())) { ... }
// 业务方法:分发给对应 MethodHandler
return dispatch.get(method).invoke(args);
}
和 MyBatis MapperProxy 是同一个套路 (见 简单聊聊MyBatis源码):JDK 动态代理 + 方法元数据 + handler 分发------面试官听到你能把两个框架串起来讲,是明显加分项。
五、接口元数据解析:Contract 与 MethodMetadata
Contract 接口负责把接口解析成元数据:
java
public interface Contract {
List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType);
}
- Feign 默认的
Contract只认 Feign 自己的注解(@RequestLine("GET /pay/{id}")); - Spring Cloud 用
SpringMvcContract,让@GetMapping/@PathVariable/@RequestParam/@RequestBody全部兼容------这就是为什么 Feign 接口写法和 Controller 一模一样; MethodMetadata里存什么:方法名、HTTP 方法、路径模板、参数位置注解索引、返回值类型、是否异步------构建请求所需的全部信息都在这。
六、请求模板构建:RequestTemplate
SynchronousMethodHandler.invoke(最核心的方法之一):
java
public Object invoke(Object[] argv) throws Throwable {
// ① 用元数据 + 参数构建请求模板:占位符填充、query 拼接、body 序列化
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv); // 方法级超时覆盖
Retryer retryer = this.retryer.clone(); // 每次调用克隆一个,计数互不干扰
while (true) {
try {
// ② 发请求 + 解码(核心)
return executeAndDecode(template, options);
} catch (RetryableException e) {
// ③ 可重试异常 → 按 Retryer 策略决定继续还是抛出
retryer.continueOrPropagate(e);
continue;
}
}
}
executeAndDecode 简化:
java
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template); // 模板 → 真实请求
Response response = client.execute(request, options); // 发 HTTP
if (response.status() >= 200 && response.status() < 300) {
// 2xx:按返回值类型解码
if (isVoidType) return null;
return decoder.decode(response, metadata.returnType());
} else if (response.status() == 404 && isResponseType) {
return Response.builder().status(404).build(); // 404 且返回类型是 Response:原样给业务
}
// 其他非 2xx:ErrorDecoder 转成 FeignException 抛出
throw errorDecoder.decode(metadata.configKey(), response);
}
七、Client 执行层:发请求与负载均衡
Client 接口只有一个方法:
java
public interface Client {
Response execute(Request request, Request.Options options) throws IOException;
}
- 默认实现
Client.Default:JDK 的HttpURLConnection,无连接池,每次新建连接------生产一般换 OkHttp/Apache HttpClient(连接池复用); - Spring Cloud 用
FeignBlockingLoadBalancerClient包装(核心):
java
public Response execute(Request request, Request.Options options) throws IOException {
URI originalUri = URI.create(request.url());
String serviceId = originalUri.getHost(); // name 即服务名
// ① 从 LoadBalancer 挑一个实例(轮询等策略)
ServiceInstance instance = loadBalancerClient.choose(serviceId, ...);
// ② 把 URL 里的服务名替换成实例地址
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
Request newRequest = buildRequest(request, reconstructedUrl);
// ③ 交给被包装的真实 Client 发请求
return delegate.execute(newRequest, options);
}
面试点 :Feign 本身不做负载均衡,它只是"把 URL 的 host 当成服务名";是 FeignBlockingLoadBalancerClient 在发请求前完成"服务名 → 实例地址"的替换。注册中心、负载均衡完整链路见简单聊聊SpringCloud 微服务调用链路源码 。
八、编解码:Encoder / Decoder
- Encoder :把请求体对象序列化(默认 JDK 序列化,基本不可用);Spring 下用
SpringEncoder,委托 Spring MVC 的HttpMessageConverters(Jackson)转 JSON; - Decoder :把响应体反序列化成返回值类型;
SpringDecoder同理走 Jackson; - 扩展点:接口返回
Response时拿原始响应,返回 byte\[\]/Stream 时控制下载行为------解码器对返回值类型分支处理。
九、错误处理与重试:ErrorDecoder / Retryer
ErrorDecoder:状态码 → 异常类型:
404 → FeignException.NotFound
503 → RetryableException(可重试!)
其余 → FeignException
Retryer:
- 默认实现
Retryer.Default:最多重试 5 次 ,间隔 100ms 起、1.5 倍指数退避、上限 5 秒; - 重试只对
RetryableException(连接失败、503 等)生效; - 注意 :Spring Cloud 里默认换成
Retryer.NEVER_RETRY(不重试)------因为重试交给了负载均衡/熔断层,防止"Feign 重试 + 网关重试"叠加造成重复请求(写接口/扣款接口重试是资损风险,幂等设计见相关笔记); continueOrPropagate:达到次数上限抛RetryableException,否则 sleep 后放行。
十、拦截器与扩展点
RequestInterceptor(最常用):
java
@Bean
public RequestInterceptor traceInterceptor() {
return template -> {
template.header("X-Trace-Id", TraceContext.getTraceId()); // 链路透传
template.header("Authorization", TokenHolder.get()); // 鉴权透传
};
}
- 每个请求都会经过所有拦截器,公共 header 统一在这里加;
- 与 OpenFeign 新版的
Capability一样,都是"不改业务代码横切请求"的扩展点。
十一、Spring Cloud 整合(重头戏)
11.1 @EnableFeignClients 做了什么
java
@EnableFeignClients("com.interview.clients")
// ↓ 内部 @Import(FeignClientsRegistrar.class)
FeignClientsRegistrar(实现 ImportBeanDefinitionRegistrar):
- 扫描 指定包下所有标了
@FeignClient的接口; - 每个接口注册一个
FeignClientFactoryBean的 BeanDefinition(把 name、url、fallback 等属性塞进去); - 注册默认配置(
@EnableFeignClients的 configuration/defaultConfiguration)。
为什么用 FactoryBean 而不是直接注册代理? 代理的构建需要注册中心、配置中心就绪,所以用 FactoryBean 把代理的创建推迟到第一次注入时。
11.2 FeignClientFactoryBean.getObject:装配六件套
java
protected Feign.Builder feign(FeignContext context) {
Feign.Builder builder = get(context, Feign.Builder.class)
.encoder(get(context, Encoder.class)) // SpringEncoder
.decoder(get(context, Decoder.class)) // SpringDecoder
.contract(get(context, Contract.class)); // SpringMvcContract
configureFeign(context, builder); // 拦截器、日志、重试、超时
// 组装 FeignBlockingLoadBalancerClient:负载均衡能力从这里注入
builder.client(new FeignBlockingLoadBalancerClient(
get(context, Client.class), loadBalancerClient, loadBalancerClientFactory));
return builder;
}
三个关键点:
FeignContext是每个 @FeignClient 独立的子上下文 (Spring 的NamedContextFactory):不同 Feign 客户端可以有不同的拦截器、超时、编解码配置,互不污染------"A 服务 5 秒超时、B 服务 30 秒超时"就是这样实现的;- 熔断集成 :配了
fallback/fallbackFactory时,Targeter 换成 Sentinel/Resilience4j 的实现,把代理再包一层熔断; contextId解决同名问题 :多个@FeignClient(name = "pay-service")接口用 contextId 区分各自配置。
11.3 注入与启动时机
第一次注入 PayServiceClient 时 → FeignClientFactoryBean.getObject() → 走完整构建链 → 拿到代理。所以 Feign 接口是懒构建的,启动时只注册定义,不建连接。
十二、与 MyBatis 的对比(面试加分项)
| 维度 | MyBatis | Feign |
|---|---|---|
| 接口 | Mapper + SQL 注解 | 接口 + HTTP 注解 |
| 动态代理 | MapperProxy | FeignInvocationHandler |
| 元数据 | MappedStatement | MethodMetadata |
| 执行器 | Executor(JDBC) | Client(HTTP) |
| 工厂 | MapperFactoryBean | FeignClientFactoryBean |
| 整合套路 | MapperScannerConfigurer 扫描 | FeignClientsRegistrar 扫描 |
套路完全一致:声明式接口 → 扫描注册 → 动态代理 → 元数据驱动执行。 讲透一个,另一个就是"换了个执行协议"。
十三、生产实践(常见坑)
- 连接池 :默认
Client.Default无连接池,高并发下必须换 OkHttp/Apache(连接复用 + 池化); - 超时 :
connectTimeout+readTimeout都要配(默认 10s/60s 偏大,建议按下游 SLA 收紧),可方法级覆盖; - 重试要克制:写接口别重试(或靠幂等兜底);Feign 重试、负载均衡重试、网关重试三者叠加是灾难;
- 日志 :
Feign的 FULL 级别日志会打印请求体,生产只对排查中的客户端开,且注意脱敏; - 接口不能方法重载(Method 是 handler 的 key)、接口方法参数注解必须完整(Feign 靠注解定位参数)。
十四、高频追问速答
Q1:Feign 底层原理?
JDK 动态代理:@FeignClient 接口被扫描注册成 FeignClientFactoryBean,注入时构建代理;方法调用被 FeignInvocationHandler 分发到 MethodHandler,构建 RequestTemplate 后由 Client 发 HTTP。
Q2:Feign 和 MyBatis 为什么这么像?
同一套路:声明式接口 + 扫描注册 + 动态代理 + 元数据驱动执行。区别只是元数据(HTTP 注解 vs SQL)和执行器(Client vs JDBC Executor)。
Q3:Feign 怎么做负载均衡?
Feign 自己不负载均衡,Spring Cloud 把真实 Client 包成 FeignBlockingLoadBalancerClient:发请求前用 LoadBalancer 从注册中心挑实例,把 URL 里的服务名替换成实例地址。
Q4:Feign 默认 Client 有什么问题?
Client.Default 基于 HttpURLConnection,每次请求新建连接;生产换 OkHttp/Apache 的 Client,连接池复用。
Q5:Feign 默认重试几次?
原生 Retryer.Default 最多 5 次(指数退避);但 Spring Cloud 默认 NEVER_RETRY,把重试交给负载均衡层,避免多重试叠加。
Q6:@FeignClient 的 url 和 name 什么区别?
name 走注册中心服务发现 + 负载均衡;url 直接写死地址(测试环境、不走注册中心的外呼场景用)。
Q7:为什么 Feign 接口不能方法重载?
代理分发以 Method 为 key 建映射,重载会产生两个相同方法名的 handler,元数据 key 冲突。
Q8:Feign 调用怎么透传 TraceId?
RequestInterceptor 统一加 header------所有请求必经拦截器,链路追踪、鉴权 token 都在这加。
Q9:Feign 和 RestTemplate 对比?
RestTemplate 是编程式(自己拼 URL、自己序列化),Feign 是声明式(接口即契约);Feign 的注解和 Controller 同源,开发效率和可维护性更高,现在 RestTemplate 基本被替代。
Q10:fallback 和 fallbackFactory 区别?
fallback 只给降级结果,拿不到异常;fallbackFactory 能拿到异常原因(区分超时/限流/业务失败),做精细降级。
十五、总结
- 骨架:JDK 动态代理 + 方法元数据 + handler 分发,和 MyBatis 同一套路;
- 主线:调用 → 填 RequestTemplate → 拦截器 → 负载均衡选实例 → Client 执行 → Decoder 解码,异常走 ErrorDecoder/Retryer/fallback;
- Spring 整合三件事 :
@EnableFeignClients扫描注册 FactoryBean、懒构建代理、NamedContextFactory给每个客户端独立配置; - 面试主线:动态代理原理 → 完整调用链 → 负载均衡怎么注入 → 生产实践(连接池/超时/重试),把"为什么这么设计"串起来讲。
一句话:Feign = 把接口当契约,代理负责把"方法调用"翻译成"HTTP 请求",Client 负责发,负载均衡在发之前换地址,解码器负责把响应变回对象。
相关笔记
- 简单聊聊SpringCloud 微服务调用链路源码------Feign 在微服务调用链路中的完整位置(注册中心、LoadBalancer、Sentinel)
- 简单聊聊MyBatis源码------同一"声明式接口 + 动态代理"套路的另一实现
- 简单聊聊SpringBoot 执行流程源码------@EnableFeignClients 的 Import 机制与自动装配
- 简单聊聊Sentinel源码------Feign 熔断集成与降级