Skip to content

Spring Cloud OpenFeign 学习笔记

版本说明:本文基于 Spring Cloud 2023.x / Spring Boot 3.x / JDK 17 OpenFeign 是 Netflix Feign 的 Spring Cloud 增强版,完美集成了 LoadBalancer、Resilience4j 等


1. 声明式 HTTP 客户端概述

⭐ 1.1 什么是 OpenFeign

Feign 是一个声明式 HTTP 客户端。你只需要写一个接口 + 注解,Feign 会自动帮你生成实现、发起 HTTP 请求、解析响应。

对比传统方式

java
// 方式1:RestTemplate(传统,要拼 URL、解析 JSON,繁琐)
User user = restTemplate.getForObject(
    "http://user-service/api/users/1",
    User.class
);

// 方式2:Feign(声明式,像调用本地方法一样)
User user = userClient.getUser(1L);

一句话:Feign 把"HTTP 调用"变成了"Java 方法调用"。

1.2 Feign vs 其他 HTTP 客户端

对比项OpenFeignRestTemplateWebClient原生 HTTP
编程模型声明式接口模板调用响应式 Mono手动写
负载均衡✅ 内置 LB需加 @LoadBalanced需加 @LoadBalanced
熔断降级✅ 内置 Resilience4j❌ 手动❌ 手动
拦截器✅ RequestInterceptor✅ ExchangeFilter
超时✅ 注解/YAML 配置setConnectTimeouttimeout()手动
代码量少(只写接口)很多
异步⚠️ 配合 @Async⚠️ 手动✅ 原生支持

💡 1.3 Feign 架构原理简图

你写的 Feign 接口(UserClient)


   JDK 动态代理(Proxy)  ← 启动时扫描 @FeignClient 生成

    调用方法时触发


   RequestTemplate(封装请求)

    经过 RequestInterceptor 链


   LoadBalancer 选出服务实例


   HTTP 客户端发请求(默认 java.net.HttpURLConnection)
    (可替换为 OkHttp / Apache HttpClient)


   Decoder 解析 JSON → Java 对象


   返回给你

2. 快速入门

🚩 2.1 三步引入 Feign

第 1 步:加依赖

xml
<!-- 必选:OpenFeign 核心 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!-- 必选:负载均衡(Spring Cloud 2020+ 使用 LoadBalancer)-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!-- 必选:注册中心(Nacos 示例)-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

第 2 步:启动类加 @EnableFeignClients

java
package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 订单服务启动类
 * @EnableFeignClients 开启 Feign 客户端自动扫描
 */
@SpringBootApplication
@EnableFeignClients  // ← 这行加了才生效!
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

第 3 步:写接口、注入、调用

java
/** 定义 Feign 客户端接口(放在 client 包下) */
@FeignClient(name = "user-service")  // 目标服务名(注册中心里的名字)
public interface UserClient {

    @GetMapping("/api/users/{id}")
    User getUser(@PathVariable("id") Long id);  // ← @PathVariable 要指定值!
}

/** 在 Service 里注入使用 */
@Service
public class OrderService {

    private final UserClient userClient;

    // 直接注入(Spring 自动代理生成实现类)
    public OrderService(UserClient userClient) {
        this.userClient = userClient;
    }

    public Order createOrder(Long userId) {
        // 像调用本地方法一样发 HTTP 请求到 user-service
        User user = userClient.getUser(userId);
        // ... 业务逻辑
        return new Order(user.getId(), user.getName());
    }
}

⚠️ Spring Cloud 2022+ 变化@PathVariable@RequestParam 建议加上显式的值,如 @PathVariable("id"),否则在严格模式下可能找不到参数名。

2.2 与注册中心配合

yaml
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

# Feign 默认配置(可以不配,使用默认值)
feign:
  client:
    config:
      default:           # 全局默认配置(对所有 FeignClient 生效)
        connectTimeout: 5000
        readTimeout: 10000

3. @FeignClient 全属性详解

3.1 @FeignClient 所有属性

java
@FeignClient(
    // 必填:目标服务在注册中心的 name(lb:// 后面那个)
    name = "user-service",

    // 等同 name(老版本用法,现在推荐 name)
    // value = "user-service",

    // 直接指定 URL(不走注册中心,name 仍要填)
    // url = "http://localhost:8081",

    // 统一路径前缀(相当于所有方法路径都自动加 /api)
    path = "/api",

    // 指定配置类(可以对这个客户端单独配置超时、日志等)
    configuration = UserFeignConfig.class,

    // 降级处理类(FallbackFactory 优先于 fallback)
    fallbackFactory = UserClientFallbackFactory.class,
    // fallback = UserClientFallback.class,

    // 熔断后返回 null 而不是抛异常(Spring Cloud 2022+ 支持)
    fallbackOnly = false,

    // 同名服务区分(Nacos 多环境同名服务场景)
    contextId = "user-service-prod"
)
public interface UserClient { /* ... */ }

3.2 参数绑定全注解

java
@FeignClient(name = "user-service", path = "/api/users")
public interface UserClient {

    /** 路径参数 */
    @GetMapping("/{id}")
    User getUser(@PathVariable("id") Long id);

    /** 多个路径参数 */
    @GetMapping("/{parentId}/children/{childId}")
    List<User> getChildren(@PathVariable("parentId") Long p,
                           @PathVariable("childId") Long c);

    /** URL 查询参数(方法参数拼到 ? 后面)*/
    @GetMapping("/search")
    List<User> search(@RequestParam("keyword") String keyword,
                      @RequestParam(value = "page", defaultValue = "0") int page,
                      @RequestParam(value = "size", defaultValue = "10") int size);

