Fegin 实践

使用 Fegin 的步骤编写 Http 客户端的步骤

首先肯定要添加项目依赖,你需要什么放什么,我都放出来了在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<dependencies>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!-- 服务注册发现(以 Eureka 为例)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!-- 熔断器(可选)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>

然后需要启用 Fegin 客户端,在主启动类上添加 @EnableFeignClients 注解:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

接下来需要继续创建 Feign 客户端接口,这是 Feign 的核心,通过接口定义来声明 HTTP 调用:

与 RestTemple 不同的是, Feign 的接口需要在上面标注注解,来表明你的请求方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@FeignClient(
name = "user-service", // 服务名称
path = "/api/v1", // 基础路径
fallback = UserServiceFallback.class // 降级处理类
)
public interface UserServiceClient {

// GET 请求
@GetMapping("/users/{id}")
UserDTO getUserById(@PathVariable("id") Long id);

// GET 请求带查询参数
@GetMapping("/users")
List<UserDTO> getUsers(
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam(value = "name", required = false) String name
);

// POST 请求
@PostMapping("/users")
UserDTO createUser(@RequestBody CreateUserRequest request);

// PUT 请求
@PutMapping("/users/{id}")
UserDTO updateUser(
@PathVariable("id") Long id,
@RequestBody UpdateUserRequest request
);

// DELETE 请求
@DeleteMapping("/users/{id}")
void deleteUser(@PathVariable("id") Long id);

// 带请求头的请求
@GetMapping("/users/profile")
UserDTO getUserProfile(@RequestHeader("Authorization") String token);
}

到这里,基本的 http 客户端的控制器已经构建出来了,但是接下来我们继续看看如何做其他事情

使用 Feign 如何进行服务间的远程调用,在 Service 层直接注入 Feign 接口,调用其方法即可完成远程服务调用,这样 Feign 就把远程间的调用被简化为普通的方法调用,代码清晰且聚焦业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
public class OrderService {

// 注入 Feign 客户端接口
@Autowired
private UserServiceClient userClient;

/**
* 创建订单时,调用用户服务查询用户信息
*/
public OrderDTO createOrder(CreateOrderRequest request) {
// 1. 调用用户服务,查询用户详情(远程调用)
UserDTO user = userClient.getUserById(request.getUserId());
if (user == null) {
throw new RuntimeException("用户不存在");
}

// 2. 调用用户服务,检查用户状态(远程调用,带查询参数)
List<UserDTO> validUsers = userClient.getUsers(0, 1, user.getUsername());
if (validUsers.isEmpty()) {
throw new RuntimeException("用户状态异常");
}

// 3. 业务逻辑:创建订单(本地操作)
OrderDTO order = new OrderDTO();
order.setUserId(user.getId());
order.setUserName(user.getUsername());
order.setProductId(request.getProductId());
order.setAmount(request.getAmount());
order.setStatus("PENDING");

return order;
}
}

当远程服务不可用或响应超时,为避免级联失败,可通过 fallback 实现降级逻辑进行熔断处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 定义降级处理类(实现 Feign 接口)
@Component
public class UserServiceFallback implements UserServiceClient {

// 对 getUserById 接口的降级实现
@Override
public UserDTO getUserById(Long id) {
log.warn("调用用户服务查询用户 {} 失败,触发降级", id);
// 返回默认值或容错数据
return new UserDTO(id, "默认用户", "unknown@example.com");
}

// 其他方法的降级实现...
@Override
public List<UserDTO> getUsers(int page, int size, String name) {
log.warn("调用用户服务分页查询失败,触发降级");
return Collections.emptyList();
}

// 省略其他方法的降级实现...
}

// 2. 在 Feign 接口中关联降级类(已在 @FeignClient 中通过 fallback 指定)

application.yml 中进行 Feign 的相关配置可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Feign 配置
feign:
client:
config:
default: # 全局配置
connectTimeout: 5000
readTimeout: 10000
loggerLevel: basic
user-service: # 特定服务配置
connectTimeout: 3000
readTimeout: 8000
loggerLevel: full
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
response:
enabled: true
hystrix:
enabled: true

当然自定义 Feign 配置也可通过配置类或属性自定义 Feign 的行为(如超时时间、日志级别等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 自定义 Feign 配置类
@Configuration
public class FeignConfig {

// 配置日志级别(仅在开发环境使用,生产环境建议关闭)
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // 打印完整日志(请求头、请求体、响应等)
}

// 配置超时时间(通过 Request.Options)
@Bean
public Request.Options requestOptions() {
return new Request.Options(
Duration.ofSeconds(2), // 连接超时时间
Duration.ofSeconds(5) // 读取超时时间
);
}
}

// 在 Feign 接口中指定配置类
@FeignClient(
name = "user-service",
configuration = FeignConfig.class // 应用自定义配置
)
public interface UserServiceClient { ... }

