🎯 本文适合人群 :SpringCloud 初学者、微服务开发者
⏱️ 阅读时长 :30 分钟
📌 你将收获:掌握 OpenFeign 声明式调用、负载均衡策略、超时重试配置
📖 目录
- [一、OpenFeign 快速认识](#一、OpenFeign 快速认识)
- [二、OpenFeign 快速入门](#二、OpenFeign 快速入门)
- [三、OpenFeign 参数传递](#三、OpenFeign 参数传递)
- 四、负载均衡策略
- 五、超时与重试配置
- 六、日志配置
- 七、拦截器与请求头传递
- [八、OpenFeign 高级特性](#八、OpenFeign 高级特性)
- 九、最佳实践
- 十、常见面试题
一、OpenFeign 快速认识
1.1 什么是 OpenFeign?
OpenFeign = 声明式的 HTTP 客户端
核心思想:
- 定义接口 + 注解
- OpenFeign 自动生成实现类
- 像调用本地方法一样调用远程服务
1.2 为什么需要 OpenFeign?
传统方式(RestTemplate):
java
// ❌ 繁琐的方式
@Autowired
private RestTemplate restTemplate;
public User getUserById(Long userId) {
// 1. 拼接 URL
String url = "http://user-service/user/" + userId;
// 2. 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer token");
HttpEntity<String> entity = new HttpEntity<>(headers);
// 3. 发起请求
ResponseEntity<User> response = restTemplate.exchange(
url, HttpMethod.GET, entity, User.class
);
// 4. 获取结果
return response.getBody();
}
使用 OpenFeign:
java
// ✅ 简洁的方式
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long userId);
}
// 使用
@Autowired
private UserClient userClient;
User user = userClient.getUserById(1001L); // 一行搞定!
对比:
| 维度 | RestTemplate | OpenFeign |
|---|---|---|
| 代码量 | 多 | 少 |
| 可读性 | 差 | 好 |
| 负载均衡 | 需手动配置 | 自动集成 |
| 熔断降级 | 需手动实现 | 自动集成 |
| 维护成本 | 高 | 低 |
1.3 OpenFeign 工作原理
┌──────────────────────────────────────────────────┐
│ OpenFeign 调用流程 │
├──────────────────────────────────────────────────┤
│ │
│ ① 定义 FeignClient 接口 │
│ @FeignClient("user-service") │
│ interface UserClient { ... } │
│ ↓ │
│ ② Spring 启动时扫描接口 │
│ FeignClientFactoryBean 创建代理对象 │
│ ↓ │
│ ③ 调用接口方法 │
│ userClient.getUserById(1001L) │
│ ↓ │
│ ④ 代理对象拦截调用 │
│ ReflectiveFeign.invoke() │
│ ↓ │
│ ⑤ 解析注解,构造请求 │
│ - @GetMapping → GET 方法 │
│ - @PathVariable → 路径参数 │
│ - 服务名 → 从 Nacos 获取实例列表 │
│ ↓ │
│ ⑥ LoadBalancer 选择实例 │
│ [192.168.1.101:8081, 192.168.1.102:8081] │
│ 选中:192.168.1.101:8081 │
│ ↓ │
│ ⑦ 发送 HTTP 请求 │
│ GET http://192.168.1.101:8081/user/1001 │
│ ↓ │
│ ⑧ 反序列化响应 │
│ JSON → User 对象 │
│ ↓ │
│ ⑨ 返回结果 │
│ return user; │
└──────────────────────────────────────────────────┘
二、OpenFeign 快速入门
2.1 环境搭建
项目结构:
microservice-demo/
├── user-service/ # 服务提供者(用户服务)
├── order-service/ # 服务消费者(订单服务)
└── common/ # 公共模块(实体类)
2.2 服务提供者(用户服务)
步骤 1:pom.xml
xml
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
步骤 2:application.yml
yaml
server:
port: 8081
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
步骤 3:实体类
java
package com.example.userservice.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户实体
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String username;
private Integer age;
private String email;
}
步骤 4:控制器
java
package com.example.userservice.controller;
import com.example.userservice.entity.User;
import org.springframework.web.bind.annotation.*;
/**
* 用户控制器
*/
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 根据 ID 查询用户
*/
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
// 模拟查询数据库
return new User(id, "用户" + id, 25, "user" + id + "@example.com");
}
/**
* 根据用户名查询用户
*/
@GetMapping("/by-name")
public User getUserByName(@RequestParam String username) {
// 模拟查询
return new User(1001L, username, 30, username + "@example.com");
}
/**
* 创建用户
*/
@PostMapping
public User createUser(@RequestBody User user) {
// 模拟保存到数据库
user.setId(System.currentTimeMillis());
return user;
}
}
2.3 服务消费者(订单服务)
步骤 1:pom.xml
xml
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- LoadBalancer(负载均衡) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
步骤 2:application.yml
yaml
server:
port: 8082
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
步骤 3:启动类
java
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 订单服务启动类
*
* @EnableFeignClients:开启 Feign 客户端扫描
* - 扫描 @FeignClient 注解
* - 生成代理对象
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 开启 Feign 客户端
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
步骤 4:定义 Feign 客户端
java
package com.example.orderservice.client;
import com.example.orderservice.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* 用户服务 Feign 客户端
*
* @FeignClient:声明这是一个 Feign 客户端
* - name/value:服务名(必须)
* - path:统一路径前缀(可选)
*/
@FeignClient(name = "user-service", path = "/user")
public interface UserClient {
/**
* 根据 ID 查询用户
*
* @PathVariable:路径参数
* - value/name:参数名(必须与接口定义一致)
*/
@GetMapping("/{id}")
User getUserById(@PathVariable("id") Long id);
/**
* 根据用户名查询用户
*
* @RequestParam:查询参数
*/
@GetMapping("/by-name")
User getUserByName(@RequestParam("username") String username);
/**
* 创建用户
*
* @RequestBody:请求体参数
*/
@PostMapping
User createUser(@RequestBody User user);
}
步骤 5:使用 Feign 客户端
java
package com.example.orderservice.controller;
import com.example.orderservice.client.UserClient;
import com.example.orderservice.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 订单控制器
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private UserClient userClient; // 注入 Feign 客户端
/**
* 创建订单
*
* 业务流程:
* 1. 调用用户服务,验证用户
* 2. 创建订单
*/
@PostMapping("/create")
public String createOrder(@RequestParam Long userId) {
// ① 调用用户服务(像调用本地方法一样简单!)
User user = userClient.getUserById(userId);
// ② 验证用户
if (user == null) {
return "用户不存在,订单创建失败";
}
// ③ 创建订单(模拟)
String orderId = "ORDER-" + System.currentTimeMillis();
// ④ 返回结果
return String.format(
"订单创建成功!\n订单ID:%s\n用户:%s(%s岁)",
orderId, user.getUsername(), user.getAge()
);
}
/**
* 测试 Feign 调用
*/
@GetMapping("/test-feign")
public String testFeign() {
// 测试不同类型的调用
// 1. 路径参数
User user1 = userClient.getUserById(1001L);
// 2. 查询参数
User user2 = userClient.getUserByName("张三");
// 3. 请求体参数
User newUser = new User(null, "李四", 28, "lisi@example.com");
User user3 = userClient.createUser(newUser);
return "Feign 调用测试成功!\n" +
"用户1:" + user1 + "\n" +
"用户2:" + user2 + "\n" +
"用户3:" + user3;
}
}
2.4 启动测试
启动顺序:
- 启动 Nacos
- 启动 user-service(8081)
- 启动 order-service(8082)
测试接口:
bash
# 创建订单
curl -X POST "http://localhost:8082/order/create?userId=1001"
# 返回:
订单创建成功!
订单ID:ORDER-1711770123456
用户:用户1001(25岁)
三、OpenFeign 参数传递
3.1 路径参数(@PathVariable)
java
/**
* 单个路径参数
*/
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);
/**
* 多个路径参数
*/
@GetMapping("/user/{id}/order/{orderId}")
String getOrderDetail(
@PathVariable("id") Long userId,
@PathVariable("orderId") Long orderId
);
调用示例:
java
User user = userClient.getUserById(1001L);
// GET http://user-service/user/1001
String detail = userClient.getOrderDetail(1001L, 2001L);
// GET http://user-service/user/1001/order/2001
3.2 查询参数(@RequestParam)
java
/**
* 单个查询参数
*/
@GetMapping("/user/search")
User searchUser(@RequestParam("name") String name);
/**
* 多个查询参数
*/
@GetMapping("/user/list")
List<User> listUsers(
@RequestParam("page") Integer page,
@RequestParam("size") Integer size
);
/**
* 可选参数(required = false)
*/
@GetMapping("/user/filter")
List<User> filterUsers(
@RequestParam(value = "age", required = false) Integer age,
@RequestParam(value = "city", required = false) String city
);
调用示例:
java
User user = userClient.searchUser("张三");
// GET http://user-service/user/search?name=张三
List<User> users = userClient.listUsers(1, 10);
// GET http://user-service/user/list?page=1&size=10
3.3 请求体参数(@RequestBody)
java
/**
* POST 请求
*/
@PostMapping("/user")
User createUser(@RequestBody User user);
/**
* PUT 请求
*/
@PutMapping("/user/{id}")
User updateUser(
@PathVariable("id") Long id,
@RequestBody User user
);
/**
* DELETE 请求
*/
@DeleteMapping("/user/{id}")
void deleteUser(@PathVariable("id") Long id);
调用示例:
java
User newUser = new User(null, "王五", 30, "wangwu@example.com");
User created = userClient.createUser(newUser);
// POST http://user-service/user
// Body: {"username":"王五","age":30,"email":"wangwu@example.com"}
3.4 请求头参数(@RequestHeader)
java
/**
* 单个请求头
*/
@GetMapping("/user/{id}")
User getUserById(
@PathVariable("id") Long id,
@RequestHeader("Authorization") String token
);
/**
* 多个请求头
*/
@GetMapping("/user/info")
User getUserInfo(
@RequestHeader("Authorization") String token,
@RequestHeader("X-Request-Id") String requestId
);
调用示例:
java
User user = userClient.getUserById(1001L, "Bearer token123");
// GET http://user-service/user/1001
// Header: Authorization: Bearer token123
3.5 表单参数(@SpringQueryMap)
java
/**
* Map 参数(自动转为查询参数)
*/
@GetMapping("/user/search")
List<User> searchUsers(@SpringQueryMap Map<String, Object> params);
调用示例:
java
Map<String, Object> params = new HashMap<>();
params.put("name", "张三");
params.put("age", 25);
params.put("city", "北京");
List<User> users = userClient.searchUsers(params);
// GET http://user-service/user/search?name=张三&age=25&city=北京
四、负载均衡策略
4.1 默认策略(轮询)
OpenFeign 集成了 Spring Cloud LoadBalancer ,默认使用轮询策略。
用户服务 3 个实例:
- 192.168.1.101:8081
- 192.168.1.102:8081
- 192.168.1.103:8081
第 1 次请求 → 192.168.1.101:8081
第 2 次请求 → 192.168.1.102:8081
第 3 次请求 → 192.168.1.103:8081
第 4 次请求 → 192.168.1.101:8081 ↻
4.2 自定义负载均衡策略
方式 1:全局配置(所有服务)
java
package com.example.orderservice.config;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* 全局负载均衡配置
*/
@Configuration
public class LoadBalancerConfig {
/**
* 配置随机策略
*/
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
}
方式 2:针对单个服务
yaml
# application.yml
spring:
cloud:
loadbalancer:
configurations: random # 全局策略:random 或 round-robin
# 针对单个服务配置
nacos:
discovery:
server-addr: localhost:8848
# 针对 user-service 使用随机策略
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
4.3 自定义负载均衡规则
java
package com.example.orderservice.loadbalancer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
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;
/**
* 自定义负载均衡规则:按权重选择
*/
public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
public WeightedLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId) {
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
return supplier.get(request).next().map(this::getInstanceResponse);
}
/**
* 按权重选择实例
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return new EmptyResponse();
}
// 计算总权重
int totalWeight = instances.stream()
.mapToInt(instance -> {
String weight = instance.getMetadata().get("weight");
return weight != null ? Integer.parseInt(weight) : 1;
})
.sum();
// 随机数
int random = ThreadLocalRandom.current().nextInt(totalWeight);
// 按权重选择
int currentWeight = 0;
for (ServiceInstance instance : instances) {
String weight = instance.getMetadata().get("weight");
int instanceWeight = weight != null ? Integer.parseInt(weight) : 1;
currentWeight += instanceWeight;
if (random < currentWeight) {
return new DefaultResponse(instance);
}
}
return new DefaultResponse(instances.get(0));
}
}
五、超时与重试配置
5.1 超时配置
全局配置:
yaml
# application.yml
feign:
client:
config:
default: # 默认配置(所有服务)
connectTimeout: 5000 # 连接超时:5 秒
readTimeout: 10000 # 读取超时:10 秒
针对单个服务:
yaml
feign:
client:
config:
user-service: # 针对 user-service
connectTimeout: 3000 # 连接超时:3 秒
readTimeout: 5000 # 读取超时:5 秒
order-service: # 针对 order-service
connectTimeout: 10000 # 连接超时:10 秒
readTimeout: 30000 # 读取超时:30 秒
代码配置:
java
package com.example.orderservice.config;
import feign.Request;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* Feign 超时配置
*/
@Configuration
public class FeignConfig {
@Bean
public Request.Options requestOptions() {
return new Request.Options(
5000, TimeUnit.MILLISECONDS, // 连接超时
10000, TimeUnit.MILLISECONDS // 读取超时
);
}
}
5.2 重试配置
默认重试器:
java
package com.example.orderservice.config;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign 重试配置
*/
@Configuration
public class FeignRetryConfig {
/**
* 配置重试器
*
* @return Retryer
*/
@Bean
public Retryer feignRetryer() {
/*
* Retryer.Default 参数说明:
* 1. period:初始重试间隔(毫秒)
* 2. maxPeriod:最大重试间隔(毫秒)
* 3. maxAttempts:最大重试次数
*
* 重试间隔计算:interval = min(period * 1.5^n, maxPeriod)
*/
return new Retryer.Default(
100, // 初始间隔 100ms
1000, // 最大间隔 1s
3 // 最多重试 3 次
);
}
}
禁用重试:
java
@Bean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY; // 不重试
}
5.3 超时测试
java
/**
* 用户服务:模拟慢接口
*/
@GetMapping("/slow")
public String slowApi() throws InterruptedException {
Thread.sleep(15000); // 休眠 15 秒
return "慢接口响应";
}
/**
* Feign 客户端
*/
@GetMapping("/slow")
String slowApi();
/**
* 订单服务:调用慢接口
*/
@GetMapping("/test-timeout")
public String testTimeout() {
try {
String result = userClient.slowApi();
return "调用成功:" + result;
} catch (Exception e) {
return "调用超时:" + e.getMessage();
}
}
测试结果:
bash
# 配置 readTimeout = 10000(10 秒)
curl http://localhost:8082/order/test-timeout
# 10 秒后返回:
调用超时:Read timed out executing GET http://user-service/user/slow
六、日志配置
6.1 日志级别
OpenFeign 日志级别:
| 级别 | 说明 | 输出内容 |
|---|---|---|
| NONE | 不记录(默认) | 无 |
| BASIC | 基本信息 | 请求方法、URL、响应状态码、执行时间 |
| HEADERS | 基本 + 请求/响应头 | BASIC + Headers |
| FULL | 完整信息 | BASIC + Headers + Body |
6.2 配置日志
方式 1:配置类
java
package com.example.orderservice.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign 日志配置
*/
@Configuration
public class FeignLogConfig {
/**
* 配置日志级别
*/
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // 记录完整请求和响应
}
}
方式 2:配置文件
yaml
# application.yml
feign:
client:
config:
default:
loggerLevel: FULL # NONE、BASIC、HEADERS、FULL
方式 3:针对单个客户端
java
@FeignClient(
name = "user-service",
configuration = FeignLogConfig.class // 指定配置类
)
public interface UserClient { ... }
6.3 开启日志输出
yaml
# application.yml
logging:
level:
# 开启 Feign 客户端的日志(DEBUG 级别)
com.example.orderservice.client.UserClient: DEBUG
6.4 日志示例
BASIC 级别:
2024-03-30 10:00:00 DEBUG UserClient : [UserClient#getUserById] ---> GET http://user-service/user/1001 HTTP/1.1
2024-03-30 10:00:01 DEBUG UserClient : [UserClient#getUserById] <--- HTTP/1.1 200 (1234ms)
FULL 级别:
2024-03-30 10:00:00 DEBUG UserClient : [UserClient#getUserById] ---> GET http://user-service/user/1001 HTTP/1.1
2024-03-30 10:00:00 DEBUG UserClient : Content-Length: 0
2024-03-30 10:00:00 DEBUG UserClient : ---> END HTTP (0-byte body)
2024-03-30 10:00:01 DEBUG UserClient : [UserClient#getUserById] <--- HTTP/1.1 200 (1234ms)
2024-03-30 10:00:01 DEBUG UserClient : content-type: application/json
2024-03-30 10:00:01 DEBUG UserClient : {"id":1001,"username":"用户1001","age":25,"email":"user1001@example.com"}
2024-03-30 10:00:01 DEBUG UserClient : <--- END HTTP (89-byte body)
七、拦截器与请求头传递
7.1 请求拦截器
应用场景:
- 统一添加请求头(Token、TraceId)
- 记录请求日志
- 签名验证
实现方式:
java
package com.example.orderservice.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
/**
* Feign 请求拦截器
*
* 作用:在发送请求前统一处理
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// ① 添加统一请求头
template.header("Authorization", "Bearer token123");
template.header("X-Request-Source", "order-service");
template.header("X-Request-Id", generateRequestId());
// ② 记录请求日志
System.out.println("Feign 请求:" + template.method() + " " + template.url());
System.out.println("请求头:" + template.headers());
// ③ 添加查询参数(可选)
template.query("from", "order-service");
}
/**
* 生成请求 ID
*/
private String generateRequestId() {
return "REQ-" + System.currentTimeMillis();
}
}
7.2 传递请求头(ThreadLocal)
问题场景:
用户请求 → Gateway(带 Token)→ 订单服务 → 用户服务
问题:订单服务调用用户服务时,Token 丢失了!
解决方案:使用 ThreadLocal 传递
步骤 1:创建 RequestContextHolder
java
package com.example.orderservice.context;
/**
* 请求上下文
*/
public class RequestContextHolder {
private static final ThreadLocal<String> TOKEN_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<String> REQUEST_ID_HOLDER = new ThreadLocal<>();
/**
* 设置 Token
*/
public static void setToken(String token) {
TOKEN_HOLDER.set(token);
}
/**
* 获取 Token
*/
public static String getToken() {
return TOKEN_HOLDER.get();
}
/**
* 设置 RequestId
*/
public static void setRequestId(String requestId) {
REQUEST_ID_HOLDER.set(requestId);
}
/**
* 获取 RequestId
*/
public static String getRequestId() {
return REQUEST_ID_HOLDER.get();
}
/**
* 清除上下文
*/
public static void clear() {
TOKEN_HOLDER.remove();
REQUEST_ID_HOLDER.remove();
}
}
步骤 2:创建 Web 拦截器(保存请求头)
java
package com.example.orderservice.interceptor;
import com.example.orderservice.context.RequestContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Web 拦截器:保存请求头到 ThreadLocal
*/
@Component
public class WebRequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 保存 Token 到 ThreadLocal
String token = request.getHeader("Authorization");
if (token != null) {
RequestContextHolder.setToken(token);
}
// 保存 RequestId
String requestId = request.getHeader("X-Request-Id");
if (requestId != null) {
RequestContextHolder.setRequestId(requestId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求结束,清除 ThreadLocal
RequestContextHolder.clear();
}
}
步骤 3:注册拦截器
java
package com.example.orderservice.config;
import com.example.orderservice.interceptor.WebRequestInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC 配置
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private WebRequestInterceptor webRequestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(webRequestInterceptor)
.addPathPatterns("/**"); // 拦截所有请求
}
}
步骤 4:Feign 拦截器传递请求头
java
package com.example.orderservice.interceptor;
import com.example.orderservice.context.RequestContextHolder;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
/**
* Feign 拦截器:传递请求头
*/
@Component
public class FeignHeaderInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从 ThreadLocal 获取 Token 并传递
String token = RequestContextHolder.getToken();
if (token != null) {
template.header("Authorization", token);
}
// 传递 RequestId
String requestId = RequestContextHolder.getRequestId();
if (requestId != null) {
template.header("X-Request-Id", requestId);
}
}
}
八、OpenFeign 高级特性
8.1 文件上传
java
/**
* Feign 客户端:文件上传
*/
@FeignClient(name = "file-service", configuration = FeignMultipartConfig.class)
public interface FileClient {
@PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String uploadFile(@RequestPart("file") MultipartFile file);
}
/**
* Feign 文件上传配置
*/
@Configuration
public class FeignMultipartConfig {
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder();
}
}
8.2 响应压缩
yaml
# application.yml
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/xml,application/xml,application/json # 压缩类型
min-request-size: 2048 # 最小压缩大小(字节)
response:
enabled: true # 开启响应压缩
8.3 GZIP 压缩
yaml
feign:
compression:
request:
enabled: true
response:
enabled: true
useGzipDecoder: true # 使用 GZIP 解码器
九、最佳实践
9.1 统一异常处理
java
package com.example.orderservice.exception;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.stereotype.Component;
/**
* Feign 异常解码器
*/
@Component
public class FeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()) {
case 400:
return new IllegalArgumentException("参数错误");
case 401:
return new RuntimeException("未授权");
case 404:
return new RuntimeException("资源不存在");
case 500:
return new RuntimeException("服务器内部错误");
default:
return new RuntimeException("未知错误:" + response.status());
}
}
}
9.2 Feign 客户端接口抽取
创建公共 API 模块:
java
// common-api 模块
package com.example.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "user-service", path = "/user")
public interface UserApi {
@GetMapping("/{id}")
User getUserById(@PathVariable("id") Long id);
}
服务提供者实现接口:
java
// user-service 模块
@RestController
public class UserController implements UserApi {
@Override
public User getUserById(Long id) {
// 实现逻辑
}
}
服务消费者直接使用:
java
// order-service 模块
@Autowired
private UserApi userApi; // 直接注入,无需定义接口
9.3 配置优先级
FeignClient 注解配置 > 配置文件针对单个服务 > 配置文件默认配置 > 代码配置
十、常见面试题
面试题 1:OpenFeign 和 Feign 的区别?
参考答案:
| 对比项 | Feign | OpenFeign |
|---|---|---|
| 维护者 | Netflix | SpringCloud 社区 |
| 集成 | 需手动配置 | 自动集成 SpringBoot |
| 负载均衡 | 集成 Ribbon | 集成 LoadBalancer |
| 熔断降级 | 需手动集成 | 自动集成 Sentinel/Hystrix |
| 推荐指数 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
面试题 2:OpenFeign 的工作原理是什么?
参考答案:
-
启动时扫描:
- 扫描 @FeignClient 注解
- 为每个接口生成代理对象(JDK 动态代理)
-
方法调用:
- 拦截接口方法调用
- 解析注解(@GetMapping、@PathVariable 等)
- 构造 HTTP 请求
-
服务发现:
- 从 Nacos 获取服务实例列表
- LoadBalancer 选择一个实例
-
发送请求:
- 发送 HTTP 请求
- 接收响应
- 反序列化为 Java 对象
面试题 3:OpenFeign 如何实现负载均衡?
参考答案:
OpenFeign 集成了 Spring Cloud LoadBalancer:
-
从 Nacos 获取实例列表:
user-service: [ {ip: "192.168.1.101", port: 8081}, {ip: "192.168.1.102", port: 8081} ] -
LoadBalancer 选择实例:
- 默认策略:轮询(Round Robin)
- 可配置:随机、加权等
-
替换服务名为实际地址:
http://user-service/user/1001 → http://192.168.1.101:8081/user/1001
面试题 4:OpenFeign 超时时间如何配置?
参考答案:
方式 1:配置文件(推荐)
yaml
feign:
client:
config:
default: # 全局配置
connectTimeout: 5000 # 连接超时 5 秒
readTimeout: 10000 # 读取超时 10 秒
user-service: # 针对单个服务
readTimeout: 30000 # 30 秒
方式 2:代码配置
java
@Bean
public Request.Options requestOptions() {
return new Request.Options(5000, 10000);
}
面试题 5:OpenFeign 如何传递请求头?
参考答案:
方式 1:在接口方法中声明
java
@GetMapping("/user/{id}")
User getUserById(
@PathVariable("id") Long id,
@RequestHeader("Authorization") String token
);
方式 2:使用拦截器
java
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("Authorization", "Bearer token123");
}
}
方式 3:ThreadLocal 传递
java
// Web 拦截器保存请求头到 ThreadLocal
RequestContextHolder.setToken(token);
// Feign 拦截器从 ThreadLocal 获取并传递
template.header("Authorization", RequestContextHolder.getToken());
面试题 6:OpenFeign 如何处理异常?
参考答案:
方式 1:自定义 ErrorDecoder
java
@Component
public class FeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()) {
case 404:
return new NotFoundException("资源不存在");
case 500:
return new ServerException("服务器错误");
default:
return new RuntimeException("未知错误");
}
}
}
方式 2:使用 try-catch
java
try {
User user = userClient.getUserById(1001L);
} catch (FeignException e) {
if (e.status() == 404) {
// 处理 404
}
}
面试题 7:OpenFeign 日志级别有哪些?
参考答案:
| 级别 | 输出内容 |
|---|---|
| NONE | 不记录(默认) |
| BASIC | 请求方法、URL、响应状态码、执行时间 |
| HEADERS | BASIC + 请求/响应头 |
| FULL | BASIC + HEADERS + 请求/响应体 |
配置方式:
java
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
面试题 8:OpenFeign 性能优化有哪些方法?
参考答案:
-
使用 HTTP 连接池:
xml<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>yamlfeign: httpclient: enabled: true max-connections: 200 max-connections-per-route: 50 -
开启响应压缩:
yamlfeign: compression: response: enabled: true -
合理设置超时时间:
yamlfeign: client: config: default: connectTimeout: 2000 readTimeout: 5000 -
禁用不必要的重试:
java@Bean public Retryer feignRetryer() { return Retryer.NEVER_RETRY; }
面试题 9:OpenFeign 和 RestTemplate 的区别?
参考答案:
| 对比项 | OpenFeign | RestTemplate |
|---|---|---|
| 调用方式 | 声明式(接口 + 注解) | 编程式(手动拼接) |
| 代码量 | 少 | 多 |
| 可读性 | 好 | 一般 |
| 负载均衡 | 自动集成 | 需要 @LoadBalanced |
| 熔断降级 | 自动集成 | 需手动实现 |
| 学习成本 | 低 | 低 |
| 推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
面试题 10:OpenFeign 如何实现降级?
参考答案:
方式 1:fallback(推荐)
java
@FeignClient(
name = "user-service",
fallback = UserClientFallback.class // 降级类
)
public interface UserClient {
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);
}
@Component
public class UserClientFallback implements UserClient {
@Override
public User getUserById(Long id) {
// 降级逻辑
return new User(id, "降级用户", 0, "");
}
}
方式 2:fallbackFactory(可获取异常信息)
java
@FeignClient(
name = "user-service",
fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient { ... }
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public User getUserById(Long id) {
System.out.println("降级原因:" + cause.getMessage());
return new User(id, "降级用户", 0, "");
}
};
}
}
总结
本文详细介绍了 OpenFeign 的使用方法:
✅ 快速入门 :定义接口 + @FeignClient 注解
✅ 参数传递 :@PathVariable、@RequestParam、@RequestBody
✅ 负载均衡 :集成 LoadBalancer,支持自定义策略
✅ 超时重试 :配置超时时间和重试策略
✅ 日志配置 :4 种日志级别,方便调试
✅ 拦截器 :统一处理请求头、签名、日志
✅ 高级特性 :文件上传、响应压缩、异常处理
✅ 最佳实践:接口抽取、统一异常处理
下一篇预告:《SpringCloud 网关、配置与熔断:Gateway+Nacos 配置+Sentinel 入门》
📚 学习建议:
- 先掌握基本用法(@FeignClient、参数传递)
- 理解负载均衡和超时配置
- 实践中遇到问题再深入研究拦截器和高级特性