Feign 远程调用原理

候选人小孙在面试字节微服务团队时,面试官问:"Feign 的原理是什么?它是怎么把一个接口调用变成 HTTP 请求的?"

小孙说:"Feign 是一个声明式的 HTTP 客户端..." 面试官追问:"那 @FeignClient 注解标注的接口,是怎么变成实际 HTTP 请求的?"

小孙说:"好像是动态代理..." 面试官继续追问:"代理是怎么创建的?请求参数是怎么转换成 HTTP 请求体的?"

小孙支支吾吾答不上来。

面试官又问:"Feign 和 Ribbon 是什么关系?Feign 的负载均衡是怎么实现的?"

小孙彻底卡住。

【面试官心理】

这道题我用来测试候选人对 Feign 底层原理的理解深度。Feign 是 Spring Cloud 中最常用的服务调用方式,但 90% 的候选人只会用注解,不知道它背后的动态代理、请求构建、超时配置的完整链路。能说出动态代理原理的占 30%,能讲清楚整个调用链路的只有 10%。这道题是区分"用过"和"理解过源码"的分水岭。

一、Feign 的本质:动态代理 🔴

1.1 最简单的 Feign 用法

// 1. 定义 Feign 客户端接口
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {

    @RequestMapping(method = RequestMethod.GET, path = "/user/{id}")
    User getUser(@PathVariable("id") Long id);

    @RequestMapping(method = RequestMethod.POST, path = "/user/create")
    User createUser(@RequestBody UserRequest request);
}

// 2. 使用接口调用(就像调用本地方法一样)
@Service
public class OrderService {
    @Autowired
    private UserClient userClient;

    public Order getOrderWithUser(Long orderId) {
        Order order = orderRepository.findById(orderId);
        User user = userClient.getUser(order.getUserId());  // 看起来像本地调用
        order.setUser(user);
        return order;
    }
}

1.2 为什么 Feign 用动态代理?

普通 HTTP 调用:

// ❌ 传统方式:每个调用都要写完整的 HTTP 请求逻辑
@Service
public class UserService {
    public User getUser(Long id) {
        // 1. 构建 URL
        String url = "http://user-service/user/" + id;

        // 2. 发送 HTTP 请求
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity = new HttpEntity<>(headers);

        // 3. 处理响应
        ResponseEntity<User> response = restTemplate.exchange(
            url,
            HttpMethod.GET,
            entity,
            User.class
        );

        return response.getBody();
    }
}

Feign 让我们用接口的方式调用远程服务:

// ✅ Feign 方式:接口即调用
@FeignClient(name = "user-service")
public interface UserClient {
    @RequestMapping(method = RequestMethod.GET, path = "/user/{id}")
    User getUser(@PathVariable("id") Long id);
}

// 使用时就像调用本地方法
userClient.getUser(1L);  // 实际会发起 HTTP 请求

二、Feign 代理创建链路 🔴

2.1 @EnableFeignClients 扫描

// FeignClientsRegistrar.java
// Spring 启动时扫描 @FeignClient 注解的接口

@Import(FeignClientsRegistrar.class)
@Configuration
@ConditionalOnClass(FeignClient.class)
public @interface EnableFeignClients {
    // 扫描的基础包路径
    String[] value() default {};
    String[] basePackages() default {};
    // 要扫描的类(更精确控制)
    Class<?>[] clients() default {};
}

// FeignClientsRegistrar 实现
public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
                                        RegisterCallback callback) {
        // 1. 获取 @EnableFeignClients 注解的配置
        Map<String, Object> attrs = metadata.getAnnotationAttributes(
            EnableFeignClients.class.getName());

        // 2. 扫描 basePackages 下的所有 @FeignClient 接口
        registerFeignClients(attrs, callback);
    }

    private void registerFeignClients(Map<String, Object> attrs, ...) {
        // 获取要扫描的包路径
        String[] basePackages = getBasePackages(attrs);

        // 使用 ClassPathScanningCandidateComponentProvider 扫描
        // 找到所有标注了 @FeignClient 的接口
        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidateComponents = scanner.findCandidates(...);
            for (BeanDefinition beanDefinition : candidateComponents) {
                // 注册为 Spring Bean
                registerFeignClient(config, beanDefinition);
            }
        }
    }
}

2.2 FeignClientFactoryBean 创建代理

// FeignClientFactoryBean.java
// 每个 @FeignClient 接口由这个 FactoryBean 创建动态代理

@Component
public class FeignClientFactoryBean implements FactoryBean<Object>, ApplicationContextAware {
    private Class<?> type;
    private String name;          // 服务名:user-service
    private String url;           // 直接 URL(可选)
    private String path;          // 路径前缀
    private Class<?> fallback;    // 降级类