提取出最核心的内容,我们来总结一下 Fegin 的使用的步骤

  • 引入依赖
  • 添加 @EnableFeginClients 注解
  • 编写 FeignClient 接口
  • 使用 FeginClient 中定义的方法代替 RestTemple

使用 Feign 进行服务调用实践

在之前我们讲解了基本的步骤,这次我们详细的进行 Fegin 的开发使用

我们需要准备模块,cloud-order 和 cloud-product 模块,在此基础上使用 Fegin,这两个模块你自己写一下,随便写就行

主要就是把实体类,repo 两个内容写出来就行,然后就相当于是把控制器的内容放到 Fegin 模块中写来了算是

然后创建一个我们的 Feign 接口并添加 @FeignClient 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package edu.software.ergoutree.feginapi.client;

@FeignClient(name = "cloudProduct", path = "/product/api/products")
public interface ProductClient {

/**
* 根据ID获取产品信息
*/
@GetMapping("/{id}")
ProductDTO getProductById(@PathVariable("id") Long id);

/**
* 获取所有激活的产品
*/
@GetMapping("/active")
List<ProductDTO> getActiveProducts();

/**
* 根据分类获取产品
*/
@GetMapping("/category")
List<ProductDTO> getProductsByCategory(@RequestParam("category") String category);
}

@FeignClient 注解中的 name 属性是一个任意的客户端名称,用于创建 Ribbon 负载均衡器。

注意,这里你对应的产品和订单的 Controller 层就需要进行修改,也需要添加对应的发方法内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 获取所有激活的产品
*/
@GetMapping("/active")
public ResponseEntity<?> getActiveProducts() {
log.info("接收到获取激活产品列表请求");
try {
List<Product> products = productRepository.findByIsActiveTrue();
log.info("找到{}个激活产品", products.size());
return ResponseEntity.ok(products);
} catch (Exception e) {
log.error("获取激活产品列表时发生错误", e);
Map<String, Object> response = new HashMap<>();
response.put("message", "获取激活产品列表时发生错误: " + e.getMessage());
response.put("status", "ERROR");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}

总之,在 Feign 中创建客户端接口总结如下

1
2
3
4
5
@FeignClient(name = "服务名称", path = "服务路径")
public interface 新客户端接口 {
@请求方法("接口路径")
返回类型 方法名(@参数注解 参数类型 参数名);
}

创建 TestController 控制器,在其中调用 Client 的方法,来测试 Feign 的服务调用的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package edu.software.ergoutree.feginapi.controller;

@RestController
@RequestMapping("/api/test")
public class TestController {

@Autowired
private ProductClient productClient;

/**
* 测试Feign调用 - 获取产品信息
*/
@GetMapping("/product/{id}")
public ProductDTO testGetProduct(@PathVariable("id") Long id) {
return productClient.getProductById(id);
}

/**
* 测试Feign调用 - 获取激活产品列表
*/
@GetMapping("/products/active")
public List<ProductDTO> testGetActiveProducts() {
return productClient.getActiveProducts();
}

/**
* 测试Feign调用 - 根据分类获取产品
*/
@GetMapping("/products/category/{category}")
public List<ProductDTO> testGetProductsByCategory(@PathVariable("category") String category) {
return productClient.getProductsByCategory(category);
}
}

最后,启动类添加@EnableFeignClients注解,来启用 Feign 服务

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableFeignClients(basePackages = "edu.software.ergoutree.feginapi.client")
public class FeginApiApplication {

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

现在我们启动这些所需要的模块,我们来看看服务正不正常

image-20250723170538505

666这个入开了(我指的服务)

既然您的项目已经有了,并且Nacos中的服务都能正常发现,下面我将正式开始Feign的实践:

我们先测试各个服务的基本功能是否正常,这一步不要忽略

image-20250723171008224

然后我们通过 fegin-api 模块来测试Feign调用,根据上面的接口,我们获取一下产品信息试试,可以看到,程序不但实现了声明式的REST APT调用,同时还实现了客户端侧的负载均衡。

image-20250723171804233

666我们通过 Feign 调用了 cloud-product 的获取产品列表的服务

使用 Feign 进行服务间调用实践

上面我们只是进行了 Feign 调用了单个的服务,现在,我们要进阶修改,进行服务间调用的内容

创建一个场景测试,例如:下单流程,需要调用产品服务检查库存,然后创建订单:

在fegin-api 的控制器中,添加一个方法,来进行订单的创建,一般服务间调用的方法都是 post 请求,添加如下接口

1
2
3
4
5
/**
* 创建订单
*/
@PostMapping
OrderDTO createOrder(@RequestBody Map<String, Object> orderData);

controller 层添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 创建订单(POST方法)
*/
@PostMapping("/create-order")
public ResponseEntity<?> createOrderDemo(@RequestBody(required = false) Map<String, Object> requestBody) {
try {
log.info("接收到创建订单请求: {}", requestBody);

// 1. 检查产品是否存在
List<Long> productIds = (List<Long>) orderData.get("productIds");
if (productIds == null || productIds.isEmpty()) {
log.warn("产品ID列表为空");
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "产品ID列表不能为空");
return ResponseEntity.badRequest().body(error);
}

try {
// 2. 创建订单
log.info("开始创建订单");
OrderDTO order = orderClient.createOrder(orderData);
log.info("订单创建成功: {}", order);

Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "订单创建成功");
result.put("orderId", order.getId());
result.put("orderNumber", order.getOrderNumber());
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("处理订单创建时出错", e);
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "处理订单创建时出错: " + e.getMessage());
error.put("errorType", e.getClass().getSimpleName());
return ResponseEntity.status(500).body(error);
}
} catch (Exception e) {
log.error("处理请求时出错", e);
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "处理请求时出错: " + e.getMessage());
error.put("errorType", e.getClass().getSimpleName());
return ResponseEntity.status(500).body(error);
}
}