    /** GET 请求传对象(⚠️ 注意:Feign 会把对象属性展开为查询参数)*/
    @GetMapping("/query")
    List<User> query(UserQuery query);  // 属性自动拼 ?name=xx&age=xx

    /** POST/PUT 请求体(JSON)*/
    @PostMapping
    User createUser(@RequestBody CreateUserRequest req);

    /** 设置请求头 */
    @GetMapping("/header-demo")
    List<User> headerDemo(
        @RequestHeader("X-Request-Id") String traceId,
        @RequestHeader(value = "X-Env", defaultValue = "prod") String env
    );

    /** 设置 Cookie */
    @GetMapping("/cookie-demo")
    String cookieDemo(@CookieValue("JSESSIONID") String sessionId);

    /** RequestOption(Feign 内置的请求选项,可覆盖超时等)*/
    @GetMapping("/option-demo")
    String optionDemo(Request.Options options);

    /** 返回文件流 */
    @GetMapping("/export")
    @SuppressWarnings("rawtypes")
    ResponseEntity<InputStreamResource> export();
}

/** 用于 GET 查询参数的 POJO */
public record UserQuery(
    String name,
    Integer ageMin,
    Integer ageMax
) {}

⚠️ 3.3 GET 请求传对象的注意事项

Feign 支持直接传 POJO,但仅限于 GET/HEAD 请求(Feign 会把字段展开为查询参数)。POST 请求要用 @RequestBody 并确保后端是 @RequestBody

java
// ✅ 正确:GET 传对象(Feign 自动把字段展开为 ?name=xx&age=xx)
@GetMapping("/list")
List<User> list(UserQuery query);

// ❌ 错误:GET 用 @RequestBody(HTTP 规范不允许 GET 有 body)
@GetMapping("/list")
List<User> list(@RequestBody UserQuery query);

// ✅ 正确:POST 传对象
@PostMapping("/search")
List<User> search(@RequestBody UserQuery query);

3.4 继承式 Feign(复用公共接口)

当多个 FeignClient 需要调用同一个后端服务时,可以抽出公共接口避免重复代码:

java
/** 公共接口:定义用户服务的 API 契约 */
@RequestMapping("/api/users")  // 注意:这里用 @RequestMapping,Feign 不直接用!
public interface UserApi {

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

    @PostMapping
    User createUser(@RequestBody CreateUserRequest req);
}

/** 服务实现侧:实现公共接口 */
@RestController
public class UserController implements UserApi {
    @Override
    public User getUser(Long id) { /* ... */ }
    @Override
    public User createUser(CreateUserRequest req) { /* ... */ }
}

/** 服务调用侧:FeignClient 继承公共接口,避免重复写方法 */
@FeignClient(name = "user-service", path = "/api/users")
public interface UserClient extends UserApi {
    // 直接继承所有方法,不需要重新写一遍!
}

💡 这种方式的好处:后端改了接口,FeignClient 会编译报错,提醒你更新。


4. 配置项大全

4.1 日志级别

⚠️ 关键点:只有 FULL 才会打印请求体/响应体!

yaml
feign:
  client:
    config:
      default:
        loggerLevel: BASIC    # NONE(默认)/ BASIC(请求行+状态码)/ HEADERS(+ 头)/ FULL(+ body)
java
// Java 代码方式设置
@Configuration
public class FeignConfig {
    @Bean
    public feign.Logger.Level feignLoggerLevel() {
        // FULL 会打印完整请求 URL、头、Body 和响应头、Body
        return feign.Logger.Level.FULL;
    }
}

⚠️ 额外一步:必须把 FeignClient 的包日志级别设为 DEBUG,否则看不到!

yaml
logging:
  level:
    # 设为 debug 才会打印 Feign 的 FULL 日志
    com.example.order.client.UserClient: debug
    # 或者整个包
    com.example.order.client: debug

4.2 超时配置

yaml
feign:
  client:
    config:
      default:
        # 建立 TCP 连接的超时(毫秒)
        connectTimeout: 5000
        # 连接建立后,等待后端响应数据的超时(毫秒)
        readTimeout: 10000

      # 可以对某个服务单独配置
      user-service:
        connectTimeout: 3000
        readTimeout: 5000
      order-service:
        connectTimeout: 2000
        readTimeout: 15000

超时公式

  • connectTimeout = 建立 TCP 连接所需时间(一般几百ms就够,内网建议 1000~3000)
  • readTimeout = 连接建立后,等后端返回数据的时间(要大于后端业务逻辑最坏时间)

⚠️ 坑点:如果后端处理时间是 8 秒,而 readTimeout 设为 5 秒 → 必然抛 FeignException$ReadTimedOut

4.3 编码解码(JSON/XML)

Spring Cloud OpenFeign 默认使用 Jackson(Spring Boot 自带)做 JSON 编解码,通常不用额外配置

java
// 需要自定义时(比如换 Gson)
@Configuration
public class FeignCodecConfig {

    @Bean
    public Encoder feignEncoder(ObjectMapper objectMapper) {
        return new SpringEncoder(() ->
            new HttpMessageConverters(
                new MappingJackson2HttpMessageConverter(objectMapper)
            )
        );
    }

    @Bean
    public Decoder feignDecoder(ObjectMapper objectMapper) {
        return new SpringDecoder(() ->
            new HttpMessageConverters(
                new MappingJackson2HttpMessageConverter(objectMapper)
            )
        );
    }
}

4.4 请求压缩

yaml
feign:
  compression:
    request:
      enabled: true
      # 压缩哪些类型
      mime-types: text/xml,application/xml,application/json
      # 多大才压缩(避免小包压缩反而更大)
      min-request-size: 2048
    response:
      enabled: true

4.5 重试策略

java
@Configuration
public class FeignRetryConfig {