    @Override
    public Object getObject() {
        // ⭐ 核心:创建 Feign 客户端的动态代理

        // 1. 构建 Feign.Builder
        Feign.Builder builder = Feign.builder();

        // 2. 添加客户端:默认使用 LoadBalancerFeignClient(集成 Ribbon 负载均衡)
        builder.client(new LoadBalancerFeignClient(
            new DefaultFeignLoadBalancedConfiguration(),
            new SpringCloudLoadBalancerFactory(),
            new DefaultSpringCloudLoadBalancerFactory()
        ));

        // 3. 添加编码器/解码器
        builder.encoder(new SpringFormEncoder());
        builder.decoder(new ResponseEntityDecoder(new SpringDecoder()));

        // 4. 添加日志
        builder.logger(new Slf4jLogger(type));

        // 5. 添加 Contract:解析 @RequestMapping 等注解
        builder.contract(new SpringMvcContract());

        // 6. 构建 Feign 客户端(ReflectiveFeign)
        Feign.Builder feignBuilder = builder;
        target = HardcodedTarget<>(type, name, url);

        // 7. 创建动态代理
        return feignBuilder.target(target);
    }

    // 最终调用的方法
    public <T> T target(Class<T> feignClient, String url) {
        return build(feignClient, new HardcodedTarget<>(feignClient, name, url));
    }
}

2.3 ReflectiveFeign 动态代理机制

// ReflectiveFeign.java - Feign 的动态代理实现

// 1. 创建 InvocationHandler
public <T> T newInstance(Target<T> target) {
    // 解析接口上的 @RequestMapping 注解,构建元数据
    Map<String, MethodHandler> nameToHandler = methodHandlerFactory.create(target);
    InvocationHandler handler = new FeignInvocationHandler(target, nameToHandler);

    // 2. JDK 动态代理:创建接口的代理对象
    T proxy = (T) Proxy.newProxyInstance(
        target.type().getClassLoader(),
        new Class<?>[] { target.type() },
        handler
    );

    return proxy;
}

// 3. InvocationHandler:方法调用拦截
class FeignInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        // 4. 根据方法名找到对应的 MethodHandler
        MethodHandler handler = dispatch.get(method);

        // 5. 调用 SynchronousMethodHandler 执行实际 HTTP 请求
        return handler.invoke(args);
    }
}

// 6. SynchronousMethodHandler:实际执行 HTTP 请求
class SynchronousMethodHandler implements MethodHandler {
    @Override
    public Object invoke(Object[] argv) {
        // 6.1 构建请求模板
        RequestTemplate template = buildTemplate(argv);

        // 6.2 执行请求(通过 Client)
        Request request = template.request();
        Response response = client.execute(request, options);

        // 6.3 解码响应
        return decode(response);
    }
}

2.4 完整调用链路

sequenceDiagram
    participant C as 调用方
    participant P as JDK动态代理
    participant H as FeignInvocationHandler
    participant M as SynchronousMethodHandler
    participant L as LoadBalancerFeignClient
    participant R as Ribbon/Nacos
    participant S as 目标服务

    C->>P: userClient.getUser(1L)
    P->>H: invoke(getUser, [1L])
    H->>M: handler.invoke([1L])
    M->>M: 1. 构建RequestTemplate
    M->>M: 2. 解析@PathVariable等注解
    M->>L: 3. execute(request)
    L->>R: 4. choose("user-service")
    R->>L: 返回可用实例IP:Port
    L->>S: 5. HTTP GET /user/1
    S-->>L: 返回User JSON
    L-->>M: Response
    M-->>H: User对象
    H-->>P: User对象
    P-->>C: User对象

三、请求构建过程 🔴

3.1 Contract 解析注解

// SpringMvcContract.java - 解析 Spring MVC 注解
public class SpringMvcContract extends Contract {
    @Override
    protected void processAnnotationOnMethod(
        MethodMetadata data,
        Annotation methodAnnotation,
        Method method) {

        if (methodAnnotation instanceof RequestMapping) {
            RequestMapping mapping = (RequestMapping) methodAnnotation;

            // 解析 HTTP 方法
            data.template().method(
                String.valueOf(mapping.method()[0].name())
            );

            // 解析路径
            data.template().uri(
                resolve(mapping.value()[0], method)
            );

            // 解析 @RequestHeader 参数
            // 解析 @RequestParam 参数
            // 解析 @PathVariable 参数
            // 解析 @RequestBody 参数
        }
    }
}

// 例如:@RequestMapping(method = GET, path = "/user/{id}")
// 解析为模板:GET /user/{id}

3.2 请求参数处理

// SynchronousMethodHandler.java
// 参数如何转换成 URL 查询参数和请求体

public Object invoke(Object[] argv) {
    RequestTemplate template = buildTemplate(argv);

    // 处理参数
    // @PathVariable -> 替换 URL 中的 {id}
    // @RequestParam -> 添加到 URL 查询参数
    // @RequestHeader -> 添加到请求头
    // @RequestBody -> 作为请求体

    return dispatcher.dispatch(template, argv);
}