多参数的URL也可以使用Map构建。当目标 url 参数非常多时,可以使用这种方式简化Feign接口的编写。

1
2
@GetMapping("/user/getByUsernameAndAge")
User getByUsernameAndAge2(@RequestParam Map<String,Object> userMap);
1
2
3
4
5
6
7
@GetMapping("/getByUsernameAndAge2")
public User getByUsernameAndAge2(){
Map<String,Object> map=new HashMap<>();
map.put("username","zhaoliu");
map.put("age",20);
return userClientFeign.getByUsernameAndAge2(map);
}

一般我们的 post 请求包含会包含多个参数,可以按照这样的方式写

1
2
@PostMapping("/user/add")
int addUser(@RequestBody User user);

controller 层级里面可以加入,按实体类进行传递

1
2
3
4
@PostMapping("/add")
public int getByUsernameAndAge2(@RequestBody User user){
return userClientFeign.addUser(user);
}

当然你可以在配置文件中配置 Feign 一些常用内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
feign:
client:
config:
default: # 默认配置
connectTimeout: 5000 # 连接超时
readTimeout: 5000 # 读取超时
loggerLevel: full # 日志级别
cloudProduct: # 特定服务配置
connectTimeout: 2000
readTimeout: 3000
compression:
request:
enabled: true # 请求压缩
response:
enabled: true # 响应压缩
circuitbreaker:
enabled: true # 启用断路器

这样,我们在 fegin 的控制器中,可以同时调用产品服务和订单服务进行订单的创建

Feign 开发总结

  1. 我们要进行依赖引入,如果你的 fegin 的模块,我们一般引入 spring-cloud-starter-openfeign

  2. 在上启动类上,用 @EnableFeignClients 开启 Feign 功能,可指定扫描路径

  3. Feign 接口通过 注解 + 方法签名 映射 HTTP 请求,核心规则总结和开发流程如下

    1. @FeignClient(name = "服务名") 绑定目标服务(服务名对应注册中心的服务 ID),支持指定 URL(非注册中心场景:url = "http://localhost:8080")。

    2. @GetMapping/@PostMapping 等注解指定请求方式和路径,参数通过 @PathVariable@RequestParam@RequestBody 映射:

      • 复杂对象参数(如 UserForm)默认用 @RequestBody,对应 Content-Type: application/json
      • 路径参数 @PathVariable 必须指定 value(如 @PathVariable("id")),否则会报错。
      • 多个简单参数需用 @RequestParam 逐个指定
    3. Feign 支持通过配置类或属性文件定制行为,满足复杂场景需求

      全局配置

      1
      2
      3
      4
      5
      6
      feign:
      client:
      config:
      default: # 所有服务生效
      connect-timeout: 5000 # 连接超时
      read-timeout: 10000 # 读取超时

      局部配置

      1
      2
      3
      4
      5
      feign:
      client:
      config:
      user-service: # 服务名
      connect-timeout: 3000
    4. 如果你需要使用拦截器,用于统一处理请求(如添加鉴权头、日志打印):

      1
      2
      3
      4
      5
      6
      7
      8
      @Component
      public class AuthInterceptor implements RequestInterceptor {
      @Override
      public void apply(RequestTemplate template) {
      // 添加 Token 头
      template.header("Authorization", "Bearer " + getToken());
      }
      }

      局部生效需在 @FeignClient 中指定配置类

    5. 默认使用 Spring 的 HttpMessageConverter(支持 JSON、表单等),可自定义编码

      1
      2
      3
      4
      @Bean
      public Encoder feignEncoder() {
      return new SpringEncoder(() -> new HttpMessageConverters(new GsonHttpMessageConverter()));
      }
    6. 一般在进行 Feign 开发的时候,我们会自定义异常的解码器,当服务返回非 200 状态码时,Feign 会抛出 FeignException,可通过 ErrorDecoder 自定义异常转换:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Bean
      public ErrorDecoder errorDecoder() {
      return (methodKey, response) -> {
      if (response.status() == 404) {
      return new UserNotFoundException("用户不存在");
      }
      return new RuntimeException("服务调用失败: " + response.reason());
      };
      }
    7. 在使用 Feign 的时候,我们会不止使用 Feign,如果你需要需开启熔断功能,并指定降级方法(fallbackfallbackFactory),需要编写组件和配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @FeignClient(name = "user-service", fallback = UserClientFallback.class)
      public interface UserClient { ... }

      // 降级实现类
      @Component
      public class UserClientFallback implements UserClient {
      @Override
      public UserDTO getUserById(Long id, String name) {
      return new UserDTO(id, "默认用户", "降级数据");
      }
      }
    8. 推荐一个我习惯使用的多层的端口编写方式,服务端可将 Controller 接口抽象为公共 API 接口(如 UserApi),Feign 客户端直接继承,避免代码重复:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // 公共 API 接口
      public interface UserApi {
      @GetMapping("/users/{id}")
      UserDTO getUserById(@PathVariable("id") Long id);
      }

      // 服务端实现
      @RestController
      public class UserController implements UserApi {
      @Override
      public UserDTO getUserById(Long id) { ... }
      }

      // Feign 客户端
      @FeignClient(name = "user-service")
      public interface UserClient extends UserApi {}
    9. fegin 可能会涉及到把自己的模块作为依赖导入到其他模块中,但是 Feign 客户端接口若被同一服务的其他组件调用,需注意扫描路径,避免 @FeignClient@Service 形成循环依赖。

    10. 启用连接池:Feign 默认使用 JDK 原生 URLConnection(无连接池),推荐替换为 Apache HttpClient 或 OkHttp:

      1
      2
      3
      4
      <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-okhttp</artifactId>
      </dependency>

      并在配置中启用:feign.okhttp.enabled: true