    /**
     * 默认重试器(不推荐用,Resilience4j 更好控制)
     * 参数:period(第一次重试间隔 ms)、maxPeriod(最大间隔 ms)、attempts(重试次数)
     */
    @Bean
    public Retryer feignRetryer() {
        // 第一次等 100ms 重试,之后指数退避(1s、2s...),最多重试 3 次
        return new Retryer.Default(100, 1000, 3);
    }
}

💡 推荐:用 Resilience4j 的 @Retry 注解做重试,粒度更细,能跟熔断联动。

4.6 连接池优化(切换到 Apache HttpClient)

默认 Feign 用 java.net.HttpURLConnection(无连接池,每次新建连接),生产环境建议切换

xml
<!-- Apache HttpClient 5(新版推荐)-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
</dependency>

<!-- 或 OkHttp(另一选择)-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
yaml
feign:
  # 启用 Apache HttpClient(默认 false,即 HttpURLConnection)
  httpclient:
    hc5:
      enabled: true
  # 或者启用 OkHttp
  okhttp:
    enabled: true
java
@Configuration
public class FeignClientPoolConfig {

    /** 如果你需要自定义 Apache HttpClient 参数 */
    @Bean
    public org.apache.hc5.core.io.reactor.IOReactorConfig ioReactorConfig() {
        return org.apache.hc5.core.io.reactor.IOReactorConfig.custom()
                .setConnectTimeout(org.apache.hc5.core.util.Timeout.ofMilliseconds(3000))
                .build();
    }
}

5. 请求拦截器

5.1 RequestInterceptor 原理

Feign 发起每个 HTTP 请求之前,都会先执行所有 RequestInterceptorapply 方法。典型用途:Token 透传、TraceId 透传、签名。

执行顺序取决于 Spring 容器中 Bean 的注册顺序。

🚩 5.2 Token 透传拦截器(微服务认证链必备)

java
package com.example.order.feign.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * Feign 请求拦截器:Token 透传
 * 场景:用户浏览器 → 订单服务(携带 Token)→ 用户服务
 *       订单服务调用用户服务时,把浏览器的 Token 透传给用户服务
 *
 * ⚠️ 注意:在异步线程(@Async/CompletableFuture)中 RequestContextHolder 会为空!
 */
@Component
public class TokenRequestInterceptor implements RequestInterceptor {

    private static final String AUTH_HEADER = "Authorization";

    @Override
    public void apply(RequestTemplate template) {
        // 1. 从当前 HTTP 请求上下文取
        ServletRequestAttributes attributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        if (attributes == null) {
            // 没有 HTTP 请求上下文(如定时任务触发),跳过透传
            return;
        }

        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader(AUTH_HEADER);

        // 2. 透传给下游服务
        if (token != null && !token.isBlank()) {
            template.header(AUTH_HEADER, token);
        }
    }
}

🚩 5.3 TraceId 透传拦截器(分布式追踪必备)

java
package com.example.order.feign.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

/**
 * Feign 请求拦截器:TraceId 透传
 * 从 MDC(Mapped Diagnostic Context,Slf4j 的日志上下文)取 TraceId 并传给下游
 */
@Component
public class TraceIdRequestInterceptor implements RequestInterceptor {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";
    private static final String TRACE_ID_MDC_KEY = "traceId";  // 与日志框架 MDC 的 key 对应

    @Override
    public void apply(RequestTemplate template) {
        // 1. 从 MDC 取(网关/拦截器放进去的)
        String traceId = MDC.get(TRACE_ID_MDC_KEY);

        // 2. 如果 MDC 没有,尝试从 HTTP 请求头取
        if (traceId == null || traceId.isBlank()) {
            var attributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
            if (attributes instanceof org.springframework.web.context.request.ServletRequestAttributes sra) {
                traceId = sra.getRequest().getHeader(TRACE_ID_HEADER);
            }
        }

        // 3. 传给下游
        if (traceId != null && !traceId.isBlank()) {
            template.header(TRACE_ID_HEADER, traceId);
            // 同时写进本线程 MDC,确保 Feign 内部日志也带 TraceId
            MDC.put(TRACE_ID_MDC_KEY, traceId);
        }
    }
}

5.4 多个拦截器的顺序

java
/**
 * 用 @Order 控制多个拦截器的顺序
 * 值越小越先执行
 */
@Component
@Order(1)    // 先:TraceId
public class TraceIdRequestInterceptor implements RequestInterceptor { /* ... */ }

@Component
@Order(2)    // 后:Token
public class TokenRequestInterceptor implements RequestInterceptor { /* ... */ }

6. 熔断降级

💡 6.1 Fallback vs FallbackFactory

对比项fallbackfallbackFactory
参数无参构造即可构造方法接收 Throwable
知道异常原因✅ 知道为什么降级
推荐不推荐(不知道为何降级,排障难)推荐
Spring 官方态度文档中已不推荐使用官方推荐方式

6.2 Resilience4j 集成(Spring Cloud 2022+ 标准做法)

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
yaml
feign:
  # ⚠️ 必须开启!否则 Fallback 不会生效!
  circuitbreaker:
    enabled: true

# Resilience4j 熔断配置(推荐放在 bootstrap.yml 或 Nacos 配置中心)
resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 20
        slidingWindowType: COUNT_BASED
        failureRateThreshold: 50                 # 失败率 > 50% 熔断
        waitDurationInOpenState: 10s             # 熔断后 10s 再试
        permittedNumberOfCallsInHalfOpenState: 5 # 半开时最多 5 个探测请求
        minimumNumberOfCalls: 10                 # 最少 10 个请求才开始统计
        registerHealthIndicator: true
        exceptionPredicate: "#{ e instanceof java.io.IOException || e instanceof java.net.SocketTimeoutException }"
    instances:
      # 这里的名称格式为:FeignClient#方法名(参数类型列表)
      # 简单做法:用默认配置(不指定 instances)所有 FeignClient 共享 default
      user-service#UserClient#getUser(Long):
        baseConfig: default
        failureRateThreshold: 30                 # 用户服务更严格(30% 就熔断)

  # 超时控制(读超时,秒)
  timelimiter:
    configs:
      default:
        timeoutDuration: 5s
        cancelRunningFuture: true

