Skip to content
第 224 / 250 章架构⏱ 12 分钟阅读

第 224 章:OpenFeign 服务调用

学习目标

  • 掌握 OpenFeign 声明式调用
  • 学会自定义配置
  • 实现拦截器
  • 集成 Sentinel 熔断

一、OpenFeign 简介

OpenFeign 是声明式 HTTP 客户端,通过注解定义接口,自动生成实现。

1.1 对比

工具写法特点
RestTemplate手写 URL模板方法,繁琐
WebClient响应式函数式,需 Reactor 基础
OpenFeign接口注解声明式,优雅
gRPCStub高性能,跨语言
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: 30000

4.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: FULL

5.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: 30

9.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: 200

9.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: true

11.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: true
java
@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: 50

13.2 关闭日志(生产)

yaml
feign:
  client:
    config:
      default:
        loggerLevel: NONE

13.3 关闭客户端诊断

yaml
feign:
  client:
    config:
      default:
        enable: true

十四、版本演进

OpenFeignSpring Cloud
4.xSpring Cloud 2023.x(Spring Boot 3.x)
3.xSpring Cloud 2020.x
2.xSpring 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

动手练习

  1. 用 OpenFeign 实现订单查询库存
  2. 自定义拦截器,透传 X-User-Id
  3. 配置 Sentinel,实现降级
  4. 用 FallbackFactory 输出失败原因

推荐阅读


下一章:第 225 章:配置中心与 Spring Cloud 全景

本站基于 VitePress 构建 · 由 Codebook 团队维护