Feign 的自定义配置

虽然 Feign 会和其他 Spring 组件一样,在自动装配和自动配置的时候就有了很完备的默认配置,但是万一部分不符合需求,你可以进行很高程度上的自定义,在实践中,你有两种主流方式来配置 Feign,配置文件和配置类,这是和别的一样的

在一般情况下,我们常用配置 Feign 的五个内容

image-20250725102943036

我们通常会配置日志的相关配置,因为 Feign 貌似自身不会打印任何日志,一旦出问题(比如参数没传对、对方服务报错),你将无从下手。

所以我们需要在 application.yml 中为你的 Feign 客户端指定日志级别。

Feign 的日志级别分为四种:

  • NONE: 不记录任何日志(默认)。
  • BASIC: 仅记录请求方法、URL、响应状态码及执行时间。
  • HEADERS: 在 BASIC 基础上,增加请求和响应的头信息。
  • FULL: 记录所有请求与响应的明细,包括头信息、请求体、元数据。(调试)

一般日志常用的配置项目如下

1
2
3
4
5
6
7
8
9
10
# 设置Feign全局日志级别(NONE/BASE/HEADERS/FULL)
feign.client.config.default.logger-level=full

# 格式:feign.client.config.【Feign客户端名称】.logger-level=级别
# 其中“Feign客户端名称”对应@FeignClient注解的name/value属性
feign.client.config.user-service.logger-level=headers
feign.client.config.order-service.logger-level=base

# 开启Feign日志实现类的DEBUG级别日志(固定包路径)
logging.level.feign.Logger=debug

例如

1
2
3
4
5
logging:
level:
# 这里的 key 是你 @FeignClient 注解接口的完整包路径
# 例如,你的接口是 com.example.clients.UserClient
com.example.clients.UserClient: FULL

虽然用配置文件足够简单,但你仍需要提供一个 Logger 的 Bean。你可以在一个全局的 Feign 配置类中定义它。在我们的日志文件比较复杂的时候,配置类更简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 这个 @Configuration 注解可以加,也可以不加。
// 如果不加,它就不会被Spring组件扫描到,只能在 @FeignClient 中显式指定。
// 这是一种很好的隔离方式,后面会详细讲。
@Configuration
public class GlobalFeignConfig {

@Bean
Logger.Level feignLoggerLevel() {
// 这里定义了日志级别为 FULL
return Logger.Level.FULL;
}
}

然后,你需要告诉 Feign 去使用这个配置。你可以设置为全局默认配置:

1
2
3
4
5
6
7
8
// Application.java
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = GlobalFeignConfig.class) // 应用到所有 Feign 客户端
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

我们一般会组合使用。在 Java 配置类中提供 Logger.Level Bean,然后在 application.yml 中灵活控制具体哪个客户端在什么环境下开启哪个级别的日志。

接下来,就是配置超时时间,防止服务雪崩

这是生产环境必须配置的项。Feign 默认的连接和读取超时时间较短(或依赖底层客户端的默认值),在高延迟网络或对方服务响应慢时,会频繁超时失败,或者长时间等待导致自身服务资源耗尽。