🚩 6.3 FallbackFactory 完整实现

java
package com.example.order.feign.fallback;

import com.example.order.feign.UserClient;
import com.example.order.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;

/**
 * UserClient 的降级处理
 * FallbackFactory 能拿到 Throwable,方便定位降级原因
 */
@Slf4j
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

    @Override
    public UserClient create(Throwable cause) {
        // 返回一个"代理实现":每个方法都返回降级值
        return new UserClientFallback(cause);
    }

    /** 降级实现(独立类也可以,匿名内部类也可以)*/
    static class UserClientFallback implements UserClient {

        private final Throwable cause;

        UserClientFallback(Throwable cause) {
            this.cause = cause;
        }

        @Override
        public User getUser(Long id) {
            log.warn("[降级] getUser({}) 失败,原因:{}", id, cause.toString());
            // 返回一个默认值,或者抛业务异常让上层感知
            return new User(id, "默认用户-" + id, "default@example.com");
        }

        @Override
        public com.example.order.entity.Page<User> search(String keyword, int page, int size) {
            log.warn("[降级] search({}) 失败", keyword, cause);
            return new com.example.order.entity.Page<>(Collections.emptyList(), page, size, 0);
        }
    }
}

关联到 FeignClient:

java
@FeignClient(
    name = "user-service",
    fallbackFactory = UserClientFallbackFactory.class  // ← 这里绑定
)
public interface UserClient { /* ... */ }

6.4 Hystrix(旧版,仅兼容)

xml
<!-- 新版 Spring Cloud 已经移除 Hystrix,只有老版本才有 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
java
// 老版本写法(@HystrixCommand),现在建议直接用 Resilience4j
@HystrixCommand(fallbackMethod = "getUserFallback")
public User getUser(Long id) {
    return userClient.getUser(id);
}

public User getUserFallback(Long id) {
    return new User(id, "降级用户", "fallback@example.com");
}

7. 错误处理

7.1 ErrorDecoder(自定义错误翻译)

Feign 默认把非 2xx 的 HTTP 响应转为 FeignException。可以自定义映射为你自己的业务异常:

java
package com.example.order.feign.decoder;

import feign.FeignException;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 自定义 Feign 错误解码器
 * 把后端服务返回的业务错误码翻译成你的业务异常
 */
public class CustomErrorDecoder implements ErrorDecoder {

    private static final Logger log = LoggerFactory.getLogger(CustomErrorDecoder.class);
    private final ErrorDecoder defaultDecoder = new ErrorDecoder.Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        int status = response.status();
        String url = response.request().url();

        log.warn("Feign 调用失败 [{}] -> {} status={}", methodKey, url, status);

        return switch (status) {
            case 404 -> new BusinessException(404, "目标资源不存在:" + url);
            case 401, 403 -> new BusinessException(status, "认证/授权失败");
            case 429 -> new BusinessException(429, "被限流了,请稍后重试");
            case 503 -> new CircuitOpenException("目标服务熔断中");
            default -> defaultDecoder.decode(methodKey, response);  // 其他走默认
        };
    }
}

/** 注册到 Spring 容器 */
@Configuration
public class FeignErrorDecoderConfig {
    @Bean
    public ErrorDecoder customErrorDecoder() {
        return new CustomErrorDecoder();
    }
}

⚠️ Spring Cloud 封装 Feign 时内部已经做了一层 ErrorDecoder,某些版本可能需要用 Configuration 类注册才能对 FeignClient 单独生效。如果全局不生效,可以在 @FeignClient(configuration = Xxx.class) 的 Configuration 里 @Bean 注册。

7.2 重试与容错(Resilience4j @Retry)

java
@Service
public class OrderBizService {

    private final UserClient userClient;

    public OrderBizService(UserClient userClient) {
        this.userClient = userClient;
    }

    /**
     * Resilience4j 重试 + 熔断 注解
     * 注意:@Retry 和 @CircuitBreaker 的 name 要对应 resilience4j.instances 里的 key
     *
     * 推荐放在 FeignClient 对应的配置里统一管理,而不是每个方法都加
     */
    @Retry(name = "user-service", fallbackMethod = "getUserRetryFallback")
    @CircuitBreaker(name = "user-service")
    public User getUserWithRetry(Long userId) {
        return userClient.getUser(userId);
    }

    /** 重试耗尽后的降级方法(放在同一个类)*/
    public User getUserRetryFallback(Long userId, Exception e) {
        // 参数签名:与主方法参数一致,末尾多加一个 Throwable 参数
        return new User(userId, "重试耗尽降级用户", "fallback@example.com");
    }
}
yaml
resilience4j:
  retry:
    configs:
      default:
        maxAttempts: 3                          # 总共执行 3 次(含第一次)
        waitDuration: 500ms                    # 第一次重试前等 500ms
        enableExponentialBackoff: true         # 指数退避(500ms → 1s → 2s)
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - java.io.IOException
          - feign.FeignException$ReadTimedOut
    instances:
      user-service:
        baseConfig: default

8. 高级特性

🚩 8.1 多文件上传 Feign

java
/** Feign 客户端:多文件上传 */
@FeignClient(name = "file-service")
public interface FileClient {