// 参数替换示例
@FeignClient(name = "user-service")
public interface UserClient {
    // /user/123?name=zhang&age=25
    @RequestMapping(method = RequestMethod.GET, path = "/user/{id}")
    User getUser(
        @PathVariable("id") Long id,       // -> URL 路径参数
        @RequestParam("name") String name, // -> URL 查询参数
        @RequestHeader("X-Token") String token  // -> 请求头
    );
}

四、Feign 与 Ribbon 的集成 🔴

4.1 LoadBalancerFeignClient

// LoadBalancerFeignClient.java - Feign 集成负载均衡
public class LoadBalancerFeignClient implements Client {
    private final LoadBalancer loadBalancer;

    @Override
    public Response execute(Request request, Request.Options options) {
        // 1. 从 URL 中提取服务名:http://user-service/user/1 -> user-service
        URL url = URL.parse(request.url());
        String clientName = url.getHost();

        // 2. 调用 Ribbon 的负载均衡器选择实例
        Server chosen = loadBalancer.chooseServer(clientName);

        // 3. 替换 URL 中的服务名为实际 IP:Port
        String uri = request.uri().toString()
            .replace(clientName, chosen.getHost() + ":" + chosen.getPort());

        // 4. 发送实际的 HTTP 请求
        Request modifiedRequest = Request.create(
            request.method(),
            uri,
            request.headers(),
            request.body()
        );
        return delegate.execute(modifiedRequest, options);
    }
}

4.2 Feign 超时配置

# Feign 超时配置
feign:
  client:
    config:
      # 全局配置
      default:
        connect-timeout: 5000     # 连接超时:5秒
        read-timeout: 10000       # 读取超时:10秒
        logger-level: basic        # 日志级别
      # 针对特定服务配置
      user-service:
        connect-timeout: 3000     # user-service 专属配置
        read-timeout: 5000
  # 开启重试
  hystrix:
    enabled: true
// 默认超时时间
public class Request.Options {
    public Options() {
        this(10 * 1000, 60 * 1000);  // 默认连接超时 10s,读取超时 60s
    }
}

五、常见翻车现场 🔴

❌ 翻车点一:FeignClient 接口和调用的方法参数名丢失

// ❌ 错误:编译时参数名被擦除
@FeignClient(name = "user-service")
public interface UserClient {
    @RequestMapping(method = RequestMethod.GET, path = "/user/{id}")
    User getUser(@PathVariable Long id);  // 丢失参数名
}

// @PathVariable 需要指定 value
// 否则编译后没有参数名,Feign 无法知道参数对应哪个占位符

// ✅ 正确:明确指定参数名
@FeignClient(name = "user-service")
public interface UserClient {
    @RequestMapping(method = RequestMethod.GET, path = "/user/{id}")
    User getUser(@PathVariable("id") Long id);
}

❌ 翻车点二:Feign 和 Ribbon 默认重试机制

// 默认情况下,Ribbon 的重试策略:
// - 同一实例最多重试 0 次
// - 所有实例重试次数为 1

// ❌ 容易忽略:当服务短暂不可用时,Ribbon 会自动重试
// 可能导致:
// - 幂等接口被重复调用
// - 非幂等接口产生副作用

// ✅ 正确:明确配置重试策略
@Configuration
public class FeignConfig {
    @Bean
    public Retryer feignRetryer() {
        // 不重试
        return new Retryer.NeverRetryer();
        // 或者:最多重试 3 次,每次失败后间隔 100ms 再试
        return new Retryer.Default(100, 1000, 3);
    }
}

❌ 翻车点三:没有指定 contextId 导致 Bean 覆盖

// ❌ 错误:多个 FeignClient 指向同一个服务,Bean 名称冲突
@FeignClient(name = "user-service")
public interface UserClient {}

@FeignClient(name = "user-service")  // 同名,覆盖了上面的 Bean
public interface UserClient2 {}

// ✅ 正确:使用 contextId 区分
@FeignClient(name = "user-service", contextId = "userClient1")
public interface UserClient1 {}

@FeignClient(name = "user-service", contextId = "userClient2")
public interface UserClient2 {}
⚠️

当服务名相同但不同用途时(如调用同一个服务的不同接口),一定要指定不同的 contextId,否则 Spring 容器只会注册一个 Bean。

【面试官心理】

这道题我通常从动态代理开始,逐步深入到 Contract 解析、请求构建、负载均衡集成。能说出动态代理原理的占 40%,能讲清楚整个调用链路的占 20%,能回答重试机制和生产避坑的只有 10%。Feign 是 Spring Cloud 中最核心的组件之一,能把原理讲清楚的候选人,对 JDK 动态代理和 HTTP 客户端有较深的理解。