为 Feign 调用设置合理的连接超时和读取超时。

  • 连接超时 (Connect Timeout): 与目标服务器建立连接的最长时间。
  • 读取超时 (Read Timeout): 建立连接后,等待服务器返回响应的最长时间。

通过 application.yml 配置这个是不推荐的,因为它在较新版本中可能不生效或被弃用,因为级别太低,而且好像会被覆盖

最标准、最可靠的方式我们基本建一个Config,创建一个 Request.Options 类型的 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// CustomFeignConfig.java
import feign.Request;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.TimeUnit;

public class CustomFeignConfig {
@Bean
public Request.Options options() {
// 参数: connectTimeout, connectTimeoutUnit, readTimeout, readTimeoutUnit, followRedirects
return new Request.Options(
5, TimeUnit.SECONDS, // 5秒连接超时
10, TimeUnit.SECONDS, // 10秒读取超时
true // 是否允许重定向
);
}
}

然后,将这个配置应用到具体的 Feign 客户端:

1
2
3
4
5
6
7
// UserClient.java
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient(name = "user-service", url = "http://localhost:8081", configuration = CustomFeignConfig.class)
public interface UserClient {
// ... methods
}

我们一般为不同的微服务调用设置不同的超时时间。调用一个执行复杂计算的服务,其超时时间应该比调用一个只读缓存的服务要长。因此,为每个 FeignClient 创建独立的配置类是最佳实践

自定义错误处理

如果 Feign 调用返回 4xx 或 5xx 状态码,它会统一抛出一个 FeignException。我看到全是这玩意直接秒开哈气形态

使用 ErrorDecoder,ErrorDecoder 可以让你检查原始的响应信息,并根据自己的业务逻辑返回一个自定义的异常。

创建一个自定义异常

1
2
3
4
5
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}

创建 ErrorDecoder 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// CustomErrorDecoder.java
import feign.Response;
import feign.codec.ErrorDecoder;

public class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultDecoder = new Default();

@Override
public Exception decode(String methodKey, Response response) {
// 如果是 404 状态码
if (response.status() == 404) {
// 这里可以根据 response.body() 的内容构造更详细的异常信息
return new ResourceNotFoundException("Resource not found for request: " + methodKey);
}

// 其他情况,使用默认的解码器,抛出 FeignException
return defaultDecoder.decode(methodKey, response);
}
}

在配置类中注册并应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// FeignWithErrorHandlingConfig.java
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;

public class FeignWithErrorHandlingConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}

// 应用到客户端
@FeignClient(name = "any-service", configuration = FeignWithErrorHandlingConfig.class)
public interface AnyServiceClient {
// ...
}

ErrorDecoder 是提升服务健壮性的利器。通过它,你的业务代码可以 try-catch 特定的业务异常,使代码逻辑更清晰。

全局配置 和 客户端独立配置的选择存在一个重要实践原则

一个常见的困惑是:这些配置应该放在哪里?是全局共享还是每个客户端独立一份?

  • 全局配置 (@EnableFeignClients(defaultConfiguration = …)):
    • 适用场景: 对所有客户端都通用的配置,如日志 Bean 的提供、全局的分布式追踪拦截器。
    • 优点: 配置一次,到处生效。
    • 缺点: 不够灵活,一个不当的全局配置(如过长的超时时间)会影响所有客户端。
  • 客户端独立配置 (@FeignClient(configuration = …)):
    • 适用场景: 针对特定服务的配置,如对 A 服务的超时时间、对 B 服务的认证拦截器、对 C 服务的错误解码器。
    • 优点: 精准控制,隔离性好,互不影响。这是更推荐的方式。
    • 缺点: 如果多个客户端需要相同配置,会产生一些重复代码(可以通过继承配置类解决)。

一般情况下我们会进行隔离配置类

一个非常重要的技巧是:不要让你为 Feign 写的 @Configuration 类被主应用的 @ComponentScan 扫描到!

如果 GlobalFeignConfig 被扫描到,它里面的 Bean(如 Request.Options)会变成一个全局 Bean,可能会影响到其他不想关的技术栈。

正确做法:

  1. 不要在 Feign 的配置类上加 @Configuration 注解。
  2. 或者,如果加了 @Configuration,就把它放在一个不会被主应用扫描到的包里。

这样,这个配置类就只对通过 defaultConfigurationconfiguration 属性显式指定的 Feign 客户端生效,实现了完美隔离。

1
2
3
4
5
6
7
8
9
10
11
// 正确的独立配置类
public class MyClientConfig {
@Bean
public feign.Retryer retryer() {
// 配置一个重试器:初始间隔100ms,最大间隔1s,最多重试3次
return new feign.Retryer.Default(100, 1000, 3);
}
}

@FeignClient(name = "resilient-service", configuration = MyClientConfig.class)
public interface ResilientClient { ... }

Feign 的性能调优

连接优化

Feign 的性能优化已经很好了,在我们上文进行的 Feign 的原理分析我们知道了,默认情况下,Feign 使用 Java 的 HttpURLConnection 来发送 HTTP 请求。它的问题是不支持连接池

