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 请求、解析响应。
对比传统方式:
// 方式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 客户端
| 对比项 | OpenFeign | RestTemplate | WebClient | 原生 HTTP |
|---|---|---|---|---|
| 编程模型 | 声明式接口 | 模板调用 | 响应式 Mono | 手动写 |
| 负载均衡 | ✅ 内置 LB | 需加 @LoadBalanced | 需加 @LoadBalanced | ❌ |
| 熔断降级 | ✅ 内置 Resilience4j | ❌ 手动 | ❌ 手动 | ❌ |
| 拦截器 | ✅ RequestInterceptor | ❌ | ✅ ExchangeFilter | ❌ |
| 超时 | ✅ 注解/YAML 配置 | setConnectTimeout | timeout() | 手动 |
| 代码量 | 少(只写接口) | 中 | 多 | 很多 |
| 异步 | ⚠️ 配合 @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 步:加依赖
<!-- 必选: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
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 步:写接口、注入、调用
/** 定义 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 与注册中心配合
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
# Feign 默认配置(可以不配,使用默认值)
feign:
client:
config:
default: # 全局默认配置(对所有 FeignClient 生效)
connectTimeout: 5000
readTimeout: 100003. @FeignClient 全属性详解
3.1 @FeignClient 所有属性
@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 参数绑定全注解
@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。
// ✅ 正确: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 需要调用同一个后端服务时,可以抽出公共接口避免重复代码:
/** 公共接口:定义用户服务的 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 才会打印请求体/响应体!
feign:
client:
config:
default:
loggerLevel: BASIC # NONE(默认)/ BASIC(请求行+状态码)/ HEADERS(+ 头)/ FULL(+ body)// Java 代码方式设置
@Configuration
public class FeignConfig {
@Bean
public feign.Logger.Level feignLoggerLevel() {
// FULL 会打印完整请求 URL、头、Body 和响应头、Body
return feign.Logger.Level.FULL;
}
}⚠️ 额外一步:必须把 FeignClient 的包日志级别设为 DEBUG,否则看不到!
logging:
level:
# 设为 debug 才会打印 Feign 的 FULL 日志
com.example.order.client.UserClient: debug
# 或者整个包
com.example.order.client: debug4.2 超时配置
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 编解码,通常不用额外配置。
// 需要自定义时(比如换 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 请求压缩
feign:
compression:
request:
enabled: true
# 压缩哪些类型
mime-types: text/xml,application/xml,application/json
# 多大才压缩(避免小包压缩反而更大)
min-request-size: 2048
response:
enabled: true4.5 重试策略
@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(无连接池,每次新建连接),生产环境建议切换:
<!-- 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>feign:
# 启用 Apache HttpClient(默认 false,即 HttpURLConnection)
httpclient:
hc5:
enabled: true
# 或者启用 OkHttp
okhttp:
enabled: true@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 请求之前,都会先执行所有 RequestInterceptor 的 apply 方法。典型用途:Token 透传、TraceId 透传、签名。
执行顺序取决于 Spring 容器中 Bean 的注册顺序。
🚩 5.2 Token 透传拦截器(微服务认证链必备)
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 透传拦截器(分布式追踪必备)
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 多个拦截器的顺序
/**
* 用 @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
| 对比项 | fallback | fallbackFactory |
|---|---|---|
| 参数 | 无参构造即可 | 构造方法接收 Throwable |
| 知道异常原因 | ❌ | ✅ 知道为什么降级 |
| 推荐 | 不推荐(不知道为何降级,排障难) | 推荐 |
| Spring 官方态度 | 文档中已不推荐使用 | 官方推荐方式 |
6.2 Resilience4j 集成(Spring Cloud 2022+ 标准做法)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>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 完整实现
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:
@FeignClient(
name = "user-service",
fallbackFactory = UserClientFallbackFactory.class // ← 这里绑定
)
public interface UserClient { /* ... */ }6.4 Hystrix(旧版,仅兼容)
<!-- 新版 Spring Cloud 已经移除 Hystrix,只有老版本才有 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>// 老版本写法(@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。可以自定义映射为你自己的业务异常:
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)
@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");
}
}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: default8. 高级特性
🚩 8.1 多文件上传 Feign
/** 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 文件下载(流式响应)
@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)
<!-- 需要加响应式支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>/**
* 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 默认是阻塞同步的。如果你的调用量大,建议:
- 同步 Feign + 业务代码中用
@Async包一层(简单)- 改用 WebClient(原生响应式,更高效但学习曲线陡)
8.4 多环境同名服务(contextId)
Nacos 中不同环境可能有同名服务,Feign 默认按 name 唯一查找,会报错:
/**
* 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 这边请求体也要匹配:
/** 后端定义:多个支付方式的抽象基类 */
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 自动继承:
// 无需额外配置,lb:// 协议自动使用 LoadBalancer
@FeignClient(name = "user-service") // 自动 lb://user-service
public interface UserClient { /* ... */ }9.2 自定义负载均衡策略
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 粘性会话
spring:
cloud:
loadbalancer:
# 保持一个客户端连接到同一个服务实例(基于 IP Hash 等)
cache:
ttl: 35s
capacity: 25610. 性能优化 💡
10.1 连接池优化
@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();
}
}feign:
httpclient:
hc5:
enabled: true
client:
config:
default:
connectTimeout: 3000
readTimeout: 1000010.2 异步调用(Feign + CompletableFuture)
Feign 本身是同步阻塞的。高并发场景建议用 CompletableFuture + @Async:
@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 批量请求合并
/** 不好的做法:在循环里调 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 缓存策略
@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 调用开销
| 维度 | OpenFeign | WebClient | RestTemplate |
|---|---|---|---|
| 编程复杂度 | 低(写接口就行) | 高(响应式链式) | 中 |
| 性能 | 中(同步阻塞) | 高(异步非阻塞) | 中(同步阻塞) |
| 内存占用 | 低(线程池) | 低(EventLoop) | 低(线程池) |
| 熔断降级 | ✅ 内置 Resilience4j | ❌ 需手动 | ❌ 需手动 |
| 拦截器 | ✅ 成熟 | ✅ 成熟 | ❌ 手动 |
12. 最佳实践与踩坑 ⚠️
⚠️ 12.1 不要用 GET + @RequestBody
// ❌ 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 要大于业务最坏时间
# 后端最坏要 8 秒,你设了 5 秒 → 必超时!
feign.client.config.default.readTimeout: 15000 # 保守点,给点缓冲⚠️ 12.3 同名多服务问题(contextId)
// ❌ 两个 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 为空
// ❌ @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 中做事务
// ❌ Feign 是 HTTP 调用,不走本地事务!@Transactional 对 Feign 调用无效
@Transactional
public void createOrder() {
orderClient.create(); // HTTP 调用,远程服务的事务不跟你联动
userClient.update(); // 两个 Feign 调用不在一个事务里
// 后面一个失败,前面一个不会回滚!
}
// ✅ 分布式事务用 Saga / TCC / Seata AT 模式,或者接受最终一致性⚠️ 12.6 泛型擦除问题
// ❌ 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 默认重试的坑
# ❌ 默认 Retryer 可能会导致重复提交(POST 请求重试 = 下单两次)
# 解决:在 configuration 里禁用 Retryer
feign:
client:
config:
default:
# 设为 NO_RETRYER(Spring Cloud 2023+ 写法)
retryable: false@Configuration
public class NoRetryFeignConfig {
@Bean
public feign.Retryer feignRetryer() {
// 不重试
return new feign.Retryer.Default(0, 0, 1);
}
}13. 完整实战示例 🚩
订单服务调用用户服务 + 商品服务
# ===== 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 日志要配合这个// ===== 主启动类 =====
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);
}
}// ===== 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);
}// ===== 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();
}
}
}// ===== 拦截器 =====
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);
}
}
}
}// ===== 业务层 =====
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 调用变成了像调本地方法一样简单。真正用好的关键是拦截器链和熔断降级配置。