Skip to content

Feign 声明式 HTTP 客户端原理

你有没有这种感觉:

调用一个 REST API,要写 RestTemplate、要处理 HttpClient、要管理连接池、还要考虑超时重试……

一个简单的 HTTP 请求,代码写了一堆。

Feign 就是来解决这个问题的——它的目标是:「像调用本地方法一样调用远程 HTTP 接口」。

今天,我们来彻底搞清楚 Feign 的工作原理。

Feign 的核心思想

Feign 的理念是声明式 REST 客户端

你只需要定义一个接口,Feign 会帮你完成所有的 HTTP 请求:

java
// 你写的代码
@FeignClient(name = "user-service")
public interface UserClient {

    @GetMapping("/users/{id}")
    User getUser(@PathVariable("id") Long id);

    @PostMapping("/users")
    User createUser(@RequestBody User user);
}

// Feign 帮你生成的代码(伪代码)
public class UserClientImpl implements UserClient {
    @Override
    public User getUser(Long id) {
        // 发送 GET /users/{id} 请求
        // 解析响应为 User 对象
        // 返回结果
    }
}

你只关心接口定义,不需要关心 HTTP 请求的细节。

Feign vs RestTemplate vs HttpClient

维度RestTemplateHttpClientFeign
代码风格字符串拼接 URL底层 API声明式接口
类型安全❌ 参数容易写错✅ 接口约束
可读性
配置复杂度
学习曲线

Feign 的工作原理

Feign 的工作流程可以分为三个阶段:

┌─────────────────────────────────────────────────────────┐
│                   Feign 工作流程                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1. 启动阶段(Bootstrap)                               │
│     ┌─────────────────────────────────────────────┐   │
│     │  扫描 @FeignClient 注解                    │   │
│     │  生成 JDK 动态代理对象                       │   │
│     │  解析 @RequestLine、@Header 等注解          │   │
│     └─────────────────────────────────────────────┘   │
│                         ↓                               │
│  2. 请求阶段(Invocation)                              │
│     ┌─────────────────────────────────────────────┐   │
│     │  MethodHandler 拦截方法调用                  │   │
│     │  将注解翻译为 HTTP 请求                      │   │
│     │  设置 Header、参数、URL                      │   │
│     └─────────────────────────────────────────────┘   │
│                         ↓                               │
│  3. 响应阶段(Response)                                │
│     ┌─────────────────────────────────────────────┐   │
│     │  Client 发送 HTTP 请求                       │   │
│     │  Decoder 解析响应                           │   │
│     │  返回业务对象                               │   │
│     └─────────────────────────────────────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘

1. 启动阶段:动态代理生成

Spring Boot 启动时,FeignClientsRegistrar 会扫描所有 @FeignClient 注解:

java
public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {

        // 扫描 @EnableFeignClients 指定的包
        registerDefaultConfiguration(metadata, registry);

        // 扫描所有 @FeignClient 注解的接口
        registerFeignClients(metadata, registry);
    }
}

注册过程:

java
private void registerFeignClient(
        BeanDefinitionRegistry registry,
        AnnotationMetadata annotationMetadata,
        Map<String, Object> attrs) {

    // 创建 FeignClient 的 BeanDefinition
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);

    // 添加属性:name、url、fallback 等
    definition.addPropertyValue("name", attrs.get("name"));
    definition.addPropertyValue("url", attrs.get("url"));
    definition.addPropertyValue("fallback", attrs.get("fallback"));

    // 注册为 Bean
    registry.registerBeanDefinition(
        attrs.get("name").toString() + "FeignClient",
        definition.getBeanDefinition()
    );
}

2. 请求阶段:MethodHandler 处理

当调用 Feign Client 方法时,InvocationHandler 会拦截调用:

java
public class ReflectiveFeign extends Feign {

    @Override
    public <T> T newInstance(Target<T> target) {
        // 解析接口上的方法
        Map<String, MethodHandler> nameToHandler = parseMethods(target);

        // 创建 JDK 动态代理
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                // 获取方法对应的 Handler
                MethodHandler methodHandler = nameToHandler.get(method.getName());
                // 执行调用
                return methodHandler.invoke(args);
            }
        };

        return (T) Proxy.newProxyInstance(
            target.type().getClassLoader(),
            new Class[]{target.type()},
            handler
        );
    }
}

3. 请求构建:RequestTemplate

MethodHandler 会将注解翻译成 RequestTemplate

java
public class SynchronousMethodHandler implements MethodHandler {