这意味着每次 Feign 调用都可能需要经历一次完整的 TCP 三次握手和 TLS 握手,然后连接被关闭。在高并发场景下,这会产生巨大的性能开销和资源浪费。

一般我们会替换 Feign 底层的 HTTP 客户端为 Apache HttpClientOkHttpClient。它们都内置了强大的连接池管理。

选择其中一个添加到你的 pom.xml 中。通常两者性能相近,Apache HttpClient 在 Spring 生态中更常见。

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

application.yml 中启用并配置连接池,一旦你添加了依赖,Spring Cloud Feign 的自动配置机制会检测到它。你只需要在 application.yml 中开启并调优即可。

如果你对性能十分敏感,在这里关掉日志还能更进一步,连接池打印的日志挺多的

以 Apache HttpClient 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
feign:
# 启用 Apache HttpClient 替代默认的 HttpURLConnection
httpclient:
# 启用开关
enabled: true
# 最大连接数(整个连接池的大小)
# 把它想象成你的车库里总共能停多少辆车
max-connections: 200
# 每个路由(即每个目标主机)的最大连接数
# 把它想象成通往某个特定目的地(如"user-service")有几条专用车道
max-connections-per-route: 50
# 从连接池获取连接的超时时间,单位毫秒
connection-timer-repeat: 1000
# 连接存活时间
time-to-live: 900
time-to-live-unit: SECONDS
  • 在任何生产项目中,这都应该是标准操作。

  • 如何设置池大小max-connectionsmax-connections-per-route 的值需要根据你的应用的 QPS(每秒查询率)和下游服务的数量进行压测和调整。初始值可以设为 200 和 50,然后通过监控工具(如 Micrometer、Prometheus)观察连接池的使用情况来微调。

  • 过小的连接池会在高并发时导致请求排队等待连接,增加响应延迟;过大的连接池会占用过多内存和文件句柄。

数据传输优化

在微服务之间传输巨大的 JSON 对象会消耗大量的网络带宽,增加网络传输时间,成为性能瓶颈,尤其是在跨机房或公网调用时。我们需要减少网络传输的数据量。常用的解决方案如下

  1. 启用 GZIP 压缩:对请求体和响应体进行压缩。
  2. 更换更高效的序列化库(微优化)。

application.yml中开启 GZIP 压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
feign:
compression:
request:
# 开启请求体压缩
enabled: true
# 指定哪些 MIME 类型的内容需要压缩
mime-types:
- text/xml
- application/xml
- application/json
# 请求体达到多大时才进行压缩(单位:字节),太小的数据压缩反而浪费CPU
min-request-size: 2048
response:
# 开启响应体压缩
enabled: true
  • 这里存在一个权衡:压缩会消耗 CPU,但能大幅减少网络 I/O。对于目前大多数应用来说,CPU 资源相对廉价,而网络延迟是主要矛盾,因此开启压缩通常是明智之举
  • min-request-size 是一个很好的调优参数,避免为几十个字节的小请求执行压缩操作。

当然你也可以更换 JSON 序列库,Spring Boot 默认使用 Jackson。在绝大多数情况下,它的性能已经足够好。但如果你在性能分析中发现序列化/反序列化是应用的热点,可以考虑更换为 GSON 或其他更高性能的库。

这是一个微优化。除非有明确的性能瓶颈证据,否则坚持使用默认的 Jackson 即可,避免引入新的技术栈和潜在的兼容性问题。

异步优化

串行到并行的优化也很关键

在一个业务逻辑中,你可能需要调用多个不同的微服务来组装数据(比如,获取订单详情时,需要调用用户服务、商品服务、库存服务)。默认的 Feign 调用是同步阻塞的,这意味着这些调用会一个接一个地执行,总耗时是所有调用耗时之和。

一般情况下我们会将多个独立的 Feign 调用并行化执行,将总耗时缩短为其中最慢的那个调用的耗时。

结合 Spring 的 @AsyncCompletableFuture 来异步执行 Feign 调用。

在启动类开启异步支持

1
2
3
4
@EnableAsync
@SpringBootApplication
@EnableFeignClients
public class MyApplication { ... }

创建一个专门的 Service,将 Feign 的同步调用封装在异步方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class AggregationService {

@Autowired
private UserClient userClient;

@Autowired
private ProductClient productClient;

@Async // 关键注解:表示该方法将在独立的线程池中异步执行
public CompletableFuture<User> getUserDetails(String userId) {
// Feign 调用本身是同步的,但整个方法是异步的
User user = userClient.getUserById(userId);
return CompletableFuture.completedFuture(user);
}

@Async
public CompletableFuture<Product> getProductDetails(String productId) {
Product product = productClient.getProductById(productId);
return CompletableFuture.completedFuture(product);
}
}

