第 224 章:OpenFeign 服务调用
学习目标
- 掌握 OpenFeign 声明式调用
- 学会自定义配置
- 实现拦截器
- 集成 Sentinel 熔断
一、OpenFeign 简介
OpenFeign 是声明式 HTTP 客户端,通过注解定义接口,自动生成实现。
1.1 对比
| 工具 | 写法 | 特点 |
|---|---|---|
| RestTemplate | 手写 URL | 模板方法,繁琐 |
| WebClient | 响应式 | 函数式,需 Reactor 基础 |
| OpenFeign | 接口注解 | 声明式,优雅 |
| gRPC | Stub | 高性能,跨语言 |
| HttpClient | 手动 | 灵活,代码量大 |
1.2 工作原理
二、快速上手
2.1 引入依赖
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>2.2 启用 Feign
java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderServiceApplication { ... }2.3 定义客户端
java
@FeignClient(name = "inventory-service")
public interface InventoryClient {
@GetMapping("/api/inventory/{skuId}")
Result<InventoryDTO> getStock(@PathVariable("skuId") String skuId);
@PostMapping("/api/inventory/deduct")
Result<Void> deduct(@RequestBody DeductRequest req);
@GetMapping("/api/inventory/list")
Result<List<InventoryDTO>> listBySkus(
@RequestParam("skuIds") List<String> skuIds
);
}2.4 服务端 Controller
java
@RestController
@RequestMapping("/api/inventory")
public class InventoryController {
@GetMapping("/{skuId}")
public Result<InventoryDTO> getStock(@PathVariable String skuId) {
return Result.ok(inventoryService.getBySku(skuId));
}
@PostMapping("/deduct")
public Result<Void> deduct(@RequestBody DeductRequest req) {
inventoryService.deduct(req);
return Result.ok();
}
}2.5 调用
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final InventoryClient inventoryClient;
public void createOrder(OrderRequest req) {
// 调用方直接像调用本地方法一样
Result<InventoryDTO> resp = inventoryClient.getStock(req.getSkuId());
if (!resp.isSuccess() || resp.getData().getQuantity() < req.getQuantity()) {
throw new BizException("库存不足");
}
}
}三、参数传递
3.1 路径参数
java
// 必须写 @PathVariable("xxx")
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/api/users/{id}")
Result<UserDTO> get(@PathVariable("id") Long id);
}3.2 查询参数
java
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/api/users")
Result<List<UserDTO>> list(
@RequestParam("status") String status,
@RequestParam("name") String name
);
// 对象自动展开
@GetMapping("/api/users")
Result<List<UserDTO>> search(UserQuery query);
}3.3 请求体
java
@PostMapping("/api/orders")
Result<OrderDTO> create(@RequestBody OrderRequest req);3.4 Header
java
@GetMapping("/api/users")
Result<List<UserDTO>> list(@RequestHeader("Authorization") String token);3.5 文件上传
java
@PostMapping(value = "/api/files/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Result<FileDTO> upload(@RequestPart("file") MultipartFile file);四、超时与重试
4.1 默认超时
- connectTimeout: 10 秒
- readTimeout: 60 秒
4.2 自定义超时
yaml
feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时
readTimeout: 10000 # 读超时
loggerLevel: BASIC # 日志级别
retryer: com.example.MyRetryer
# 单服务覆盖
inventory-service:
connectTimeout: 3000
readTimeout: 300004.3 自定义重试
java
@Bean
public Retryer feignRetryer() {
// 1.5 秒后第一次重试,最多 3 次,最多 5 秒
return new Retryer.Default(1500, 5000, 3);
}五、日志配置
5.1 日志级别
| 级别 | 输出 |
|---|---|
| NONE | 不输出 |
| BASIC | 方法名 + 状态码 + 时间 |
| HEADERS | + Header 信息 |
| FULL | + 请求体 + 响应体 |
5.2 配置
yaml
feign:
client:
config:
default:
loggerLevel: FULL5.3 启用日志
java
@Configuration
public class FeignLogConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
// 或指定包
logging.level.com.example.feign=DEBUG六、拦截器
6.1 RequestInterceptor
java
@Component
public class UserContextFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest req = attrs.getRequest();
String userId = req.getHeader("X-User-Id");
if (userId != null) {
template.header("X-User-Id", userId);
}
String token = req.getHeader("Authorization");
if (token != null) {
template.header("Authorization", token);
}
}
// 或从 UserContextHolder 取
if (UserContextHolder.getUserId() != null) {
template.header("X-User-Id", UserContextHolder.getUserId());
}
}
}6.2 全局生效
注册为 @Component 即全局生效。
6.3 单 Client 拦截器
java
@FeignClient(
name = "payment-service",
configuration = PaymentFeignConfig.class
)
public interface PaymentClient { ... }
public class PaymentFeignConfig {
@Bean
public RequestInterceptor paymentInterceptor() {
return template -> {
template.header("X-Service", "order-service");
template.query("version", "v1");
};
}
}七、数据压缩
yaml
feign:
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048 # 超过 2KB 才压缩
response:
enabled: true八、继承客户端定义
8.1 公共 BaseClient
java
public interface BaseClient<T> {
@GetMapping
Result<List<T>> list();
@GetMapping("/{id}")
Result<T> get(@PathVariable Long id);
@PostMapping
Result<T> create(@RequestBody T obj);
}8.2 扩展
java
@FeignClient("inventory-service")
public interface InventoryClient extends BaseClient<InventoryDTO> {
@Override
@GetMapping("/api/inventory")
Result<List<InventoryDTO>> list();
@Override
@GetMapping("/api/inventory/{id}")
Result<InventoryDTO> get(@PathVariable Long id);
@PostMapping("/api/inventory/deduct")
Result<Void> deduct(@RequestBody DeductRequest req);
}九、客户端配置
9.1 自定义 OkHttp 客户端
xml
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>yaml
feign:
okhttp:
enabled: true
httpclient:
max-connections: 200
time-to-live: 309.2 使用 HttpClient5
xml
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient5</artifactId>
</dependency>yaml
feign:
httpclient5:
enabled: true
disable-ssl-validation: false
max-connections: 2009.3 客户端选择
| 客户端 | 性能 | 适用 |
|---|---|---|
| HttpURLConnection(默认) | 中 | 简单 |
| Apache HttpClient | 中 | 兼容 |
| OkHttp | 高 | 高并发 |
| HttpClient5 | 高 | 异步 |
十、结果解码与错误处理
10.1 自定义 Decoder
java
@Bean
public Decoder feignDecoder() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return new ResponseEntityDecoder(new SpringDecoder(mapper));
}10.2 自定义 ErrorDecoder
java
@Component
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
int status = response.status();
String url = response.request().url();
return switch (status) {
case 400 -> new BizException("BAD_REQUEST", "客户端错误: " + url);
case 401 -> new UnauthorizedException("未授权");
case 404 -> new NotFoundException("资源不存在: " + url);
case 500, 502, 503 -> new ServiceUnavailableException("服务不可用");
default -> new BizException("HTTP_" + status, "未知错误");
};
}
}java
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}十一、Sentinel 熔断降级
11.1 集成
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>yaml
feign:
sentinel:
enabled: true11.2 Fallback
java
@FeignClient(
name = "inventory-service",
fallback = InventoryClientFallback.class
)
public interface InventoryClient {
@GetMapping("/api/inventory/{skuId}")
Result<InventoryDTO> getStock(@PathVariable String skuId);
}
@Component
public class InventoryClientFallback implements InventoryClient {
@Override
public Result<InventoryDTO> getStock(String skuId) {
return Result.error("INVENTORY_BUSY", "库存查询暂不可用");
}
}11.3 FallbackFactory(携带异常)
java
@FeignClient(
name = "inventory-service",
fallbackFactory = InventoryFallbackFactory.class
)
public interface InventoryClient { ... }
@Component
public class InventoryFallbackFactory
implements FallbackFactory<InventoryClient> {
@Override
public InventoryClient create(Throwable cause) {
log.error("Feign 调用失败: {}", cause.getMessage());
return new InventoryClient() {
@Override
public Result<InventoryDTO> getStock(String skuId) {
return Result.error("INVENTORY_ERROR", "调用失败: " + cause.getMessage());
}
};
}
}十二、Hystrix 兼容
OpenFeign 兼容 Hystrix(已停维,推荐 Sentinel)。
yaml
feign:
hystrix:
enabled: truejava
@FeignClient(
name = "user-service",
fallback = UserFallback.class
)
public interface UserClient { ... }十三、性能优化
13.1 启用 HTTP Pool
yaml
# OkHttp 连接池
feign:
okhttp:
enabled: true
httpclient:
time-to-live: 60
max-connections-per-route: 5013.2 关闭日志(生产)
yaml
feign:
client:
config:
default:
loggerLevel: NONE13.3 关闭客户端诊断
yaml
feign:
client:
config:
default:
enable: true十四、版本演进
| OpenFeign | Spring Cloud |
|---|---|
| 4.x | Spring Cloud 2023.x(Spring Boot 3.x) |
| 3.x | Spring Cloud 2020.x |
| 2.x | Spring Cloud Hoxton |
注意 Spring Boot 3.x 要求 JDK 17+。
十五、实战经验
15.1 常见问题
| 问题 | 解决方案 |
|---|---|
@PathVariable 必须有 value | 显式标注 |
@RequestParam 缺失 | 显式标注或加 , required = false |
| 时间日期序列化 | 加 JavaTimeModule |
| 401 调用失败 | 拦截器传 token |
| 接收方收到 null body | 启用 spring.mvc.servlet.load-on-startup |
| OpenFeign 调用异步方法 | OpenFeign 同步,改用 WebClient |
15.2 单元测试 Mock
java
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean
private InventoryClient inventoryClient;
@Test
void testCreateOrder() {
when(inventoryClient.getStock("SKU-001"))
.thenReturn(Result.ok(new InventoryDTO(10)));
orderService.createOrder(new OrderRequest("SKU-001", 1));
verify(orderRepository, times(1)).save(any());
}
}十六、本章小结
| 主题 | 要点 |
|---|---|
| 声明式 | 接口注解 |
| 拦截器 | 透传上下文 |
| 超时重试 | 客户端配置 |
| 解码 | 自定义 Decoder / ErrorDecoder |
| 熔断 | Sentinel / Hystrix |
动手练习
- 用 OpenFeign 实现订单查询库存
- 自定义拦截器,透传 X-User-Id
- 配置 Sentinel,实现降级
- 用 FallbackFactory 输出失败原因