    @Override
    public Object invoke(Object[] argv) throws Throwable {
        // 1. 构建请求
        RequestTemplate template = buildTemplate(argv);

        // 2. 发送请求
        Request request = Request.create(
            HttpMethod.GET,
            template.toUri().toString(),
            template.headers(),
            Request.Body.empty()
        );

        // 3. 执行请求
        Response response = client.execute(request, options);

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

    private RequestTemplate buildTemplate(Object[] argv) {
        RequestTemplate template = RequestTemplate.from(metadata.template());

        // 处理 @Param 注解
        for (Map.Entry<String, Object> entry : argvMap.entrySet()) {
            String name = entry.getKey();
            Object value = entry.getValue();
            template.resolve(Collections.singletonMap(name, value));
        }

        return template;
    }
}

Feign 的核心组件

Feign 的可扩展性来自于它的核心组件:

组件作用默认实现
Contract解析注解(@RequestLine、@Headers)RibbonContract
Client发送 HTTP 请求LoadBalancerFeignClient
Encoder请求体编码JacksonEncoder
Decoder响应体解码JacksonDecoder
Logger日志记录Slf4jLogger
Retryer重试策略Retryer.NEVER_RETRY

组件的 SPI 扩展

java
// 定义接口
public interface Encoder {
    void encode(Object object, Type bodyType, RequestTemplate template);
}

// SPI 配置
// META-INF/services/feign.Logger
com.example.MyEncoder = com.example.CustomEncoder

替换默认组件

java
@Configuration
public class FeignConfig {

    @Bean
    public Decoder decoder() {
        return new JacksonDecoder(
            new ObjectMapper().registerModule(new JavaTimeModule())
        );
    }

    @Bean
    public Encoder encoder() {
        return new JacksonEncoder();
    }

    @Bean
    public Retryer retryer() {
        // 重试配置:重试 3 次,间隔 100ms
        return new Retryer.Default(100, 1000, 3);
    }
}

Feign 的请求拦截

Feign 支持通过 RequestInterceptor 拦截所有请求:

java
@Configuration
public class FeignInterceptorConfig {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                // 添加 Header
                template.header("X-Request-Id", UUID.randomUUID().toString());
                template.header("Authorization",
                    "Bearer " + getToken());

                // 添加全局参数
                template.query("tenantId", getTenantId());
            }
        };
    }
}

完整示例

java
// 1. 定义 Feign Client
@FeignClient(
    name = "user-service",
    url = "http://user-service:8080",
    configuration = FeignConfig.class,
    fallback = UserClientFallback.class
)
public interface UserClient {

    @GetMapping("/users/{id}")
    User getUser(@PathVariable("id") Long id);

    @PostMapping("/users")
    User createUser(@RequestBody User user);

    @GetMapping("/users")
    List<User> getUsers(@RequestParam("status") String status);
}

// 2. 定义 Fallback(容错)
@Component
public class UserClientFallback implements UserClient {

    @Override
    public User getUser(Long id) {
        // 服务降级:返回默认用户
        return new User(id, "Default User");
    }

    @Override
    public User createUser(User user) {
        throw new ServiceUnavailableException("Service is unavailable");
    }

    @Override
    public List<User> getUsers(String status) {
        return Collections.emptyList();
    }
}

// 3. 使用
@RestController
public class OrderController {

    @Autowired
    private UserClient userClient;

    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        // 像调用本地方法一样调用远程服务
        User user = userClient.getUser(id);
        return orderService.findById(id, user);
    }
}

Feign 的 HTTP 客户端

Feign 支持多种 HTTP 客户端,默认使用 URLConnection(每次请求创建新连接)。

推荐使用 OkHttpApache HttpClient

OkHttp 配置

xml
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
java
@Configuration
public class OkHttpConfig {

    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient.Builder()
            .connectTimeout(5, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .connectionPool(new ConnectionPool(
                10,      // 最大空闲连接数
                5,       // 保持时间
                TimeUnit.MINUTES
            ))
            .build();
    }
}

面试追问方向

  • Feign 为什么使用动态代理而不是字节码生成?
  • Feign 和 Ribbon 是什么关系?Feign 怎么实现负载均衡的?
  • 如何让 Feign 支持 OAuth 2.0 认证?
  • Feign 的超时配置是怎么生效的?ConnectTimeout 和 ReadTimeout 有什么区别?

总结

Feign 的设计哲学是「约定优于配置」

┌─────────────────────────────────────────────────────────┐
│                   Feign 调用模型                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   你写的代码:                                           │
│   ┌─────────────────────────────────────────────┐     │
│   │ @FeignClient(name = "user-service")        │     │
│   │ interface UserClient {                      │     │
│   │   @GetMapping("/users/{id}")                │     │
│   │   User getUser(@PathVariable Long id);      │     │
│   │ }                                           │     │
│   └─────────────────────────────────────────────┘     │
│                         ↓                               │
│   Feign 帮你做的:                                       │
│   ┌─────────────────────────────────────────────┐     │
│   │ 1. 解析注解 → RequestTemplate               │     │
│   │ 2. 负载均衡 → Ribbon 选择实例                │     │
│   │ 3. 发送请求 → HTTP Client                  │     │
│   │ 4. 解析响应 → Decoder 解码                 │     │
│   │ 5. 返回结果 → 业务对象                      │     │
│   └─────────────────────────────────────────────┘     │
│                                                         │
└─────────────────────────────────────────────────────────┘

Feign 让 HTTP 客户端变得像本地接口一样优雅,真正实现了「像调用本地方法一样调用远程服务」。

基于 VitePress 构建