在业务逻辑中并行编排,在你的主业务逻辑中,同时发起调用并等待所有结果返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
public class OrderController {

@Autowired
private AggregationService aggregationService;

@GetMapping("/order-details")
public Map<String, Object> getOrderDetails() {
// 1. 同时发起两个异步调用
CompletableFuture<User> userFuture = aggregationService.getUserDetails("123");
CompletableFuture<Product> productFuture = aggregationService.getProductDetails("p456");

// 2. 等待所有异步任务完成
CompletableFuture.allOf(userFuture, productFuture).join();

// 3. 获取结果并组装
Map<String, Object> result = new HashMap<>();
try {
result.put("user", userFuture.get());
result.put("product", productFuture.get());
} catch (InterruptedException | ExecutionException e) {
// 异常处理
}
return result;
}
}

这是一种架构层面的优化,效果非常显著。但需要注意配置和管理 Spring 的异步线程池(TaskExecutor),避免线程池耗尽。

负载均衡优化

负载均衡的方面我们也可以进行优化

默认的轮询(Round-robin)策略无法感知下游服务的真实负载和健康状况,可能将请求发送给一个响应缓慢或即将崩溃的实例。而且默认 Feign 不进行重试。如果配置了重试,它可能会对非幂等的写操作(如 POST)进行重试,导致数据重复。

在分析原理的时候我们知道Feign是无缝集成Spring Cloud LoadBalancer (替代了 Ribbon)的, 提供了灵活的策略。在 application.yml 中配置,为特定服务指定不同的负载均衡策略。

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
loadbalancer:
clients:
# 为 "user-service" 这个服务配置
user-service:
# Spring Cloud LoadBalancer 默认是轮询
# 如果想用随机,需要自定义实现一个 ReactorLoadBalancer
# 但我们可以利用其缓存能力
cache:
enabled: true
ttl: 5s # 缓存5秒的服务实例列表,减少每次调用都去注册中心拉取

如果启用 Load Balancer 缓存 (spring.cloud.loadbalancer.cache.enabled: true) ,也是一个简单有效的优化,可以降低与服务注册中心的交互频率。

对于更高级的策略,如基于响应时间的加权负载均衡,你需要自定义实现,参考我负载均衡那章

精细化重试策略

通过 Java 配置类自定义 Retryer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SmartRetryerConfig.java
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.TimeUnit;

public class SmartRetryerConfig {
@Bean
public Retryer feignRetryer() {
// 不要使用 Retryer.NEVER_RETRY (默认)
// 或者 Retryer.Default (会对所有请求重试)

// 我们可以实现自己的 Retryer,决定何时重试
// 但一个更简单的做法是在 Feign 之外,使用 Resilience4J 的 Retry 模块,
// 它可以更精细地控制对哪些异常、哪些方法进行重试。

// 这里我们先配置一个标准的重试器
// 参数:初始间隔,最大间隔,最大重试次数 (不含首次)
return new Retryer.Default(100, TimeUnit.SECONDS.toMillis(1L), 3);
}
}
  • 幂等性警告:切记,Feign 的重试对 POST 等非幂等操作有风险。
  • 最佳实践:将重试逻辑交给更专业的熔断降级组件,如 Resilience4J。你可以用它的 @Retry 注解来包裹 Feign 调用方法,并精确配置重试的条件(如只对 IOException 或 5xx 错误重试),而对 4xx 业务错误不重试。这比 Feign 内置的 Retryer 更强大、更安全。

Feign 的最佳实践

方式一:继承,给消费者的 FeignClient 和提供者的 Controller 定义统一的父接口作为标准

image-20250725105147954

首先,这个接口的目的是让消费者基于这些声明的信息,去发送一次 http 请求,这个请求最终会到达对应服务的一个实例的Controller中

image-20250725105300611

整个方法的声明,除了方法名,都是一样的,那么既然都一样,可以不可以进行抽取,肯定可以

所以我们创建一个共享接口(API模块)来统一服务契约,既然服务提供方 (Provider)在 Controller 中定义接口,包括 @RequestMapping, @PathVariable, @RequestBody 等注解,以及方法的参数和返回类型。服务消费方 (Consumer)又在 FeignClient 接口中,必须一模一样地再声明一遍这些注解、参数和返回类型。

这种方式存在巨大的问题,为了解决上述问题,我们引入一个专门的 API 模块。里面写好其中的API,然后 Client 和 Controller 直接继承就可以了。

image-20250725110044562

但是 Spring 官方不推荐这么做

image-20250725110147923

这会造成紧耦合,仔细一想,从 API 都耦合了,一炸都炸,成鞭炮了

而且对 SpringMVC 这招不好使,父接口方法的参数列表参数是继承不下来的,自己还得写一篇

但是面向契约的思想还是很好的

方式二:抽取 将 FeignClient 抽取为独立的模块,并且把接口有关的POJO,默认的 Feign 配置都放到这个模块中,提供过所有的消费者使用

这就是最现代,最常用,目前最佳的方式了,我也是这样,无数人也是这样