    /**
     * 多文件上传
     * consumes 必须是 multipart/form-data
     */
    @PostMapping(value = "/api/files/upload",
                 consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
    List<FileUploadResult> uploadFiles(
            @RequestPart("files") List<org.springframework.web.multipart.MultipartFile> files,
            @RequestParam("bucket") String bucket,
            @RequestParam(value = "tags", required = false) List<String> tags
    );

    /** 单文件上传 */
    @PostMapping(value = "/api/files/upload-single",
                 consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
    FileUploadResult uploadSingle(
            @RequestPart("file") org.springframework.web.multipart.MultipartFile file,
            @RequestParam("bucket") String bucket
    );
}

/** 调用方 */
@Service
public class BusinessService {

    private final FileClient fileClient;

    public BusinessService(FileClient fileClient) {}

    public List<String> upload(org.springframework.web.multipart.MultipartFile[] files) {
        // MultipartFile[] 可以直接传给 Feign 的 List<MultipartFile>
        var results = fileClient.uploadFiles(List.of(files), "avatar-bucket", null);
        return results.stream().map(FileUploadResult::getUrl).toList();
    }
}

💡 加依赖(支持文件上传编解码):

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<!-- Feign 对 Spring MultipartFile 的支持在 spring-cloud-openfeign 中已包含 -->

8.2 文件下载(流式响应)

java
@FeignClient(name = "file-service")
public interface FileClient {

    /** 返回二进制流 */
    @GetMapping(value = "/api/files/{fileId}/download",
                produces = "application/octet-stream")
    feign.Response downloadFile(@PathVariable("fileId") String fileId);

    /** 或者用 Spring 的 ResponseEntity */
    @GetMapping(value = "/api/files/{fileId}/download2",
                produces = "application/octet-stream")
    org.springframework.http.ResponseEntity<org.springframework.core.io.InputStreamResource>
    downloadFile2(@PathVariable("fileId") String fileId);
}

8.3 响应式 Feign(返回 Mono/Flux)

xml
<!-- 需要加响应式支持 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
java
/**
 * Feign 直接返回 Mono/Flux(响应式)
 * 注意:这需要配合 WebFlux 或 Reactor Resilience4j 使用
 */
@FeignClient(name = "user-service")
public interface ReactiveUserClient {

    @GetMapping("/api/users/{id}")
    reactor.core.publisher.Mono<User> getUser(@PathVariable("id") Long id);

    @GetMapping("/api/users")
    reactor.core.publisher.Flux<User> listUsers();

    @PostMapping("/api/users")
    reactor.core.publisher.Mono<User> createUser(@RequestBody CreateUserRequest req);
}

/** 使用 */
@Service
public class ReactiveOrderService {

    private final ReactiveUserClient userClient;

    public ReactiveOrderService(ReactiveUserClient userClient) {}

    public reactor.core.publisher.Mono<Order> createOrderAsync(Long userId) {
        return userClient.getUser(userId)
                .map(user -> new Order(user.getId(), user.getName()))
                .switchIfEmpty(reactor.core.publisher.Mono.error(
                        new IllegalArgumentException("用户不存在")));
    }
}

💡 同步 vs 异步:Feign 默认是阻塞同步的。如果你的调用量大,建议:

  1. 同步 Feign + 业务代码中用 @Async 包一层(简单)
  2. 改用 WebClient(原生响应式,更高效但学习曲线陡)

8.4 多环境同名服务(contextId)

Nacos 中不同环境可能有同名服务,Feign 默认按 name 唯一查找,会报错:

java
/**
 * contextId 让同 name 的 FeignClient 在 Spring 容器中共存
 * name 用于负载均衡,contextId 用于 Spring 容器中的 Bean 区分
 */
@FeignClient(name = "user-service", contextId = "user-prod", url = "${user.prod.url:http://user-prod:8081}")
public interface ProdUserClient { /* ... */ }

@FeignClient(name = "user-service", contextId = "user-staging", url = "${user.staging.url:http://user-staging:8082}")
public interface StagingUserClient { /* ... */ }

8.5 多态参数(@JsonTypeInfo 等)

后端如果用了 @JsonTypeInfo 做多态反序列化,Feign 这边请求体也要匹配:

java
/** 后端定义:多个支付方式的抽象基类 */
public abstract class PaymentMethod {
    private String type;
    // ...
}

/** 具体实现 */
public class AlipayPayment extends PaymentMethod {
    private String appId;
    // ...
}

public class WechatPayment extends PaymentMethod {
    private String merchantId;
    // ...
}

/** Feign 调用:后端配置了 @JsonTypeInfo(use = Id.NAME, property = "type") */
@FeignClient(name = "payment-service")
public interface PaymentClient {

    /** 传具体子类,Feign 会按 Jackson 序列化(带 @class 或 type 字段)*/
    @PostMapping("/api/pay")
    PaymentResult pay(@RequestBody PaymentMethod method);
}

9. 负载均衡

9.1 LoadBalancer 集成(默认)

Spring Cloud 2020+ 默认用 Spring Cloud LoadBalancer(替代 Ribbon),Feign 自动继承:

java
// 无需额外配置,lb:// 协议自动使用 LoadBalancer
@FeignClient(name = "user-service")  // 自动 lb://user-service
public interface UserClient { /* ... */ }

9.2 自定义负载均衡策略

java
package com.example.order.feign.loadbalancer;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 加权随机负载均衡器
 * 根据实例元数据中的 weight 字段决定权重
 */
public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;
    private final org.springframework.cloud.loadbalancer.core.ObjectProvider<ServiceInstanceListSupplier> supplierProvider;

    public WeightedLoadBalancer(
            org.springframework.cloud.loadbalancer.core.ObjectProvider<ServiceInstanceListSupplier> supplierProvider,
            String serviceId) {
        this.supplierProvider = supplierProvider;
        this.serviceId = serviceId;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(org.springframework.cloud.client.loadbalancer.Request request) {
        return supplierProvider.getObject(request)
                .get(request)
                .next()
                .map(instances -> {
                    if (instances.isEmpty()) {
                        return new org.springframework.cloud.client.loadbalancer.EmptyResponse();
                    }

                    // 构建带权重的实例列表
                    List<ServiceInstance> weighted = new java.util.ArrayList<>();
                    for (ServiceInstance instance : instances) {
                        int weight = Integer.parseInt(
                                instance.getMetadata().getOrDefault("weight", "1"));
                        for (int i = 0; i < weight; i++) {
                            weighted.add(instance);
                        }
                    }

                    ServiceInstance selected = weighted.get(
                            ThreadLocalRandom.current().nextInt(weighted.size()));
                    return new org.springframework.cloud.client.loadbalancer.DefaultResponse(selected);
                });
    }
}

/** 注册:给某个服务指定自定义 LB */
@Configuration
public class LoadBalancerConfig {

    @Bean
    public ReactorServiceInstanceLoadBalancer weightedUserLoadBalancer(
            org.springframework.cloud.loadbalancer.core.ObjectProvider<ServiceInstanceListSupplier> supplier,
            Environment env) {
        String name = env.getProperty(
                org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME);
        return new WeightedLoadBalancer(supplier, name);
    }
}

/** Feign 指定用哪个 LB(可选) */
// @FeignClient(name = "user-service", configuration = LoadBalancerConfig.class)

9.3 粘性会话

yaml
spring:
  cloud:
    loadbalancer:
      # 保持一个客户端连接到同一个服务实例(基于 IP Hash 等)
      cache:
        ttl: 35s
        capacity: 256

10. 性能优化 💡

10.1 连接池优化

java
@Configuration
public class FeignPoolConfig {

    /** Apache HttpClient 连接池优化(推荐替代默认 HttpURLConnection) */
    @Bean
    public org.apache.hc5.core.io.reactor.IOReactorConfig ioReactorConfig() {
        return org.apache.hc5.core.io.reactor.IOReactorConfig.custom()
                .setConnectTimeout(org.apache.hc5.core.util.Timeout.ofMilliseconds(3000))
                .setIoThreadCount(Runtime.getRuntime().availableProcessors())
                .build();
    }

    /** 连接管理器:最大总连接数、每路由最大连接数 */
    @Bean
    public org.apache.hc5.client.config.ConnectionConfig connectionConfig() {
        return org.apache.hc5.client.config.ConnectionConfig.custom()
                .setSocketTimeout(org.apache.hc5.core.util.Timeout.ofMilliseconds(10000))
                .build();
    }
}
yaml
feign:
  httpclient:
    hc5:
      enabled: true
  client:
    config:
      default:
        connectTimeout: 3000
        readTimeout: 10000

10.2 异步调用(Feign + CompletableFuture)

Feign 本身是同步阻塞的。高并发场景建议用 CompletableFuture + @Async

java
@Service
public class OrderBizService {

    private final UserClient userClient;
    private final ProductClient productClient;

    public OrderBizService(UserClient userClient, ProductClient productClient) {}

    /**
     * 并行调用多个 Feign,减少总耗时
     * 假设每个 Feign 调用 200ms,顺序调 400ms,并行调 200ms
     */
    @org.springframework.scheduling.annotation.Async
    public java.util.concurrent.CompletableFuture<OrderDetail> assembleOrderDetailAsync(
            Long userId, Long productId) {

        var userFuture = java.util.concurrent.CompletableFuture.supplyAsync(
                () -> userClient.getUser(userId));

        var productFuture = java.util.concurrent.CompletableFuture.supplyAsync(
                () -> productClient.getProduct(productId));

        return userFuture.thenCombineAsync(productFuture, (user, product) -> {
            var detail = new OrderDetail();
            detail.setUser(user);
            detail.setProduct(product);
            return detail;
        });
    }
}

💡 别忘在启动类加 @EnableAsync

10.3 批量请求合并

java
/** 不好的做法:在循环里调 Feign(N+1 问题)*/
for (Long userId : userIds) {
    User u = userClient.getUser(userId);  // 10 个用户 = 10 次 HTTP
}

/** 好的做法:批量接口一次拿回来 */
List<User> users = userClient.listUsers(userIds);  // 1 次 HTTP 搞定

// 对应 Feign 接口
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/api/users/batch")
    List<User> listUsers(@RequestParam("ids") List<Long> ids);
}

10.4 缓存策略

java
@Service
public class CachedUserService {

