简单聊聊 OpenFeign 框架源码

简单聊聊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):

  1. 扫描 指定包下所有标了 @FeignClient 的接口;
  2. 每个接口注册一个 FeignClientFactoryBean 的 BeanDefinition(把 name、url、fallback 等属性塞进去);
  3. 注册默认配置(@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;
}

三个关键点:

  1. FeignContext 是每个 @FeignClient 独立的子上下文 (Spring 的 NamedContextFactory):不同 Feign 客户端可以有不同的拦截器、超时、编解码配置,互不污染------"A 服务 5 秒超时、B 服务 30 秒超时"就是这样实现的;
  2. 熔断集成 :配了 fallback/fallbackFactory 时,Targeter 换成 Sentinel/Resilience4j 的实现,把代理再包一层熔断;
  3. 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 扫描

套路完全一致:声明式接口 → 扫描注册 → 动态代理 → 元数据驱动执行。 讲透一个,另一个就是"换了个执行协议"。

十三、生产实践(常见坑)

  1. 连接池 :默认 Client.Default 无连接池,高并发下必须换 OkHttp/Apache(连接复用 + 池化);
  2. 超时 :connectTimeout + readTimeout 都要配(默认 10s/60s 偏大,建议按下游 SLA 收紧),可方法级覆盖;
  3. 重试要克制:写接口别重试(或靠幂等兜底);Feign 重试、负载均衡重试、网关重试三者叠加是灾难;
  4. 日志 :Feign 的 FULL 级别日志会打印请求体,生产只对排查中的客户端开,且注意脱敏;
  5. 接口不能方法重载(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 负责发,负载均衡在发之前换地址,解码器负责把响应变回对象。


相关笔记

相关推荐
Buke..1 小时前
【APP 逆向】哔哩哔哩 sign 参数逆向(下):OLLVM 混淆还原 sign 算法
java·javascript·爬虫·python·算法
半生过往2 小时前
前端学 Java 课程笔记
java·前端·笔记
mmsx2 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp
Buke..2 小时前
【APP 逆向】哔哩哔哩 sign 参数逆向(上):Frida 反调试绕过与 unidbg 调用
java·开发语言·爬虫·python·安卓
竹枝溪2 小时前
Tomcat与Servlet全套小白教程
java·servlet·tomcat·cookie·filter·session·listener
UseLessQQ3 小时前
云计算k8s集群部署
java·kubernetes·云计算
我命由我123453 小时前
人脸识别 - 人脸识别选帧
java·人工智能·python·算法·安全·java-ee·人脸识别
宠友信息3 小时前
IM系统开发技术路线分析,即时通讯源码助力快速搭建聊天应用
java·spring boot·redis·websocket·mysql·uni-app·vue
long3164 小时前
封装(Encapsulation)
java·人工智能·ai·ai编程