在 Maven 项目中,这通常是一个独立的 jar 包模块。

这个模块的核心定位是:定义服务之间通信的“合同”

image-20250725110600600

在你的多模块 Maven 项目中,创建一个新的 jar 模块,例如 feign-api。这个模块非常轻量,它只包含

  1. 项目中共享的接口定义。
  2. 接口中用到的 DTO (Data Transfer Object) 或 POJO 类(如 User, ResultVO 等)。

这个模块不包含任何具体的业务实现逻辑,也没有复杂的依赖。

之后在 feign-api 模块中定义共享接口,我们将原来分散在两边的定义,统一放在 user-api 模块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在 user-api 模块中
package com.example.user.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.example.user.api.dto.UserDTO; // DTO也定义在api模块

// 这就是那个“统一的父接口”或“服务契约”
public interface UserApi {

/**
* 根据ID获取用户详情
* 所有的 Spring MVC 注解都定义在这里,作为契约的一部分
* @param id 用户ID
* @return 用户数据
*/
@GetMapping("/user/{id}")
UserDTO findUserById(@PathVariable("id") Long id);

// ... 其他接口定义
}

所有用于定义 HTTP 请求的注解(@GetMapping, @PostMapping, @PathVariable, @RequestParam, @RequestBody 等)都必须在这里声明。这里就是这份“合同”最核心的条款。

服务提供方 (Provider) 实现接口

在 user-service 项目中:

  1. 在其 pom.xml 中添加对 feign-api 模块的依赖。
  2. 让 UserController 实现 (implements) 这个 UserApi 接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在 user-service 模块中
package com.example.userservice.controller;

import com.example.user.api.UserApi; // 引入 API 模块的接口
import com.example.user.api.dto.UserDTO;
import org.springframework.web.bind.annotation.RestController;

@RestController // Controller 自身的注解依然保留
public class UserController implements UserApi {

// ... 注入 UserService

@Override // 使用 @Override 注解,让编译器帮你检查是否正确实现了接口方法
public UserDTO findUserById(Long id) {
// 这里是具体的业务实现逻辑
// ...
return userService.findById(id);
}
}
  • UserController 实现 (implements) 接口,提供了方法的具体业务逻辑。
  • 因为 Java 的注解继承机制,UserController 中的 findUserById 方法自动继承了来自 UserApi 接口的所有注解 (@GetMapping, @PathVariable)。你无需在 Controller 的方法上重复声明它们!
  • 使用 @Override 是一个好习惯,如果接口方法签名发生变化,而实现类没有同步修改,编译器会立刻报错,将运行时风险提前到编译时解决。

服务消费方 (Consumer) 继承接口

在 order-service 项目中:

  1. 在其 pom.xml 中同样添加对 feign-api 模块的依赖。
  2. 让 UserClient (Feign 客户端) 继承 (extends) 这个 UserApi 接口。
1
2
3
4
5
6
7
8
9
10
11
// 在 order-service 模块中
package com.example.orderservice.client;

import com.example.user.api.UserApi; // 引入 API 模块的接口
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient(name = "user-service") // FeignClient 的配置注解依然保留
public interface UserClient extends UserApi {
// 里面什么都不用写!
// 它已经通过继承,拥有了 UserApi 中定义的所有方法签名和注解。
}
  • UserClient 继承 (extends) 接口。对于一个接口来说,extends 意味着它包含了父接口的所有方法声明。因为 Feign 是为接口创建动态代理,所以它不需要方法的实现体,只需要方法的声明,这正是 extends 所提供的。
  • @FeignClient 注解是 Feign 自身需要的,用于指定目标服务名等配置,所以需要保留在子接口上。
  • 接口体是空的!这极大地简化了消费方的代码。

这招带来的巨大优势体现在,单一事实来源,API 契约被定义在唯一的 user-api 模块中。任何关于接口的疑问,都以此为准。

如果需要修改接口,你只需要修改 feign-api 中的 UserApi 接口。

  • 提供方 (user-service):会立刻出现编译错误,因为它实现的接口方法签名不匹配了,强制开发者去修正实现。
  • 消费方 (order-service):无需任何改动,它会自动继承新的接口定义。 这就将原来可能在生产环境才暴露的运行时错误,提前扼杀在了开发阶段的编译期。

代码复用与维护性

避免了大量的重复代码。修改和维护只在一处进行,所有依赖方都能自动享受到更新,大大提高了开发效率和代码质量。

清晰的模块边界与职责

feigm-api 模块清晰地定义了服务的边界和能力。其他团队的开发者只需要关心这个 API 模块,就能知道如何与你的服务进行交互,而无需关心你的内部实现细节。

但是如果要对外提供开放 API (Open API),让第三方(可能使用不同语言,如 Python, Go)来调用,则不应共享 Java jar 包。此时应使用与语言无关的契约定义方式,如 OpenAPI (Swagger) 规范gRPC 的 .proto 文件