    private final UserClient userClient;
    private final com.github.benmanes.caffeine.cache.Cache<Long, User> cache;

    public CachedUserService(UserClient userClient) {
        this.userClient = userClient;
        // Caffeine 高性能本地缓存
        this.cache = com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(java.time.Duration.ofMinutes(5))
                .build();
    }

    /** 优先从缓存取,缓存未命中才调 Feign */
    public User getUser(Long id) {
        return cache.get(id, key -> {
            try {
                return userClient.getUser(key);
            } catch (Exception e) {
                throw new RuntimeException("Feign 调用失败", e);
            }
        });
    }
}

11. 架构原理剖析 💡

11.1 动态代理原理

启动阶段(Spring 容器初始化):
  扫描所有 @FeignClient 注解
    → 用 JDK Proxy 生成实现类
    → 每个 FeignClient 接口对应一个 Proxy Bean
    → 注入到 Spring 容器中

调用阶段:
  userClient.getUser(1L)
    → 进入 Proxy.invoke()
    → 根据方法签名生成 RequestTemplate(URL、Header、Body)
    → 依次执行所有 RequestInterceptor.apply()
    → LoadBalancer 拿到 user-service 的一个实例地址
    → HTTP 客户端(HttpURLConnection / OkHttp / HC5)发请求
    → Decoder 解析响应 JSON → User 对象
    → 返回给你

11.2 调用链路

你的代码(userClient.getUser(1L))


JDK Proxy(FeignInvocationHandler)


SynchronousMethodHandler(Feign 默认同步执行器)


RequestTemplate.Factory#create()  ← 封装 URL / Header / Body


[RequestInterceptor 链]             ← Token透传、TraceId、签名


LoadBalancerClient(选实例)       ← 随机/轮询/加权


Client#execute()(发请求)         ← 默认 JDK HttpURLConnection


Decoder#decode()(解析响应)        ← SpringDecoder → Jackson


FeignException(异常)             ← 非 2xx 响应

11.3 Feign vs WebClient vs RestTemplate 调用开销

维度OpenFeignWebClientRestTemplate
编程复杂度低(写接口就行)高(响应式链式)
性能中(同步阻塞)高(异步非阻塞)中(同步阻塞)
内存占用低(线程池)低(EventLoop)低(线程池)
熔断降级✅ 内置 Resilience4j❌ 需手动❌ 需手动
拦截器✅ 成熟✅ 成熟❌ 手动

12. 最佳实践与踩坑 ⚠️

⚠️ 12.1 不要用 GET + @RequestBody

java
// ❌ HTTP 规范不允许 GET 请求有 Body,Feign 也不会发
@GetMapping("/list")
List<User> list(@RequestBody UserQuery query);

// ✅ GET 传对象(Feign 会把字段展开为查询参数)
@GetMapping("/list")
List<User> list(UserQuery query);

// ✅ 或者改成 POST(后端也要对应改成 @PostMapping + @RequestBody)
@PostMapping("/list")
List<User> list(@RequestBody UserQuery query);

⚠️ 12.2 readTimeout 要大于业务最坏时间

yaml
# 后端最坏要 8 秒,你设了 5 秒 → 必超时!
feign.client.config.default.readTimeout: 15000   # 保守点,给点缓冲

⚠️ 12.3 同名多服务问题(contextId)

java
// ❌ 两个 FeignClient 都指向 user-service,Spring 会覆盖或冲突
@FeignClient(name = "user-service")
public interface ProdUserClient {}

@FeignClient(name = "user-service")  // 会报 Bean 冲突
public interface StagingUserClient {}

// ✅ 加 contextId 区分
@FeignClient(name = "user-service", contextId = "user-prod")
public interface ProdUserClient {}

@FeignClient(name = "user-service", contextId = "user-staging", url = "${user-staging.url}")
public interface StagingUserClient {}

⚠️ 12.4 异步线程中 RequestContextHolder 为空

java
// ❌ @Async 或 CompletableFuture 中 RequestContextHolder.getRequestAttributes() = null
@Async
public CompletableFuture<User> getUserAsync(Long id) {
    // TokenRequestInterceptor 里用了 RequestContextHolder → 取不到 Token
    return CompletableFuture.completedFuture(userClient.getUser(id));
}

// ✅ 解决:主线程提前把 Token 取出来,用 final 变量传给异步块
public CompletableFuture<User> getUserAsync(Long id, String token) {
    return CompletableFuture.supplyAsync(() -> {
        // token 透传可以用 Feign 的 Header 注解直接塞
        // 或者在 RequestInterceptor 里支持从 ThreadLocal 或 MDC 取
        return userClient.getUser(id);
    });
}

// ✅ 更好的方案:在拦截器里同时从 MDC 取(异步线程 MDC 默认不会传播,需手动配置)

⚠️ 12.5 不要在 Feign 中做事务

java
// ❌ Feign 是 HTTP 调用,不走本地事务!@Transactional 对 Feign 调用无效
@Transactional
public void createOrder() {
    orderClient.create();   // HTTP 调用,远程服务的事务不跟你联动
    userClient.update();    // 两个 Feign 调用不在一个事务里
    // 后面一个失败,前面一个不会回滚!
}

// ✅ 分布式事务用 Saga / TCC / Seata AT 模式,或者接受最终一致性

⚠️ 12.6 泛型擦除问题

java
// ❌ Feign 返回 List<T> 或 ResponseEntity<List<T>> 时,Jackson 反序列化可能变成 LinkedHashMap
@GetMapping("/list")
List<User> listUsers();  // 运行时 List 只有 Object,Jackson 不知道是 User

// ✅ 用 ResponseEntity 或自定义返回包一层
@GetMapping("/list")
PageResponse<User> listUsers();  // 自定义 PageResponse<T>,有具体泛型信息

// ✅ 或者用 Spring 的 ParameterizedTypeReference(Feign 里较少用,推荐上面的方式)

⚠️ 12.7 默认重试的坑

yaml
# ❌ 默认 Retryer 可能会导致重复提交(POST 请求重试 = 下单两次)
# 解决:在 configuration 里禁用 Retryer
feign:
  client:
    config:
      default:
        # 设为 NO_RETRYER(Spring Cloud 2023+ 写法)
        retryable: false
java
@Configuration
public class NoRetryFeignConfig {
    @Bean
    public feign.Retryer feignRetryer() {
        // 不重试
        return new feign.Retryer.Default(0, 0, 1);
    }
}

13. 完整实战示例 🚩

订单服务调用用户服务 + 商品服务

yaml
# ===== application.yml =====
server:
  port: 8083

spring:
  application:
    name: order-service

