四、OpenFeign远程调用

文章目录

  • 什么是OpenFeign
  • OpenFeign工作原理
  • [OpenFeign 的基本工作流程](#OpenFeign 的基本工作流程)
  • OpenFeign基本使用流程
    • [第一阶段:项目启动时创建 Feign 代理对象](#第一阶段:项目启动时创建 Feign 代理对象)
      • [1. @EnableFeignClients 开启 Feign 功能](#1. @EnableFeignClients 开启 Feign 功能)
      • [2. 扫描所有 @FeignClient 接口](#2. 扫描所有 @FeignClient 接口)
      • [3. 为每个 FeignClient 准备一组组件](#3. 为每个 FeignClient 准备一组组件)
      • [4. Contract 解析方法注解](#4. Contract 解析方法注解)
      • [5. 创建接口代理对象](#5. 创建接口代理对象)
      • [6. 把代理对象交给 Spring 管理](#6. 把代理对象交给 Spring 管理)
    • [第二阶段:调用 Feign 接口时发送 HTTP 请求](#第二阶段:调用 Feign 接口时发送 HTTP 请求)
      • [1. 代理对象拦截方法调用](#1. 代理对象拦截方法调用)
      • [2. 根据元数据创建 RequestTemplate](#2. 根据元数据创建 RequestTemplate)
      • [3. 处理方法参数](#3. 处理方法参数)
      • [4. Encoder 对请求参数进行编码](#4. Encoder 对请求参数进行编码)
      • [5. 执行 RequestInterceptor](#5. 执行 RequestInterceptor)
    • [第三阶段:OpenFeign 如何通过 Nacos 找到服务](#第三阶段:OpenFeign 如何通过 Nacos 找到服务)
      • [1. name 是服务名称,不一定是域名](#1. name 是服务名称,不一定是域名)
      • [2. OpenFeign 不会把服务名直接发给网络](#2. OpenFeign 不会把服务名直接发给网络)
      • [3. 从服务发现组件获取实例列表](#3. 从服务发现组件获取实例列表)
      • [4. LoadBalancer 选择一台服务实例](#4. LoadBalancer 选择一台服务实例)
      • [5. Nacos 和 OpenFeign 的完整配合流程](#5. Nacos 和 OpenFeign 的完整配合流程)
    • [第四阶段:底层 HTTP 客户端发送请求](#第四阶段:底层 HTTP 客户端发送请求)
    • 第五阶段:服务提供者处理请求
    • [Decoder 解析响应](#Decoder 解析响应)
    • 远程调用异常时如何处理
      • [1. 正常响应,](#1. 正常响应,)
      • [2. 异常响应](#2. 异常响应)
      • [3. 熔断和降级](#3. 熔断和降级)
  • [客户端负载均衡 VS 服务端负载均衡](#客户端负载均衡 VS 服务端负载均衡)
  • [OpenFeign 日志](#OpenFeign 日志)
    • [1、OpenFeign 日志的四个级别](#1、OpenFeign 日志的四个级别)
    • [2、开启 OpenFeign 日志](#2、开启 OpenFeign 日志)
      • [1. 配置 application.yml](#1. 配置 application.yml)
    • [为所有 Feign 客户端开启日志](#为所有 Feign 客户端开启日志)
    • [使用 Java 配置类开启日志](#使用 Java 配置类开启日志)
      • [1. 创建 FeignLogConfig](#1. 创建 FeignLogConfig)
      • [2. 在 Feign 接口中引用配置类](#2. 在 Feign 接口中引用配置类)
    • 调用接口后的日志效果
  • [OpenFeign 超时控制](#OpenFeign 超时控制)
  • [OpenFeign 重试机制](#OpenFeign 重试机制)
    • 重试机制中的核心组件
      • [1. RetryableException](#1. RetryableException)
      • [2. Retryer](#2. Retryer)
      • [3. ErrorDecoder](#3. ErrorDecoder)
    • [OpenFeign 重试完整执行流程](#OpenFeign 重试完整执行流程)
    • [开启 OpenFeign 重试](#开启 OpenFeign 重试)
      • [1. 创建重试配置类](#1. 创建重试配置类)
      • [2. Feign 接口引用重试配置](#2. Feign 接口引用重试配置)
    • [针对 HTTP 503 进行重试](#针对 HTTP 503 进行重试)
    • 哪些情况会重试
  • [OpenFeign 拦截器](#OpenFeign 拦截器)
  • Fallback-兜底返回

什么是OpenFeign

OpenFeign 是一个声明式 HTTP 客户端。

所谓"声明式",就是开发人员只需要编写一个 Java 接口,并在接口和方法上添加 @FeignClient、@GetMapping、@PostMapping 等注解,不需要手动编写创建连接、拼接 URL、发送请求、解析响应等代码。

OpenFeign 会在项目启动时,为这个接口创建一个代理对象。程序调用接口方法时,实际上调用的是代理对象,由代理对象把这次 Java 方法调用转换成一次 HTTP 请求。Spring Cloud OpenFeign还集成了 Spring MVC 注解、编码器、解码器、Spring Cloud LoadBalancer 和熔断器等组件。

RestTemplate:编程式、命令式HTTP 客户端

OpenFeign 和 RestTemplate 的核心区别

对比项 OpenFeign RestTemplate
调用方式 声明式接口调用 命令式、模板式调用
代码形式 定义接口和注解 手动编写请求代码
核心注解或方法 @FeignClient@GetMapping getForObject()postForObject()exchange()
URL 处理 根据接口注解自动构造 开发人员手动构造
实现类 OpenFeign动态生成代理对象 直接调用RestTemplate对象
服务发现 可根据服务名结合注册中心与负载均衡 需要创建并配置@LoadBalanced RestTemplate
负载均衡 使用服务名时可集成Spring Cloud LoadBalancer 添加@LoadBalanced后集成Spring Cloud LoadBalancer
参数处理 按方法参数和注解自动处理 需要手动组织参数、请求体和请求头
响应转换 按接口返回类型自动转换 在调用方法中明确指定返回类型
代码量 相对较少 相对较多
接口集中管理 比较方便 请求代码容易散落在业务类中
灵活程度 适合结构固定的远程接口 适合高度动态或特殊请求
调用模型 通常是同步阻塞 同步阻塞
典型场景 微服务之间固定接口调用 通用HTTP调用、动态URL、简单调用

OpenFeign工作原理

  1. 开发人员定义 Feign 接口,并声明需要调用的远程服务。

    开发人员首先创建一个 Java 接口,在接口上添加 @FeignClient 注解,通过 name 属性指定需要调用的目标服务。例如 @FeignClient(name = "user-service") 表示当前接口用于调用名为 user-service 的远程服务。然后在接口方法上使用 @GetMapping@PostMapping 等 Spring MVC 注解描述远程接口的请求方式和请求路径,使用 @PathVariable@RequestParam@RequestBody 等注解说明参数应该放在请求路径、查询参数还是请求体中。此时开发人员只定义接口,不需要手动编写实现类,也不需要自己使用 RestTemplate 拼接 URL、封装请求和解析响应。

  2. Spring Boot 启动时扫描 Feign 接口,并为接口创建动态代理对象。

    项目启动时,@EnableFeignClients 会开启 Feign 客户端扫描功能,Spring 会查找指定包下所有添加了 @FeignClient 的接口。扫描到接口后,OpenFeign 会解析接口上的服务名称、请求路径、请求方式、请求参数、请求头、请求体和返回值类型等信息,并将这些内容保存为方法元数据。例如,OpenFeign 会记录 getUserById 方法需要向 user-service 发送一个 GET 请求,请求路径为 /user/{id},方法参数需要填充到路径中,响应结果需要转换成 User 对象。解析完成后,OpenFeign 通过 JDK 动态代理为这个接口创建代理对象,并将代理对象注册到 Spring 容器中。因此,业务类中通过 @Autowired 或构造器注入的 UserClient,实际上不是开发人员编写的实现类,而是 OpenFeign 自动创建的代理对象。

  3. 业务代码调用 Feign 接口方法时,动态代理会拦截本次方法调用。

    当业务代码执行 userClient.getUserById(1L) 时,表面上看是在调用一个普通的 Java 接口方法,实际上会进入 OpenFeign 创建的动态代理对象。代理对象会根据当前调用的方法找到启动时保存的方法元数据,确定目标服务是 user-service、请求方式是 GET、请求路径是 /user/{id}、实际参数 id 的值是 1,以及返回值应该转换成 User 类型。也就是说,OpenFeign 的代理对象会把一次普通的 Java 方法调用,解释成一次远程 HTTP 调用任务。

  4. OpenFeign 根据方法元数据和实际参数构建 HTTP 请求。

    动态代理获得方法元数据后,会创建一个 RequestTemplate 请求模板,并将调用方法时传入的参数填充到模板中。例如,接口上声明的请求路径为 /user/{id},本次调用传入的 id=1,那么最终路径就会被解析为 /user/1。如果参数使用 @RequestParam,OpenFeign 会将参数拼接到 URL 后面;如果使用 @RequestHeader,参数会被放入请求头;如果使用 @RequestBody,编码器 Encoder 会将 Java 对象序列化成 JSON 等请求体内容。在请求发送之前,请求拦截器 RequestInterceptor 还可以统一添加 Token、用户身份、TraceId、公共请求头等信息。经过这些处理后,一次接口方法调用就被转换成了一份完整的 HTTP 请求。

  5. OpenFeign 根据服务名称,通过 Nacos 获取服务实例并完成负载均衡。

    Feign 接口中配置的是 user-service 这样的服务名称,而不是具体的 IP 地址和端口,所以 OpenFeign 此时还不能直接发送请求。它会结合 Spring Cloud LoadBalancer 和 Nacos 服务发现功能,根据 user-service 从消费者本地缓存或 Nacos 中获取可用实例列表。例如,Nacos 中可能存在 192.168.1.10:8081192.168.1.11:8081 两个健康实例。Spring Cloud LoadBalancer 会根据负载均衡策略从中选择一个具体实例,假设本次选择了 192.168.1.10:8081,那么原来的逻辑地址 http://user-service/user/1 就会被转换成真实地址 http://192.168.1.10:8081/user/1。这里需要注意,Nacos 负责提供实例列表,LoadBalancer 负责选择实例,OpenFeign 负责组织和发起远程调用。

  6. 底层 HTTP 客户端向选中的服务实例发送请求。

    获取真实 IP 和端口后,OpenFeign 会调用底层 HTTP Client,将已经构建好的请求发送给目标服务。底层客户端可以是 Feign 默认客户端,也可以配置为 OkHttp、Apache HttpClient 等实现。目标服务收到请求后,会按照正常的 Spring MVC 流程进入 Controller、Service 和数据库等业务逻辑。例如,user-service 收到 /user/1 请求后,根据用户编号查询用户信息,然后将查询结果封装成 JSON 响应返回。Nacos 不会参与这次业务请求的转发,真正的请求路径是消费者直接访问服务提供者。

  7. OpenFeign 接收响应,并将响应内容转换成 Java 对象。

    目标服务返回 HTTP 响应后,OpenFeign 会先检查响应状态码。如果请求成功,解码器 Decoder 会读取响应体,并根据 Feign 接口方法声明的返回值类型,将 JSON 数据反序列化成对应的 Java 对象。例如,远程服务返回用户 JSON,OpenFeign 会将其转换成 User 对象,然后交给动态代理。如果远程服务返回 400404500 等异常状态码,OpenFeign 可以通过 ErrorDecoder 将错误响应转换成 Java 异常;如果发生连接超时、读取超时等情况,则会根据项目中的超时、重试和容错配置进行处理。

  8. 动态代理将最终结果返回给业务调用方。

    响应数据经过 Decoder 转换完成后,Feign 动态代理会把转换后的 Java 对象返回给原来的业务代码。因此,调用方最终可以直接得到 User 对象,就像调用本地方法一样,不需要关心底层的 URL 拼接、请求参数转换、服务发现、负载均衡、HTTP 请求发送和 JSON 反序列化过程。如果项目配置了 Sentinel、CircuitBreaker 等容错组件,当远程服务调用失败时,还可以触发降级方法,返回默认结果,避免远程服务异常继续影响当前服务。

总结: OpenFeign 的完整工作过程就是:开发人员先使用 @FeignClient 定义远程调用接口;Spring Boot 启动时扫描接口、解析注解并创建动态代理对象;业务代码调用接口方法时,代理对象根据方法信息构建 HTTP 请求;然后通过 Nacos 获取目标服务实例,由负载均衡选择具体地址;底层 HTTP 客户端将请求发送给 Provider;收到响应后,Decoder 将响应转换成 Java 对象,最后由动态代理返回给调用方。

一句话理解:

OpenFeign 的本质,就是利用动态代理把 Java 接口方法调用自动转换成 HTTP 远程请求,并自动完成请求构建、服务发现、负载均衡、请求发送和响应解析。

OpenFeign 的基本工作流程

假设订单服务 cloud-order 需要调用用户服务 cloud-user。

text 复制代码
订单服务调用UserClient方法
        ↓
UserClient本身只有接口,没有实现类
        ↓
OpenFeign为接口创建动态代理对象
        ↓
代理对象读取@FeignClient
        ↓
确定目标服务名称cloud-user
        ↓
读取@GetMapping、@PostMapping等注解
        ↓
拼接请求路径和请求参数
        ↓
通过服务发现获取cloud-user实例列表
        ↓
Spring Cloud LoadBalancer选择一个服务实例
        ↓
发送HTTP请求
        ↓
cloud-user处理请求并返回数据
        ↓
OpenFeign解析响应内容
        ↓
转换成Java对象
        ↓
返回给订单服务

OpenFeign基本使用流程

第一阶段:项目启动时创建 Feign 代理对象

1. @EnableFeignClients 开启 Feign 功能

通常会在启动类上添加:

java 复制代码
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

@EnableFeignClients 的作用不是直接发送请求,而是告诉 Spring:启动时扫描项目中的 @FeignClient 接口,并为这些接口创建代理对象。

也就是说,如果没有启用 Feign 客户端扫描,Spring 就不会主动把这些接口注册成可以注入的 Bean。

2. 扫描所有 @FeignClient 接口

项目启动时,OpenFeign 会扫描指定包下的接口。例如扫描到:

java 复制代码
@FeignClient(name = "cloud-user")
public interface UserFeignClient {

    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

OpenFeign会读取:

text 复制代码
服务名称:cloud-user
接口类型:UserFeignClient
请求方式:GET
请求路径:/user/{id}
方法参数:id
返回类型:User

这些信息并不会立即产生 HTTP 请求,而是会被解析并保存到方法元数据中,等待以后真正调用接口方法时使用。

3. 为每个 FeignClient 准备一组组件

Spring Cloud会把每个 @FeignClient 看成一个"命名客户端"。例如:

java 复制代码
@FeignClient(name = "cloud-user")

这里的 cloud-user 不仅是服务名称,也是这个 Feign 客户端的重要标识。

Spring Cloud会为客户端准备一组组件,主要包括:

组件 作用
Contract 解析接口及方法上的注解
Encoder 将 Java 参数转换成 HTTP 请求数据
Decoder 将 HTTP 响应转换成 Java 对象
Client 真正执行 HTTP 请求
RequestInterceptor 在请求发送前统一处理请求
ErrorDecoder 处理非正常状态码
Retryer 控制是否重试
Feign.Builder 组装 Feign 客户端

Spring Cloud OpenFeign 默认使用 SpringMvcContract 解析 Spring MVC 注解,使用 SpringEncoder 编码请求,使用包装了 SpringDecoder 的解码器解析响应;存在 Spring Cloud LoadBalancer 时,会使用支持负载均衡的 Feign Client。

4. Contract 解析方法注解

Contract 可以理解为"注解解析规则"。在 Spring Cloud OpenFeign 中,主要由 SpringMvcContract 负责解析:

java 复制代码
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@RequestMapping
@PathVariable
@RequestParam
@RequestHeader
@RequestBody

例如:

java 复制代码
@PostMapping("/user")
User addUser(@RequestBody User user);

会被解析成类似下面的元数据:

java 复制代码
HTTP请求方式:POST
请求路径:/user
参数位置:请求体
请求体类型:User
返回值类型:User

再比如:

java 复制代码
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);

会被解析成:

text 复制代码
HTTP请求方式:GET
请求路径:/user/{id}
id参数需要替换路径中的{id}
返回结果需要转换成User对象

5. 创建接口代理对象

UserFeignClient 只是一个接口,没有实现类:

java 复制代码
public interface UserFeignClient {
}

但是我们却可以直接注入:

java 复制代码
@Autowired
private UserFeignClient userFeignClient;

原因是 OpenFeign 在启动过程中为接口创建了一个代理对象。可以理解为 OpenFeign 在运行时帮我们生成了一个"看不见的实现类":

java 复制代码
public class UserFeignClientProxy implements UserFeignClient {
    @Override
    public User getUserById(Long id) {
        // 根据注解生成HTTP请求
        // 获取服务实例
        // 发送HTTP请求
        // 解析响应结果
        // 返回User对象
    }
}

这段实现类并不是开发人员真正写出来的,只是为了帮助理解。OpenFeign 的实际实现会通过反射、方法元数据和代理机制完成这一过程。官方将其描述为:Feign会为带有相关注解的接口创建动态实现。

6. 把代理对象交给 Spring 管理

代理对象创建完成后,会被放入 Spring IOC 容器。所以业务类中才能正常注入:

java 复制代码
@Service
public class OrderService {

    @Autowired
    private UserFeignClient userFeignClient;
}

此时注入的不是接口本身,也不是服务提供者中的 Controller,而是:OpenFeign 为 UserFeignClient 创建的代理对象。到这里,项目启动阶段完成。

第二阶段:调用 Feign 接口时发送 HTTP 请求

现在执行:

java 复制代码
User user = userFeignClient.getUserById(1L);

整个调用流程如下。

1. 代理对象拦截方法调用

调用:

java 复制代码
userFeignClient.getUserById(1L);

看起来像普通 Java 方法调用,实际上会先进入 Feign 代理对象。代理对象会获取:

text 复制代码
当前调用的方法:getUserById
当前传入的参数:1L
当前方法对应的请求元数据

OpenFeign会根据方法找到启动阶段已经解析好的请求信息。

2. 根据元数据创建 RequestTemplate

OpenFeign会创建一个请求模板 RequestTemplate。这个模板中会逐步保存:

text 复制代码
HTTP请求方式
请求路径
查询参数
请求头
请求体
字符编码
目标服务名称

例如接口:

java 复制代码
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);

调用:getUserById(1L); 生成的请求模板类似:

text 复制代码
请求方式:GET
请求路径:/user/1
目标服务:cloud-user
返回类型:User

3. 处理方法参数

OpenFeign会根据参数上的注解,决定参数放到什么位置。

java 复制代码
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);

生成:/user/1
java 复制代码
@GetMapping("/user/list")
List<User> list(@RequestParam("name") String name,
                @RequestParam("age") Integer age);

生成:/user/list?name=张三&age=20
java 复制代码
@GetMapping("/user/info")
User getUser(@RequestHeader("Authorization") String token);

生成请求头:Authorization: Bearer xxxxx
java 复制代码
@PostMapping("/user")
User addUser(@RequestBody User user);

OpenFeign会使用编码器把 User 对象转换成 JSON:
{
  "id": 1,
  "name": "张三",
  "age": 20
}

4. Encoder 对请求参数进行编码

Encoder 负责把 Java 对象转换成 HTTP 请求可以传输的数据。例如:

java 复制代码
User user

经过 SpringEncoder 后可能转换成:

java 复制代码
{
  "name": "张三",
  "age": 20
}

请求头可能同时设置为:

java 复制代码
Content-Type: application/json

Spring Cloud OpenFeign会复用 Spring Web 中的 HttpMessageConverters,因此其 JSON 转换通常与 Spring MVC 使用的消息转换体系保持一致。可以简单理解为:

text 复制代码
Java对象
   ↓
Encoder
   ↓
JSON、表单、字符串或二进制请求体

5. 执行 RequestInterceptor

正式发送请求之前,会执行请求拦截器。例如我们配置一个拦截器,统一添加 Token:

java 复制代码
@Bean
public RequestInterceptor requestInterceptor() {
    return requestTemplate -> {
        requestTemplate.header("Authorization", "Bearer xxxxx");
    };
}

执行顺序可以理解为:

text 复制代码
生成RequestTemplate
        ↓
执行RequestInterceptor
        ↓
追加Token、请求头、链路信息
        ↓
形成最终请求

请求拦截器通常用于:

text 复制代码
传递登录Token
传递用户信息
添加请求ID
传递链路追踪信息
添加公共请求头
记录请求日志

第三阶段:OpenFeign 如何通过 Nacos 找到服务

这是 OpenFeign 原理中最容易混淆的地方。

1. name 是服务名称,不一定是域名

例如:

java 复制代码
@FeignClient(name = "cloud-user")
public interface UserFeignClient {
}

这里的:cloud-user,通常对应服务提供者配置的:

yml 复制代码
spring:
  application:
    name: cloud-user

服务提供者启动后,会把自己的服务信息注册到 Nacos,例如:

text 复制代码
服务名称:cloud-user
IP地址:192.168.1.10
端口:8081
健康状态:true
权重:1
集群:DEFAULT

Nacos 的服务注册信息本身就包含服务名、IP、端口、健康状态、权重和元数据等内容。

2. OpenFeign 不会把服务名直接发给网络

下面这个地址不能直接被普通网络识别:

text 复制代码
http://cloud-user/user/1

因为 cloud-user 不是普通 DNS 域名。因此,发送请求之前必须把:cloud-user转换成真正的:IP地址 + 端口 。例如:192.168.1.10:8081

3. 从服务发现组件获取实例列表

假设 Nacos 中有三个 cloud-user 实例:

text 复制代码
192.168.1.10:8081
192.168.1.11:8081
192.168.1.12:8081

服务消费者可以通过 Spring Cloud 的服务发现抽象获得这些实例。这里需要特别注意:

  1. Nacos负责保存和提供服务实例信息
  2. Spring Cloud LoadBalancer负责选择具体实例
  3. OpenFeign负责构造并发起远程HTTP调用

所以,更准确的关系是:

text 复制代码
OpenFeign
    ↓
Spring Cloud LoadBalancer
    ↓
Spring Cloud服务发现接口
    ↓
Nacos Discovery
    ↓
Nacos Server

Spring Cloud OpenFeign 在存在 LoadBalancer 时会使用负载均衡客户端;Nacos Discovery 则按照 Spring Cloud 的服务注册与发现规范接入,并向消费者提供服务实例。

4. LoadBalancer 选择一台服务实例

获得实例列表后,Spring Cloud LoadBalancer会按照负载均衡策略选择一个实例。例如选择:192.168.1.11:8081,于是原来的逻辑地址:http://cloud-user/user/1,会被转换成真正的请求地址:http://192.168.1.11:8081/user/1

最终发送的是这个真实地址。

5. Nacos 和 OpenFeign 的完整配合流程

text 复制代码
订单服务调用 UserFeignClient
              ↓
Feign代理对象拦截方法调用
              ↓
获得目标服务名 cloud-user
              ↓
Spring Cloud LoadBalancer请求服务实例
              ↓
Nacos Discovery提供cloud-user实例列表
              ↓
获得:
192.168.1.10:8081
192.168.1.11:8081
192.168.1.12:8081
              ↓
LoadBalancer选择一个实例
              ↓
选择192.168.1.11:8081
              ↓
拼接真实请求地址
              ↓
http://192.168.1.11:8081/user/1
              ↓
发送HTTP请求

第四阶段:底层 HTTP 客户端发送请求

服务实例选定后,才真正进入网络请求阶段。

OpenFeign本身主要负责:

text 复制代码
解析接口
生成代理
构造请求
组织调用流程

真正的网络通信由底层 Client 完成。

根据依赖和配置,可以使用不同的 HTTP 实现,例如 Feign 默认客户端、Apache HttpClient 5、OkHttp 或 HTTP/2 Client。Spring Cloud OpenFeign会根据类路径及配置选择相应实现。发送出去的请求可能类似:

yml 复制代码
GET /user/1 HTTP/1.1
Host: 192.168.1.11:8081
Authorization: Bearer xxxxx
Accept: application/json

第五阶段:服务提供者处理请求

请求到达服务提供者:

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }
}

服务提供者的处理过程与普通 HTTP 请求没有区别:

text 复制代码
Tomcat接收请求
      ↓
DispatcherServlet处理请求
      ↓
找到UserController
      ↓
调用getUserById()
      ↓
查询数据库
      ↓
返回User对象
      ↓
转换成JSON

响应:

text 复制代码
{
  "id": 1,
  "name": "张三",
  "age": 20
}

Decoder 解析响应

OpenFeign收到 HTTP 响应后,会交给 Decoder 处理。

接口方法返回值是:

java 复制代码
User getUserById(Long id);

因此解码器会根据这个返回类型,把 JSON:

text 复制代码
{
  "id": 1,
  "name": "张三",
  "age": 20
}

转换成:

text 复制代码
User user

整个转换过程:

text 复制代码
HTTP响应
   ↓
响应体JSON
   ↓
Decoder
   ↓
User对象
   ↓
返回给业务代码

因此业务代码最终拿到的不是字符串,而是一个正常的 Java 对象:

java 复制代码
User user = userFeignClient.getUserById(1L);
System.out.println(user.getName());

远程调用异常时如何处理

1. 正常响应,

例如:

text 复制代码
HTTP状态码:200
响应体:JSON

会进入:Decoder,然后转换成 Java 对象。

2. 异常响应

例如服务提供者返回:

text 复制代码
404
500
503

通常不会按照正常返回值直接解码,而会进入错误处理流程:

text 复制代码
HTTP异常状态码
      ↓
ErrorDecoder
      ↓
转换成FeignException或自定义异常
      ↓
抛给业务代码

3. 熔断和降级

配置 Spring Cloud CircuitBreaker 后,调用失败还可以进入降级逻辑。例如:

java 复制代码
@FeignClient(name = "cloud-user",fallback = UserFeignClientFallback.class)
public interface UserFeignClient {
    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

降级类:

java 复制代码
@Component
public class UserFeignClientFallback implements UserFeignClient {
    @Override
    public User getUserById(Long id) {
        User user = new User();
        user.setId(id);
        user.setName("默认用户");
        return user;
    }
}

调用过程:

text 复制代码
调用远程服务
      ↓
调用成功
      ├── 是:返回真实结果
      ↓ 否
触发熔断或降级
      ↓
执行fallback
      ↓
返回默认结果

Spring Cloud OpenFeign可以与 Spring Cloud CircuitBreaker 集成,但是否启用、使用哪种实现,要看项目依赖和配置。

客户端负载均衡 VS 服务端负载均衡

1、客户端负载均衡

  1. 服务提供者将多个服务实例注册到注册中心。

    假设商品服务部署了三个实例,分别是商品服务1、商品服务2和商品服务3。每个实例启动后,都会将自己的服务名称、IP、端口、健康状态、权重和元数据等信息注册到 Nacos。例如,三个实例的服务名称都为 product-service,但真实地址分别为 192.168.1.10:8081192.168.1.11:8081192.168.1.12:8081。Nacos 负责保存这些实例信息,并通过心跳或长连接维护实例的健康状态。

  2. 订单服务调用商品服务前,先获取商品服务的实例列表。

    当订单服务通过 OpenFeign 调用 product-service 时,代码中使用的是服务名称,而不是某个固定的 IP 地址。订单服务中的 Nacos Client 会根据 product-service 查询可用实例列表。Nacos 会过滤掉已经下线、被禁用或者不健康的实例,然后将健康实例返回给订单服务。实例列表通常会缓存在订单服务本地,因此后续调用一般直接读取本地缓存,不需要每次请求都访问 Nacos。

  3. 订单服务在本地通过负载均衡算法选择一个实例。

    当订单服务拿到多个商品服务实例后,Spring Cloud LoadBalancer 会在订单服务内部执行负载均衡,根据轮询、随机、权重或其他策略选择一个具体实例。假设本次选择了 192.168.1.11:8081,那么 OpenFeign 原来的逻辑调用地址 http://product-service/product/1,就会被转换成真实地址 http://192.168.1.11:8081/product/1

  4. 订单服务直接向选中的商品服务实例发送请求。

    实例选择完成后,订单服务会直接向 192.168.1.11:8081 发起 HTTP 远程调用,Nacos 不参与业务请求的转发。下一次调用时,订单服务可能根据负载均衡算法选择另一个实例。当商品服务新增、下线或者发生故障时,Nacos 会通知订单服务更新本地实例列表,订单服务后续只从最新的健康实例中进行选择。

总结:客户端负载均衡就是由服务调用方自己获取实例列表、自己选择实例,然后直接调用该实例。


2、服务端负载均衡

  1. 客户端只知道负载均衡器的统一地址。

    在服务端负载均衡模式下,浏览器、App、小程序或者其他调用方,不需要知道订单服务部署了多少个实例,也不需要知道每个实例的真实 IP 和端口。客户端只需要访问 Nginx、网关或其他负载均衡器提供的统一地址,例如 https://api.example.com/order/query

  2. 所有请求先发送到负载均衡器。

    当浏览器、App 或小程序发送请求时,请求不会直接到达某一个订单服务实例,而是先到达 Nginx 或网关。对于客户端来说,Nginx 或网关就是唯一的服务入口,客户端不负责获取订单服务实例列表,也不负责选择具体实例。

  3. 负载均衡器从多个服务实例中选择一个。

    Nginx 或网关内部维护订单服务1、订单服务2和订单服务3的地址,也可以通过注册中心动态获取实例列表。当请求到达后,负载均衡器根据轮询、随机、权重、最少连接数或一致性哈希等策略选择一个订单服务实例。例如,本次选择订单服务2,那么负载均衡器就会把请求转发到订单服务2。

  4. 目标服务处理请求,并通过负载均衡器返回结果。

    订单服务2收到请求后执行具体业务逻辑,然后将响应返回给 Nginx 或网关,再由 Nginx 或网关把结果返回给原来的客户端。整个过程中,客户端只和负载均衡器通信,并不知道最终是哪一个订单服务实例处理了请求。下一次请求到达时,负载均衡器可能选择订单服务1或订单服务3。

总结:服务端负载均衡就是客户端先请求负载均衡器,再由负载均衡器选择服务实例并转发请求。


二者的核心区别

  1. 客户端负载均衡由调用方选择实例。

    例如订单服务调用商品服务时,订单服务先从 Nacos 获取商品服务实例列表,再由订单服务内部的 Spring Cloud LoadBalancer 选择一个实例,最后直接向该实例发送请求。

  2. 服务端负载均衡由中间的负载均衡器选择实例。

    例如浏览器请求订单服务时,浏览器先把请求发送给 Nginx 或网关,再由 Nginx 或网关选择一个订单服务实例,并将请求转发过去。

  3. 判断方式非常简单:谁负责选择具体的服务实例,谁就是负载均衡的执行者。

text 复制代码
客户端自己选择实例
    ↓
客户端负载均衡

Nginx或网关选择实例
    ↓
服务端负载均衡

一句话概括:

客户端负载均衡是"调用方自己选实例并直接调用",服务端负载均衡是"调用方先请求负载均衡器,再由负载均衡器选择实例并转发"。

OpenFeign 日志

OpenFeign 日志主要用于查看一次远程调用中的:

text 复制代码
请求方式
请求地址
请求头
请求参数
请求体
响应状态码
响应头
响应结果
请求耗时

例如订单服务调用:

java 复制代码
userFeignClient.getUserById(1L);

开启日志后,就能看到 OpenFeign 最终生成了什么 HTTP 请求,以及用户服务返回了什么内容。


1、OpenFeign 日志的四个级别

OpenFeign 提供四种日志级别:

日志级别 记录内容
NONE 不记录任何日志,默认级别
BASIC 记录请求方式、URL、响应状态码和调用耗时
HEADERS 在 BASIC 基础上,增加请求头和响应头
FULL 记录请求和响应的请求头、请求体、响应体等全部信息

官方说明:每个 Feign 客户端都会创建一个 Logger,Logger 默认以 Feign 接口的完整类名 命名,而且 Feign 日志只响应 DEBUG 级别(spring的日志级别需要配debug级别)。

开发环境通常使用:FULL。生产环境建议使用:BASIC

2、开启 OpenFeign 日志

OpenFeign 日志需要同时配置两个地方:

text 复制代码
1. 将Feign接口的Spring日志级别设置为DEBUG

2. 设置Feign自己的日志详细级别

注意:两个配置缺一不可。

1. 配置 application.yml

你的 OpenFeign 接口是:

java 复制代码
package com.example.order.feign;

@FeignClient(name = "cloud-user")
public interface UserFeignClient {
    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

那么在订单服务 cloud-orderapplication.yml 中添加:

yaml 复制代码
server:
  port: 8002

spring:
  application:
    name: cloud-order
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

logging:
  level:
    com.example.order.feign.UserFeignClient: DEBUG # 开启指定Feign接口的DEBUG日志

feign:
  client:
    config:
      cloud-user:
        loggerLevel: FULL # 记录cloud-user客户端的完整请求和响应日志

这里有两个关键配置。

  • Spring 日志开关

    yaml 复制代码
    logging:
      level:
        com.example.order.feign.UserFeignClient: DEBUG

    这里必须填写 Feign 接口的:

    text 复制代码
    完整包名 + 接口名称

    也就是:

    text 复制代码
    com.example.order.feign.UserFeignClient

    如果包名写错,控制台不会输出 Feign 日志。

  • Feign 日志级别

    yaml 复制代码
    feign:
      client:
        config:
          cloud-user:
            loggerLevel: FULL

    这里的:

    text 复制代码
    cloud-user

    对应:

    java 复制代码
    @FeignClient(name = "cloud-user")

    对应关系:

    text 复制代码
    @FeignClient中的name
               ↓
    feign.client.config下面的客户端名称

为所有 Feign 客户端开启日志

如果项目中有很多 Feign 接口:

text 复制代码
UserFeignClient
ProductFeignClient
StockFeignClient
PayFeignClient

完整配置:

yaml 复制代码
logging:
  level:
    com.example.order.feign: DEBUG # 开启整个feign包下面的DEBUG日志

feign:
  client:
    config:
      default:
        loggerLevel: FULL # 所有Feign客户端都使用FULL日志

这样 com.example.order.feign 包下面的所有 Feign 接口都会输出完整日志。

使用 Java 配置类开启日志

除了在配置文件中设置,也可以创建 Java 配置类。

1. 创建 FeignLogConfig

java 复制代码
package com.example.order.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignLogConfig {

    @Bean
    public Logger.Level feignLoggerLevel() {

        return Logger.Level.FULL; // 输出完整的请求和响应日志
    }
}

Logger.Level.FULL 表示记录:

text 复制代码
请求方式
请求地址
请求头
请求体
响应状态码
响应头
响应体
请求耗时

官方也提供了通过注册 Logger.Level Bean 将日志设置为 FULL 的方式。

2. 在 Feign 接口中引用配置类

java 复制代码
package com.example.order.feign;

import com.example.order.config.FeignLogConfig;
import com.example.order.domain.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "cloud-user",configuration = FeignLogConfig.class)
public interface UserFeignClient {

    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

但是 application.yml 中仍然需要开启 DEBUG

yaml 复制代码
logging:
  level:
    com.example.order.feign.UserFeignClient: DEBUG

调用接口后的日志效果

控制台会输出类似日志:

text 复制代码
[UserFeignClient#getUserById] ---> GET http://cloud-user/user/1 HTTP/1.1
[UserFeignClient#getUserById] Accept: application/json
[UserFeignClient#getUserById] ---> END HTTP (0-byte body)

这部分表示 OpenFeign 正在发送请求:

text 复制代码
调用方法:getUserById
请求方式:GET
服务名称:cloud-user
请求路径:/user/1
请求体长度:0

用户服务返回结果后,会继续输出:

text 复制代码
[UserFeignClient#getUserById] <--- HTTP/1.1 200 (35ms)
[UserFeignClient#getUserById] content-type: application/json
[UserFeignClient#getUserById]
{"id":1,"username":"张三","age":20,"phone":"13800000000"}
[UserFeignClient#getUserById] <--- END HTTP (75-byte body)

这部分表示:

text 复制代码
HTTP状态码:200
调用耗时:35毫秒
响应类型:application/json
响应内容:用户JSON数据
响应体大小:75字节

OpenFeign 超时控制

OpenFeign 超时控制用于限制一次远程调用最多等待多长时间,避免服务消费者一直阻塞。例如订单服务调用用户服务:

java 复制代码
User user = userFeignClient.getUserById(1L);

假如用户服务宕机、网络不通,或者接口处理速度特别慢,如果不设置合理的超时时间,订单服务中的线程就可能长时间等待,最终导致大量请求堆积。

OpenFeign 主要有两种超时:

text 复制代码
连接超时:connectTimeout
读取超时:readTimeout

OpenFeign 支持为全部客户端设置默认超时,也支持根据 @FeignClient 的客户端名称单独设置。

一、连接超时 connectTimeout

连接超时表示: OpenFeign 尝试和远程服务器建立网络连接时,最多可以等待多长时间。 例如订单服务准备调用:

text 复制代码
cloud-user

通过 Nacos 和负载均衡获得真实实例:

text 复制代码
192.168.1.10:8001

然后准备与该地址建立连接:

text 复制代码
订单服务
    ↓
尝试连接192.168.1.10:8001
    ↓
在规定时间内是否连接成功

假设配置:

yaml 复制代码
connectTimeout: 3000

表示最多等待:

text 复制代码
3000毫秒,也就是3秒

如果超过 3 秒仍然没有建立连接,OpenFeign 就会抛出连接超时异常。常见场景:

text 复制代码
1. 服务提供者已经宕机
2. 服务提供者IP或端口无法访问
3. 网络出现故障
4. 防火墙阻止连接
5. 服务提供者连接压力过大
6. 负载均衡选中的服务实例不可用

完整过程:

text 复制代码
OpenFeign准备发送请求
        ↓
通过Nacos获取服务实例
        ↓
LoadBalancer选择一个实例
        ↓
尝试建立网络连接
        ↓
3秒内连接成功
        ├── 是:继续发送请求
        ↓ 否
抛出连接超时异常

二、读取超时 readTimeout

读取超时表示:OpenFeign 已经成功连接到服务提供者,但是等待服务提供者返回响应的时间过长。

假设配置:

yaml 复制代码
readTimeout: 5000

表示连接建立后,等待远程服务返回数据的时间最多为:

text 复制代码
5000毫秒,也就是5秒

调用过程:

text 复制代码
订单服务成功连接用户服务
        ↓
发送GET /user/1请求
        ↓
用户服务开始处理请求
        ↓
查询数据库、执行业务逻辑
        ↓
5秒内是否返回响应
        ├── 是:正常获得结果
        ↓ 否
抛出读取超时异常

官方文档说明,readTimeout 在连接建立后生效,当等待响应结果时间过长时触发。

常见场景:

text 复制代码
1. 服务提供者执行了耗时业务
2. SQL查询速度过慢
3. 数据库连接池没有可用连接
4. 服务提供者线程池繁忙
5. 服务提供者又调用了其他慢服务
6. 返回的数据量特别大
7. 网络传输响应数据过慢

三、两种超时的区别

超时类型 发生阶段 说明
connectTimeout 建立连接阶段 无法及时连接到远程服务器
readTimeout 等待响应阶段 已经连接成功,但远程服务返回太慢

四、Spring Boot 配置方式

可以在服务消费者的 application.yml 中这样配置:

yaml 复制代码
server:
  port: 8002

spring:
  application:
    name: cloud-order
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

feign:
  client:
    config:
      cloud-user:
        connectTimeout: 3000 # 建立连接最多等待3秒
        readTimeout: 5000 # 等待远程服务返回结果最多等待5秒
        loggerLevel: FULL # 输出完整Feign日志

配置中的:

yaml 复制代码
feign:
  client:
    config:
      cloud-user:

这里的 cloud-user 对应 Feign 接口中的:

java 复制代码
@FeignClient(name = "cloud-user")
public interface UserFeignClient {

    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

对应关系:

text 复制代码
@FeignClient(name = "cloud-user")
                  ↓
feign.client.config.cloud-user

所以:

yaml 复制代码
cloud-user:
  connectTimeout: 3000
  readTimeout: 5000

只会控制调用 cloud-user 服务的 Feign 客户端。

五、为所有 OpenFeign 客户端设置超时

假如订单服务中有多个 Feign 接口:

text 复制代码
UserFeignClient
ProductFeignClient
StockFeignClient
PayFeignClient

可以使用 default 统一配置:

yaml 复制代码
feign:
  client:
    config:
      default:
        connectTimeout: 3000 # 所有Feign客户端连接超时3秒
        readTimeout: 5000 # 所有Feign客户端读取超时5秒

default 表示:

text 复制代码
所有没有单独配置超时时间的Feign客户端,都使用这里的默认超时配置。

官方文档也支持使用 default 为全部 Feign 客户端设置统一配置。

六、默认配置和单独配置同时存在

yaml 复制代码
feign:
  client:
    config:

      default:
        connectTimeout: 3000 # 默认连接超时3秒
        readTimeout: 5000 # 默认读取超时5秒

      cloud-user:
        connectTimeout: 2000 # 用户服务连接超时2秒
        readTimeout: 8000 # 用户服务读取超时8秒

      cloud-pay:
        connectTimeout: 3000 # 支付服务连接超时3秒
        readTimeout: 15000 # 支付服务读取超时15秒

执行效果:

text 复制代码
调用cloud-user
    ↓
连接超时2秒
读取超时8秒

调用cloud-pay
    ↓
连接超时3秒
读取超时15秒

调用其他服务
    ↓
连接超时3秒
读取超时5秒

指定客户端的配置会覆盖 default 中相同的配置项。

七、超时后服务提供者会停止执行吗

不会自动停止。

例如:

text 复制代码
用户服务需要执行8秒
OpenFeign只等待5秒

执行过程可能是:

text 复制代码
第0秒:订单服务发起请求
        ↓
用户服务开始处理

第5秒:订单服务读取超时
        ↓
订单服务抛出异常,不再等待

第8秒:用户服务仍然可能执行完成
        ↓
但是订单服务已经不接收这个结果

也就是说:

OpenFeign 超时主要是让调用方停止等待,并不代表服务提供者中的业务线程一定会立即停止。

因此,涉及扣款、下单、库存等操作时,要特别注意:请求超时不等于业务执行失败

例如支付服务调用超时,不能直接认为"支付一定失败",应当通过订单号查询支付状态,并保证接口具有幂等性。

八、超时后的异常处理

可以在业务层捕获异常:

java 复制代码
package com.example.order.service.impl;

import com.example.order.domain.User;
import com.example.order.feign.UserFeignClient;
import feign.RetryableException;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl {

    private final UserFeignClient userFeignClient;

    public OrderServiceImpl(UserFeignClient userFeignClient) {
        this.userFeignClient = userFeignClient;
    }

    public User getOrderUser(Long userId) {

        try {
            return userFeignClient.getUserById(userId);
        } catch (RetryableException e) {
            // Feign连接超时或读取超时可能进入这里
            throw new RuntimeException("用户服务调用超时,请稍后重试", e);
        }
    }
}

不过实际项目中,更推荐统一使用:

text 复制代码
全局异常处理
+
熔断降级
+
统一返回结果

而不是每次调用都写一遍 try-catch

九、配合 fallback 降级

Feign 接口:

java 复制代码
package com.example.order.feign;

import com.example.order.domain.User;
import com.example.order.feign.fallback.UserFeignClientFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "cloud-user", fallback = UserFeignClientFallback.class)
public interface UserFeignClient {

    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

降级实现:

java 复制代码
package com.example.order.feign.fallback;

import com.example.order.domain.User;
import com.example.order.feign.UserFeignClient;
import org.springframework.stereotype.Component;

@Component
public class UserFeignClientFallback implements UserFeignClient {

    @Override
    public User getUserById(Long id) {

        User user = new User();
        user.setId(id);
        user.setUsername("用户服务暂时不可用");

        return user;
    }
}

调用过程:

text 复制代码
调用cloud-user
        ↓
是否在超时时间内返回
        ├── 是:返回真实用户数据
        ↓ 否
抛出超时异常
        ↓
触发熔断降级
        ↓
执行UserFeignClientFallback
        ↓
返回默认用户信息

需要注意,只有项目正确引入并启用了 Spring Cloud CircuitBreaker 等熔断组件后,fallback 才会生效;OpenFeign 的超时和熔断器的超时属于不同层次的控制。

十、超时时间应该设置多长

没有一个适用于所有项目的固定数值,可以先根据接口类型设置。

一般建议:

接口类型 连接超时 读取超时
普通查询接口 2~3秒 3~5秒
普通新增、修改接口 2~3秒 5~10秒
文件上传、下载 3~5秒 根据文件大小单独配置
报表、复杂统计 3~5秒 10~30秒或改为异步任务
支付等关键接口 根据实际监控设置 超时后必须查询最终状态

OpenFeign 重试机制

OpenFeign 重试机制是指: 当一次远程调用因为网络异常、连接超时、读取超时等原因失败后,OpenFeign 可以按照设定的次数和时间间隔,再次发送相同请求。 例如订单服务调用用户服务:

java 复制代码
User user = userFeignClient.getUserById(1L);

第一次调用发生超时:

text 复制代码
第一次调用cloud-user
        ↓
发生网络异常或读取超时
        ↓
判断是否允许重试
        ↓
允许重试
        ↓
等待一段时间
        ↓
再次调用cloud-user

最重要的结论:Spring Cloud OpenFeign 默认不重试

在 Spring Cloud OpenFeign 中,默认创建的是:

java 复制代码
Retryer.NEVER_RETRY

也就是:

text 复制代码
远程调用失败
      ↓
不重新发送请求
      ↓
直接抛出异常

这与原生 Feign 不同:原生 Feign 默认可以针对 IOExceptionErrorDecoder 抛出的 RetryableException 执行重试,而 Spring Cloud OpenFeign 默认使用 Retryer.NEVER_RETRY 将重试关闭。

因此,只配置:

yaml 复制代码
connectTimeout: 3000
readTimeout: 5000

并不代表超时以后会自动重试。

重试机制中的核心组件

OpenFeign 重试主要涉及三个组件:

组件名称 作用
RetryableException 表示当前异常允许重试
Retryer 决定是否继续重试、重试次数和间隔
ErrorDecoder 可以把503等HTTP异常转换成RetryableException

1. RetryableException

RetryableException 表示:

text 复制代码
当前失败可能只是临时故障,再次发送请求可能会成功。

例如:

text 复制代码
连接超时
读取超时
连接被重置
临时网络波动
503服务暂时不可用

Feign 的执行方法会捕获 RetryableException,然后把它交给 Retryer 判断是否继续。

2. Retryer

Retryer 负责决定:

text 复制代码
是否继续重试
最多重试多少次
每次重试等待多久
什么时候停止重试并抛出异常

核心方法:

java 复制代码
void continueOrPropagate(RetryableException e);

可以理解为:

text 复制代码
允许继续重试
    ↓
方法正常结束

不允许继续重试
    ↓
把RetryableException重新抛出

每次 Feign 方法调用都会复制一个独立的 Retryer,因此每个请求有自己的重试次数,不会和其他并发请求共用计数。

3. ErrorDecoder

远程服务返回非 2xx 状态码时,例如:

text 复制代码
400
404
500
503

会进入 ErrorDecoder。普通 HTTP 500、503 并不会仅仅因为状态码就必然重试。要针对某个状态码重试,可以在 ErrorDecoder 中将其转换成:

java 复制代码
RetryableException

Feign 官方也说明:需要让某个错误响应进入重试时,应当由 ErrorDecoder 返回 RetryableException,随后调用已注册的 Retryer

OpenFeign 重试完整执行流程

text 复制代码
1. 业务层调用Feign接口。
2. Feign代理对象生成HTTP请求。
3. LoadBalancer选择一个服务实例。
4. Feign向远程服务发送请求。
5. 判断调用是否成功。
6. 如果调用成功,解析响应并返回结果。
7. 如果发生IOException,Feign将其包装为RetryableException。
8. 如果返回503等状态码,则交给ErrorDecoder处理。
9. ErrorDecoder如果返回普通异常,不执行Feign重试。
10. ErrorDecoder如果返回RetryableException,则进入重试流程。
11. Feign捕获RetryableException。
12. 调用Retryer.continueOrPropagate()。
13. Retryer判断当前尝试次数是否达到上限。
14. 没达到上限,等待一段时间后重新发送请求。
15. 达到上限,抛出最后一次RetryableException。
16. 如果配置了fallback,可以继续进入降级处理。

Feign 内部本质上是一个循环:

java 复制代码
while (true) {
    发送请求

    成功:
        返回结果并结束循环

    捕获RetryableException:
        Retryer判断是否继续

        继续:
            再次执行循环

        停止:
            抛出异常
}

OpenFeign 的同步方法处理器确实会循环执行请求,捕获 RetryableException 后调用 Retryer,允许重试时继续循环,达到上限后抛出异常。

开启 OpenFeign 重试

1. 创建重试配置类

订单服务中创建:

text 复制代码
com.example.order.config.FeignRetryConfig

代码:

java 复制代码
package com.example.order.config;

import feign.Retryer;
import org.springframework.context.annotation.Bean;

public class FeignRetryConfig {

    @Bean
    public Retryer retryer() {

        return new Retryer.Default(
                1000L, // 重试基础间隔参数:1000毫秒
                3000L, // 单次等待时间最大不超过3000毫秒
                3      // 最多总共尝试3次,包含第一次请求
        );
    }
}

注意这里没有添加:

java 复制代码
@Configuration

目的是避免这个配置被主工程扫描后,意外对所有 Feign 客户端生效。 也就是说加上@Configuration那么所有的Feign接口都会重试。

java 复制代码
new Retryer.Default(
        period,
        maxPeriod,
        maxAttempts
);
  • period
    重试等待时间。请求失败后,不要马上再次请求,先等待一段时间。这个参数是计算等待时间的基础值。Retryer.Default 会逐渐增加等待时间 ,每次请求等待时间都会 * 1.5。第一次重试1000毫秒,第二次1000毫秒1.5,第三次1000毫秒1.5*1.5。
  • maxPeriod
    表示重试等待时间的最大值。无论怎么*1.5。不能超过等待时间最大值。
  • maxAttempts
    调用总共最多尝试次数。注意:这里包含最开始的第一次正常请求。如果是3,那么第一次正常调用,两次重试。

2. Feign 接口引用重试配置

java 复制代码
package com.example.order.feign;

import com.example.order.config.FeignRetryConfig;
import com.example.order.domain.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "cloud-user",configuration = FeignRetryConfig.class // 只给当前Feign客户端配置重试
)
public interface UserFeignClient {

    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

此时:

text 复制代码
UserFeignClient
    ↓
最多尝试3次

其他Feign客户端
    ↓
没有引用这个配置
    ↓
仍然默认不重试

Spring Cloud OpenFeign 支持通过 @FeignClient(configuration = ...) 覆盖默认的 Retryer

针对 HTTP 503 进行重试

仅仅启用 Retryer,并不表示所有 500、503 都会自动重试。可以创建 ErrorDecoder

java 复制代码
package com.example.order.config;

import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;

public class CustomErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {

        if (response.status() == 503) {

            return new RetryableException(
                    response.status(),
                    "用户服务暂时不可用",
                    response.request().httpMethod(),
                    null,
                    response.request()
            ); // 将503转换成可重试异常
        }
        return defaultErrorDecoder.decode(methodKey, response); // 其他状态码按默认方式处理
    }
}

然后加入 Feign 配置:

java 复制代码
package com.example.order.config;

import feign.Retryer;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;

public class FeignRetryConfig {

    @Bean
    public Retryer retryer() {

        return new Retryer.Default(
                1000L, // 重试基础间隔
                3000L, // 最大等待间隔
                3      // 最大尝试3次,包含第一次
        );
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder(); // 将指定状态码转换成可重试异常
    }
}

哪些情况会重试

开启自定义 Retryer 后,常见可进入重试的情况包括:

异常情况 是否可能重试
连接超时 可以
读取超时 可以
Connection reset 可以
网络连接中断 可以
普通业务异常 默认不重试
HTTP 400 默认不重试
HTTP 404 默认不重试
HTTP 500 默认不重试
HTTP 503 需要 ErrorDecoder 转成 RetryableException
参数校验失败 不应重试
数据不存在 不应重试

原生 Feign 会将 IOException 视为临时网络异常,并通过重试器处理;HTTP 错误状态则需要由错误解码器决定是否包装成 RetryableException

建议原则:

text 复制代码
1. 查询接口可以少量重试,一般总尝试2~3次。
2. 新增、支付、扣库存接口不要随便重试。
3. 重试必须设置等待间隔,避免瞬间连续请求。
4. 重试次数不能太多,防止请求线程长时间阻塞。
5. 超时时间乘以尝试次数,才接近最差总耗时。
6. 503等HTTP状态码需要结合ErrorDecoder控制。
7. 重试只能解决偶发故障,不能解决服务长期宕机。
8. 服务长期异常应当使用熔断和降级。

OpenFeign 拦截器

OpenFeign 拦截器的作用是:在 OpenFeign 发送 HTTP 请求之前,对即将发送的请求进行统一处理。

请求拦截器使用

最常见案例:传递 Token

假设浏览器调用订单服务时携带 Token:

text 复制代码
GET /order/1001 HTTP/1.1
Authorization: Bearer abc123

订单服务接收到请求后,又需要通过 OpenFeign 调用用户服务:

text 复制代码
User user = userFeignClient.getUserById(1L);

默认情况下,浏览器传给订单服务的 Token不会自动传给用户服务。因为这是两次独立的 HTTP 请求。因此需要使用 OpenFeign 请求拦截器,把第一个请求中的 Token 取出来,再放入第二个请求。

1、创建拦截器

java 复制代码
package com.example.order.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

public class FeignTokenInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {

        // 1. 获取当前线程绑定的请求信息
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        // 2. 当前没有HTTP请求时,直接结束
        if (!(requestAttributes instanceof ServletRequestAttributes)) {
            return;
        }

        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        // 3. 获取浏览器发送给当前服务的HttpServletRequest
        HttpServletRequest request = servletRequestAttributes.getRequest();

        // 4. 从当前请求头中获取Token
        String authorization = request.getHeader("Authorization");

        // 5. Token存在时,添加到Feign即将发送的请求中
        if (StringUtils.hasText(authorization)) {
            template.header("Authorization", authorization);
        }
    }
}

2、创建 Feign 配置类

java 复制代码
package com.example.order.config;

import com.example.order.interceptor.FeignTokenInterceptor;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;

public class FeignConfig {

    @Bean
    public RequestInterceptor feignTokenInterceptor() {

        return new FeignTokenInterceptor(); // 注册Feign请求拦截器
    }
}

这里可以不添加:@Configuration。因为这个配置类准备通过:

java 复制代码
@FeignClient(configuration = FeignConfig.class)

单独交给某一个 Feign 客户端使用。

3、 Feign 接口引用配置

java 复制代码
package com.example.order.feign;

import com.example.order.config.FeignConfig;
import com.example.order.domain.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "cloud-user", configuration = FeignConfig.class)
public interface UserFeignClient {
    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

此时,这个拦截器只作用于:UserFeignClient

Fallback-兜底返回

特别强调:此功能需要整合断路器才能实现,比如常见的Sentinel。
特别强调:此功能需要整合断路器才能实现,比如常见的Sentinel。
特别强调:此功能需要整合断路器才能实现,比如常见的Sentinel。

什么是兜底返回

兜底返回就是:OpenFeign 调用远程服务失败时,不把异常直接抛给用户,而是执行提前准备好的备用方法,返回一个可预期的结果。

例如订单服务调用商品服务:

java 复制代码
Product product = productFeignClient.getProductById(1L);

正常情况下返回真实商品:

java 复制代码
{
  "id": 1,
  "name": "机械键盘",
  "price": 399
}

如果商品服务宕机,兜底逻辑可以返回:

java 复制代码
{
  "id": 1,
  "name": "商品信息暂时无法获取",
  "price": null
}

核心流程:

text 复制代码
调用商品服务
      ↓
调用成功
      ├── 返回真实商品信息
      ↓ 调用失败
执行Fallback兜底方法
      ↓
返回默认值、空集合或友好提示

为什么需要兜底返回

  1. 防止远程服务异常直接传给用户,这种错误信息用户看不懂,也不应该直接暴露给用户。

  2. 避免一个服务故障拖垮整条业务链

    微服务之间通常存在多层调用如果仓库服务故障,并且没有任何兜底处理,异常可能沿着调用链不断向上传递,最终可能出现:一个不重要的下游服务故障,导致整个订单页面都打不开。有了兜底后,可以把异常控制在当前服务中。

  3. 返回统一、可预期的结果

    没有兜底时,失败结果可能非常混乱:

    text 复制代码
    连接超时异常
    读取超时异常
    FeignException
    服务实例不存在
    503异常
    500异常

    业务层需要面对各种不同异常。配置兜底后,可以统一转换成:

    text 复制代码
    {
      "code": 503,
      "message": "商品服务暂时不可用,请稍后重试",
      "data": null
    }

    调用方拿到的是统一格式,前端也更容易处理。

  4. 给系统留出恢复时间

    当远程服务已经发生严重故障时,继续大量调用只会让情况更加糟糕:

    text 复制代码
    商品服务已经很慢
            ↓
    订单服务继续不断请求
            ↓
    商品服务线程越来越多
            ↓
    请求越来越慢
            ↓
    最终彻底崩溃
    
    如果配合熔断机制:
    
    连续多次调用失败
            ↓
    熔断器打开
            ↓
    暂时不再调用商品服务
            ↓
    直接执行Fallback

    这样可以减少对故障服务的压力,让它有机会恢复。




下一个章节,我们将用一个完整的案例,来实现上面介绍的OpenFeign的所有功能。

相关推荐
减瓦1 小时前
深入 Quarkus:云原生时代 Java 的重生之路
java·开发语言·云原生
杨运交1 小时前
[053][核心模块]Java枚举缓存与ORM集成实践
java·开发语言·缓存
繁星蓝雨2 小时前
C++设计原理——异常处理
java·c++·异常处理·noexcept·throw·try catch
BerrySen1782 小时前
一个Java项目改成AI流程后,最难的部分完全变了
java·大数据·人工智能·可观测性·大模型应用开发·工程思维
程序猿秃头之路2 小时前
DDD 系列:DTO、VO、DO、Entity 怎么区分
java·ddd·领域驱动设计
自由随风飘3 小时前
JAVA中的面向对象-3
java·开发语言
桐薇全肯定3 小时前
MySQL学生成绩管理系统实战操作
java·数据库·sql
beibeix20154 小时前
MITK中两种微服务的三层架构实现对比
微服务·mitk