文章目录
- 一、声明式实现
-
- [1. 引入依赖](#1. 引入依赖)
- [2. 开启Feign客户端](#2. 开启Feign客户端)
- 3.定义Feign客户端接口
- [4. 注入并使用](#4. 注入并使用)
- 二、第三方API
-
- [1. 核心实现方式的对比](#1. 核心实现方式的对比)
- [2. 实战:以配置文件方式调用第三方API](#2. 实战:以配置文件方式调用第三方API)
- 三、日志配置
- [1. `Logger.Level` 的配置方式](#1.
Logger.Level的配置方式) -
- [配置方式一:在 `application.yml` 中配置(推荐)](#配置方式一:在
application.yml中配置(推荐)) - [配置方式二:通过 Java Bean 配置](#配置方式二:通过 Java Bean 配置)
- [配置方式一:在 `application.yml` 中配置(推荐)](#配置方式一:在
- [2. `logging.level` 的配置方式](#2.
logging.level的配置方式)
- 四、超时配置
-
- [1. 超时参数详解](#1. 超时参数详解)
- [2. 配置方式](#2. 配置方式)
-
- [方式一:通过 `application.yml` 配置(推荐)](#方式一:通过
application.yml配置(推荐)) -
- [1. 全局配置(对所有Feign客户端生效)](#1. 全局配置(对所有Feign客户端生效))
- 2.针对特定服务配置
- [方式二:通过 Java Bean 配置](#方式二:通过 Java Bean 配置)
- [方式三:通过 `@FeignClient` 注解的 `configuration` 属性](#方式三:通过
@FeignClient注解的configuration属性)
- [方式一:通过 `application.yml` 配置(推荐)](#方式一:通过
- 五、重试机制
-
-
- [⚠️ 重试的"坑"与黄金法则](#⚠️ 重试的“坑”与黄金法则)
-
- [1. 幂等性是第一原则](#1. 幂等性是第一原则)
- [2. 与超时配置的协同](#2. 与超时配置的协同)
- [3. 警惕"重试风暴"](#3. 警惕“重试风暴”)
-
- 六、拦截器
-
- [1. 核心原理](#1. 核心原理)
- [2. 基础使用:统一添加请求头](#2. 基础使用:统一添加请求头)
- [3. 针对不同服务做差异化处理](#3. 针对不同服务做差异化处理)
- [4. 对请求体进行拦截和修改](#4. 对请求体进行拦截和修改)
- [5. 控制拦截器的作用范围](#5. 控制拦截器的作用范围)
-
-
- [方式一:通过 `@FeignClient` 的 `configuration` 属性指定](#方式一:通过
@FeignClient的configuration属性指定) - [方式二:在拦截器内部通过 `template.feignTarget().name()` 做白名单过滤](#方式二:在拦截器内部通过
template.feignTarget().name()做白名单过滤)
- [方式一:通过 `@FeignClient` 的 `configuration` 属性指定](#方式一:通过
-
- 七、fallback机制(兜底返回)
-
-
- [1. 两种实现方式](#1. 两种实现方式)
- [2. 如何实现](#2. 如何实现)
-
一、声明式实现
1. 引入依赖
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2. 开启Feign客户端
java
@SpringBootApplication
@EnableFeignClients // 开启Feign客户端
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
3.定义Feign客户端接口
创建一个接口,并使用 @FeignClient 注解标注,其中 value 或 name 属性指定你要调用的目标服务的名称(需与注册中心的服务名一致)。接口内的方法定义应和提供方Controller的方法保持一致,使用SpringMVC注解来声明HTTP请求的细节。
java
@FeignClient(value = "userservice") // "userservice" 是目标服务在注册中心的名字
public interface UserClient {
@GetMapping("/user/{id}") // 请求路径
User findById(@PathVariable("id") Long id); // 参数和返回值类型
}
4. 注入并使用
在业务代码中,像使用普通Spring Bean一样,通过 @Autowired 或 @Resource 注入刚才定义的 UserClient,然后直接调用其方法即可完成远程调用。
java
@Service
public class OrderService {
@Resource
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// ... 查询订单逻辑
// 调用UserClient,就像调用本地方法一样
User user = userClient.findById(userId);
// ... 组装数据
return order;
}
}
二、第三方API
1. 核心实现方式的对比
| 实现方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 配置文件注入 (推荐) | 在 @FeignClient 的 url 属性中使用占位符,如 url = "${api.third-party.url}",然后在 application.yml 中为不同环境配置具体的值。 |
配置与代码分离,环境切换方便,无需修改代码。 | 需要额外维护配置文件。 |
| 硬编码URL (不推荐) | 直接在 @FeignClient 的 url 属性中写死地址,如 url = "https://api.example.com"。 |
简单直接,适合快速验证。 | 修改URL需要重新编译部署,灵活性差。 |
2. 实战:以配置文件方式调用第三方API
第一步:定义Feign客户端接口
在接口的 @FeignClient 注解中,name 属性可以随意填写(只要不与其他服务冲突即可),关键是使用 url 属性,并通过 ${...} 占位符引用配置文件中的值。如果第三方API有统一的路径前缀,可以用 path 属性指定。
java
// 使用 ${api.third-party.url} 从配置文件读取基础URL
@FeignClient(name = "thirdPartyUserClient", url = "${api.third-party.url}", path = "/user")
public interface ThirdPartyUserClient {
// 这里的路径会拼接到 url 和 path 之后,形成完整请求地址
@GetMapping("/{id}")
User getUserById(@PathVariable("id") Long id, @RequestHeader("Authorization") String auth);
@PostMapping("/query")
User queryUser(@RequestBody UserQueryRequest request);
}
第二步:在配置文件中配置URL
在 application.yml(或 application-dev.yml、application-prod.yml)中配置 api.third-party.url 的具体值。
xml
api:
third-party:
url: https://api.example.com # 替换成你的第三方API基础地址
三、日志配置
-
Logger.Level:告诉 OpenFeign "我要看多详细" 的日志(比如只看结果,还是连请求体都要看)。 -
logging.level:告诉 Spring Boot "我要给谁看" 日志(Feign 的日志默认是 DEBUG 级别,而 Spring Boot 默认只打印 INFO 及以上级别)。
1. Logger.Level 的配置方式
它的作用是设置日志的详细程度,有四种级别可选:
-
NONE:不记录任何信息(默认)。 -
BASIC:只记录请求方法、URL、响应状态码、执行时间。 -
HEADERS:在 BASIC 基础上,加上请求和响应的 Header 信息。 -
FULL:在 HEADERS 基础上,再记录请求和响应的 Body 及元数据。
配置方式一:在 application.yml 中配置(推荐)
yaml
feign:
client:
config:
# 对名为 'user-service' 的客户端生效
user-service:
loggerLevel: full
# 'default' 代表对所有 Feign 客户端生效
default:
loggerLevel: basic
配置方式二:通过 Java Bean 配置
java
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
如果是局部配置 (只对某个特定的 Feign 客户端生效),可以在配置类上不加 @Configuration,然后在 @FeignClient 中引用:
java
// 这个类没有 @Configuration 注解,作为局部配置
public class UserFeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
@FeignClient(name = "user-service", configuration = UserFeignConfig.class)
public interface UserClient {
// ...
}
2. logging.level 的配置方式
它的作用是开启指定包的日志输出 。因为 OpenFeign 的日志级别是 DEBUG,而 Spring Boot 默认的日志级别是 INFO,如果不手动设置,即使 Logger.Level 配得再高,日志也不会打印。
在 application.yml 中配置:
yaml
logging:
level:
# 方式一:将 FeignClient 接口所在的整个包设置为 DEBUG
com.example.feign: DEBUG
# 方式二:直接精确到某个 FeignClient 类(更精准)
com.example.feign.UserClient: DEBUG
注意 :这里配置的包路径,就是你项目中 @FeignClient 接口所在的包或类路径。
四、超时配置
1. 超时参数详解
| 参数 | 说明 | 建议值 |
|---|---|---|
| 连接超时 (ConnectTimeout) | 客户端与目标服务建立TCP连接的最大等待时间。 | 通常设为 1-3秒。网络环境较差可适当延长,但不宜过长。 |
| 读取超时 (ReadTimeout) | 客户端成功建立连接后,等待服务端返回响应数据的最大时间。 | 根据业务接口的平均响应时间 来定。普通接口建议 3-5秒 ,复杂接口可设 10-30秒。 |
⚠️ 重要 :超时配置必须与熔断超时配合。如果开启了熔断(如 Resilience4j/Sentinel),熔断超时必须 ≥ (连接超时 + 读取超时),否则熔断会先触发,导致 Feign 的超时配置失效。
2. 配置方式
方式一:通过 application.yml 配置(推荐)
1. 全局配置(对所有Feign客户端生效)
yaml
feign:
client:
config:
default: # 'default' 代表全局配置
connectTimeout: 3000 # 连接超时 3秒
readTimeout: 5000 # 读取超时 5秒
2.针对特定服务配置
yaml
feign:
client:
config:
user-service: # 针对名为 'user-service' 的服务
connectTimeout: 2000
readTimeout: 10000 # 该接口较慢,单独给 10秒
order-service: # 另一个服务单独配置
connectTimeout: 3000
readTimeout: 3000
方式二:通过 Java Bean 配置
java
@Configuration
public class FeignConfig {
@Bean
public Request.Options options() {
// 参数:connectTimeout, readTimeout, 时间单位
return new Request.Options(3000, 5000, TimeUnit.MILLISECONDS);
}
}
同样支持通过 configuration 属性做到局部生效 (方法同 Logger.Level 的局部配置)。
方式三:通过 @FeignClient 注解的 configuration 属性
为某一个特定的 FeignClient 单独定义配置类:
java
// 局部配置类(不加 @Configuration,防止被 Spring 扫描为全局)
public class UserFeignConfig {
@Bean
public Request.Options options() {
return new Request.Options(5000, 15000, TimeUnit.MILLISECONDS);
}
}
@FeignClient(name = "user-service", configuration = UserFeignConfig.class)
public interface UserClient {
// ...
}
五、重试机制
OpenFeign 的重试机制本质上是将故障转移的决策权交给客户端 ,通过 Retryer 组件来控制。但值得注意的是,重试是一把双刃剑------用得好能提升系统韧性,用得不好则可能引发级联故障或数据重复。
OpenFeign 默认不开启重试 ,但如果你手动配置了 Retryer Bean,它的默认实现 Retryer.Default 的行为是:最多尝试 5 次(即重试 4 次),初始间隔 100ms,之后间隔指数增长直至 1s。
它的工作流程是一个 while(true) 循环,只有当请求抛出 RetryableException(如连接超时)时,Retryer 才会决定是继续重试还是抛出异常终止循环。
方式一:使用默认实现,自定义参数
通过声明 Retryer.Default 的 Bean,并传入你期望的参数即可。
java
@Configuration
public class FeignConfig {
@Bean
public Retryer feignRetryer() {
// 参数:初始间隔(ms),最大间隔(ms),最大尝试次数(含首次)
return new Retryer.Default(100, 1000, 3);
}
}
方式二:实现自定义重试器
如果希望实现更精细的控制,比如固定间隔、特定异常才重试,可以自己实现 Retryer 接口。
java
public class CustomRetryer implements Retryer {
private final int maxAttempts;
private int attempt = 0;
private final long backoff;
public CustomRetryer(int maxAttempts, long backoff) {
this.maxAttempts = maxAttempts;
this.backoff = backoff;
}
@Override
public void continueOrPropagate(RetryableException e) {
if (++attempt >= maxAttempts) {
throw e; // 超出重试次数,抛出异常
}
try {
Thread.sleep(backoff); // 固定间隔
} catch (InterruptedException ignored) {}
}
@Override
public Retryer clone() {
return new CustomRetryer(maxAttempts, backoff);
}
}
然后在配置中指定这个类即可:
yaml
feign:
client:
config:
default:
retryer: com.example.CustomRetryer
⚠️ 重试的"坑"与黄金法则
1. 幂等性是第一原则
-
核心风险 :对非幂等 操作(如 POST 请求创建订单、支付扣款)开启重试,极有可能导致重复下单、重复扣款、库存超卖等严重数据问题。
-
最佳实践 :重试策略通常只应针对幂等 的 HTTP 方法开启,如
GET、PUT、DELETE。对POST请求开启重试,前提必须是业务接口本身已经做好了幂等性设计(如使用全局ID去重)。
2. 与超时配置的协同
重试通常由网络异常或超时触发。如果读超时(ReadTimeout)设置得过短,可能导致大量本可成功的请求因短暂超时而频繁重试,反而加剧系统负担。需要综合考量。
3. 警惕"重试风暴"
如果被调用的服务本身已处于高负载或网络抖动状态,客户端的重试会成倍增加其压力,可能成为压垮骆驼的最后一根稻草。配合熔断降级(如 Resilience4j、Sentinel)使用,是更稳妥的方案。
六、拦截器
OpenFeign的拦截器(RequestInterceptor)是一个非常强大的扩展点,它允许你在请求发出之前统一拦截、修改或增强HTTP请求。相比于在业务代码中手动添加请求头,拦截器是更优雅、更集中的解决方案。
1. 核心原理
RequestInterceptor 接口只有一个 apply(RequestTemplate template) 方法。Feign在构建每个HTTP请求时,会遍历所有注册的拦截器,依次执行 apply 方法,让你有机会修改 RequestTemplate(包含URL、请求头、请求体等信息)。
java
@FunctionalInterface
public interface RequestInterceptor {
void apply(RequestTemplate template);
}
2. 基础使用:统一添加请求头
最常见的场景是为所有Feign请求统一添加认证Token、TraceId等。
java
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从Spring Security上下文中获取Token(假设存在ThreadLocal中)
String token = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();
// 添加请求头
template.header("Authorization", "Bearer " + token);
template.header("X-Request-Id", UUID.randomUUID().toString());
template.header("Content-Type", "application/json");
}
}
只需将拦截器声明为Spring Bean(加 @Component 或在配置类中 @Bean 返回),它就会自动对所有Feign客户端生效。
3. 针对不同服务做差异化处理
如果你的项目调用了多个不同的第三方服务,需要为每个服务传递不同的Token或请求头,可以在拦截器内根据服务名进行区分。
java
@Component
public class DynamicFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 获取目标服务的名称(对应 @FeignClient 的 name 或 value 属性)
String serviceName = template.feignTarget().name();
if ("user-service".equals(serviceName)) {
// 调用内部微服务,传递用户Token
template.header("Authorization", getUserToken());
template.header("X-User-Id", getCurrentUserId());
} else if ("third-party-payment".equals(serviceName)) {
// 调用第三方支付API,传递API Key
template.header("X-API-Key", "your-api-key");
template.header("X-Signature", generateSignature(template));
} else if ("third-party-sms".equals(serviceName)) {
// 调用短信服务,传递AppId和Token
template.header("App-Id", "your-app-id");
template.header("Access-Token", getSmsToken());
}
}
}
4. 对请求体进行拦截和修改
某些场景下,你可能需要对请求体(Body)进行统一处理,比如:加密、签名、记录日志等。
java
@Component
public class BodyProcessInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 如果有请求体,且是POST/PUT请求
if (template.method() == HttpMethod.POST || template.method() == HttpMethod.PUT) {
// 获取原始请求体(byte[])
byte[] body = template.body();
if (body != null && body.length > 0) {
String originalBody = new String(body, StandardCharsets.UTF_8);
// 对请求体进行加密或签名(伪代码)
String encryptedBody = encrypt(originalBody);
// 重新设置请求体
template.body(encryptedBody.getBytes(StandardCharsets.UTF_8));
// 修改Content-Length头
template.header("Content-Length", String.valueOf(encryptedBody.length()));
// 添加签名头
template.header("X-Request-Sign", generateSign(encryptedBody));
}
}
}
}
5. 控制拦截器的作用范围
默认情况下,所有 RequestInterceptor Bean会对所有Feign客户端生效。如果需要只对特定客户端生效,有两种方式:
方式一:通过 @FeignClient 的 configuration 属性指定
java
// 局部配置类(不加 @Configuration,防止被全局扫描)
public class UserServiceFeignConfig {
@Bean
public RequestInterceptor userServiceInterceptor() {
return template -> {
template.header("X-Source", "user-service-call");
template.header("Authorization", "Bearer " + getSpecificToken());
};
}
}
@FeignClient(name = "user-service", configuration = UserServiceFeignConfig.class)
public interface UserClient {
// ...
}
方式二:在拦截器内部通过 template.feignTarget().name() 做白名单过滤
java
@Component
public class SelectiveInterceptor implements RequestInterceptor {
private static final Set<String> TARGET_SERVICES = Set.of("user-service", "order-service");
@Override
public void apply(RequestTemplate template) {
String serviceName = template.feignTarget().name();
// 只对白名单内的服务生效
if (!TARGET_SERVICES.contains(serviceName)) {
return;
}
template.header("X-Internal", "true");
}
}
七、fallback机制(兜底返回)
OpenFeign 的 Fallback 机制是为远程调用失败准备的"备用计划"。当服务不可用、超时或发生其他异常时,会执行你预先定义的兜底逻辑,返回一个安全、友好的结果,而不是直接把异常抛给用户,从而避免整个调用链路崩塌。
1. 两种实现方式
OpenFeign 提供了两种实现兜底的方式,它们在复杂度和能力上有明显区别。
| 特性 | fallback (基础版) |
fallbackFactory (生产推荐版) |
|---|---|---|
| 实现方式 | 实现被@FeignClient标记的接口。 |
实现FallbackFactory<T>接口,T为Feign接口类型。 |
| 获取异常 | ❌ 无法获取触发降级的异常对象。 | ✅ 可以在create(Throwable cause)方法中拿到具体的异常。 |
| 降级策略 | 一刀切,所有失败都返回同一个结果。 | 可以根据不同的异常类型(如超时、熔断、服务不可用)制定不同的降级策略。 |
| 排障能力 | 较弱,无法记录详细的错误日志,线上问题难以定位。 | 很强,可以记录完整的异常堆栈,便于监控和告警。 |
| 生产推荐度 | ❌ 不推荐,适合Demo或极简场景。 | ✅ 强烈推荐,是生产环境的标准实践。 |
2. 如何实现
无论哪种方式,核心步骤都类似:
-
开启熔断支持:首先需要在配置文件中开启对熔断降级的支持。
-
如果使用 Sentinel :
feign.sentinel.enabled=true。 -
如果使用 Resilience4j:需要引入相关依赖和配置。
-
-
编写兜底逻辑:
方式一:使用
fallback属性创建一个类,实现你的 Feign 接口,在方法中直接返回兜底数据。
方式二:使用
fallbackFactory属性(推荐)创建一个类实现
FallbackFactory接口,并在create方法中通过匿名内部类或Lambda表达式实现接口方法。这样就能拿到Throwable cause对象了。 -
在
@FeignClient中引用 :在注解中配置fallback或fallbackFactory属性,并指向你刚刚编写的类。最推荐的
FallbackFactory实现方式,它让你能清晰地记录失败原因。第一步:定义
FallbackFactory实现类
java
@Slf4j
@Component // 必须将其声明为Spring Bean
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
// 1. 关键步骤:打印完整的异常日志,这是排障的关键!
log.error("调用用户服务失败,异常原因:", cause);
// 2. 返回一个接口的匿名实现,用于提供降级后的默认行为
return new UserClient() {
@Override
public User queryById(Long id) {
// 3. 返回一个安全的默认值,避免业务中断
return new User(-1L, "默认用户", "服务不可用,返回默认信息");
}
};
}
}
第二步:在 @FeignClient 中配置
java
@FeignClient(
value = "user-service",
fallbackFactory = UserClientFallbackFactory.class // 引用上面定义的工厂类
)
public interface UserClient {
@GetMapping("/user/{id}")
User queryById(@PathVariable("id") Long id);
}