  cloud:
    nacos:
      discovery:
        server-addr: ${NACOS_HOST:localhost}:8848

feign:
  circuitbreaker:
    enabled: true               # ⚠️ 必须开,否则 Fallback 不生效
  httpclient:
    hc5:
      enabled: true              # Apache HttpClient 5 替代默认 HttpURLConnection
  client:
    config:
      default:
        connectTimeout: 3000
        readTimeout: 10000
        loggerLevel: BASIC       # 生产环境用 BASIC,BASIC 够用
      user-service:
        connectTimeout: 2000
        readTimeout: 5000
        loggerLevel: FULL        # 调试时对特定服务开 FULL

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 20
        slidingWindowType: COUNT_BASED
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 5
        minimumNumberOfCalls: 5
        registerHealthIndicator: true
    instances:
      user-service:
        baseConfig: default
        failureRateThreshold: 30   # 用户服务更敏感

logging:
  level:
    com.example.order.client: debug   # FULL 日志要配合这个
java
// ===== 主启动类 =====
package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableFeignClients(basePackages = "com.example.order.client")  // 指定扫描包(更快)
@EnableAsync  // 启用 @Async 异步
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
java
// ===== Feign 客户端 =====
package com.example.order.client;

import com.example.order.client.fallback.UserClientFallbackFactory;
import com.example.order.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient(
        name = "user-service",
        path = "/api",
        fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {

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

    @GetMapping("/users/batch")
    List<User> listUsers(@RequestParam("ids") List<Long> ids);
}
java
// ===== Fallback =====
package com.example.order.client.fallback;

import com.example.order.client.UserClient;
import com.example.order.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Slf4j
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

    @Override
    public UserClient create(Throwable cause) {
        log.warn("[UserClient 降级] 原因: {}", cause.toString());
        return new UserClientFallback(cause);
    }

    static class UserClientFallback implements UserClient {
        private final Throwable cause;
        UserClientFallback(Throwable cause) { this.cause = cause; }

        @Override
        public User getUser(Long id) {
            log.warn("getUser({}) 失败: {}", id, cause);
            return new User(id, "默认用户-" + id, "default@example.com");
        }

        @Override
        public List<User> listUsers(List<Long> ids) {
            log.warn("listUsers({}) 失败: {}", ids, cause);
            return Collections.emptyList();
        }
    }
}
java
// ===== 拦截器 =====
package com.example.order.client.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.core.annotation.Order;

/** TraceId 透传 */
@Component
@Order(1)
public class TraceIdInterceptor implements RequestInterceptor {
    private static final String HEADER = "X-Trace-Id";

    @Override
    public void apply(RequestTemplate template) {
        String traceId = MDC.get("traceId");
        if (traceId == null) {
            var reqAttr = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
            if (reqAttr instanceof jakarta.servlet.ServletRequestAttributes sra) {
                traceId = sra.getRequest().getHeader(HEADER);
            }
        }
        if (traceId != null) {
            template.header(HEADER, traceId);
        }
    }
}

/** Token 透传 */
@Component
@Order(2)
public class TokenInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        var reqAttr = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
        if (reqAttr instanceof jakarta.servlet.ServletRequestAttributes sra) {
            String token = sra.getRequest().getHeader("Authorization");
            if (token != null && !token.isBlank()) {
                template.header("Authorization", token);
            }
        }
    }
}
java
// ===== 业务层 =====
package com.example.order.service;

import com.example.order.client.ProductClient;
import com.example.order.client.UserClient;
import com.example.order.entity.Order;
import com.example.order.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j
@Service
public class OrderService {

    private final UserClient userClient;
    private final ProductClient productClient;

    public OrderService(UserClient userClient, ProductClient productClient) {
        this.userClient = userClient;
        this.productClient = productClient;
    }

    /**
     * 创建订单(并行调用 + 熔断保护)
     */
    @Async  // 可以异步执行
    public CompletableFuture<Order> createOrderAsync(Long userId, Long productId, int quantity) {

        // 并行查用户 + 查商品
        var userFuture = CompletableFuture.supplyAsync(() -> {
            User user = userClient.getUser(userId);
            if (user == null || user.id() == null) {
                throw new IllegalArgumentException("用户不存在或服务降级");
            }
            return user;
        });

        var productFuture = CompletableFuture.supplyAsync(() ->
                productClient.getProduct(productId)
        );

        return userFuture.thenCombineAsync(productFuture, (user, product) -> {
            // 组装订单(略)
            log.info("创建订单: user={}, product={}", user.name(), product.name());
            return new Order();
        });
    }
}

14. 小结

┌──────────────────────────────────────────────────────┐
│           Spring Cloud OpenFeign 知识地图              │
├──────────────────────────────────────────────────────┤
│ ⭐ 核心概念                                           │
│   声明式接口 · 动态代理 · RequestInterceptor           │
├──────────────────────────────────────────────────────┤
│ 🎯 必掌握                                             │
│   三步引入 · @FeignClient · 参数绑定                   │
│   Resilience4j 熔断 · FallbackFactory · Token透传       │
├──────────────────────────────────────────────────────┤
│ 💡 重点难点                                           │
│   JDK 动态代理原理 · ErrorDecoder · @Retry            │
│   连接池优化(OkHttp/HC5)· 异步并行调用               │
├──────────────────────────────────────────────────────┤
│ ⚠️ 避坑要点                                           │
│   GET 不能 + @RequestBody                              │
│   必须 feign.circuitbreaker.enabled: true 才降级生效   │
│   contextId 解决同名多服务                              │
│   异步线程 RequestContextHolder 为空                    │
│   泛型擦除 List<T> 返回 LinkedHashMap                  │
├──────────────────────────────────────────────────────┤
│ 🚩 实战代码                                           │
│   TokenInterceptor · TraceIdInterceptor · Fallback     │
│   订单服务完整 application.yml + 所有类                │
└──────────────────────────────────────────────────────┘

一句话总结:Feign = 动态代理 + HTTP 客户端 + 负载均衡 + 熔断降级,把 HTTP 调用变成了像调本地方法一样简单。真正用好的关键是拦截器链熔断降级配置