Feign 实战指南:从 REST 替代到性能优化与最佳实践
在微服务架构中,服务间通信的便捷性与性能至关重要。本文将深入解析 Feign 这一声明式 HTTP 客户端的核心应用,从替代传统 RestTemplate 解决调用痛点开始,逐步讲解自定义配置、性能优化及企业级最佳实践。内容涵盖 Feign 客户端定义与使用、日志级别配置、连接池优化,以及通过继承或抽取方式实现代码复用,帮助读者构建高效、可维护的微服务通信体系。
本专栏的内容均来自于 B 站 UP 主黑马程序员的教学视频,感谢你们提供了优质的学习资料,让编程不再难懂。
专栏地址 : https://blog.csdn.net/m0_53117341/category_12835102.html
一 . Feign 替代 RestTemplate
1.1 RestTemplate 方式调用存在的问题
先来看一下我们之前通过 RestTemplate 方式发起远程调用的代码
这种方式是通过 URL 地址指明要访问的服务名称、请求路径以及请求的参数信息 , 由 RestTemplate 来负责发起请求 .
那这段代码存在一些问题 :
- 代码可读性差 , 编程体验不够统一 : 在代码中引入了 URL , 导致代码不够统一
- 参数复杂 URL 难以维护 : 就比如我们访问的 Nacos 的路径 , 他的 URL 就非常复杂
1.2 Feign 的介绍
Feign 是一个声明式的 HTTP 客户端
声明式 : 比如早期 , 我们学习 MySQL 的事务的时候 , 我们需要手动开启关闭事务 . 那后来学习 Spring 的时候 , 就提供了 Spring 的声明式事务 , 只需要在配置文件中告诉 Spring 对谁加事务即可 . 那以后所有的事务都交给 Spring 来帮我们做 .
那声明式 HTTP 客户端也是如此 , 我们如果要发送 HTTP 请求 , 那只需要将发送请求所需要的信息声明出来 , 剩下的工作 Feign 帮助我们来做 .
Feign 的作用就是帮助我们优雅地实现 HTTP 请求的发送 , 解决上面提出的问题 .
1.3 定义和使用 Feign 客户端
1.3.1 引入依赖
在 order-service 服务中引入依赖
<!-- Feign 客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
1.3.2 添加注解
我们需要在 order-service 的启动类中添加开启 Feign 功能的注解
package com.example.order;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.example.order.mapper")
@SpringBootApplication
@EnableFeignClients // 开启 Feign 功能
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 随机轮训
// @Bean
// public IRule randomRule() {
// return new RandomRule();
// }
}
1.3.3 编写 Feign 的客户端进行接口声明
我们需要在该接口上添加一个注解 @FeignClient , 并且指定要调用的服务名称
接下来我们编写一个方法 , 方法的返回值就是我们想获取到的结果 , 方法的参数就是我们要传的参数
然后我们还需要在方法上面添加一个注解声明请求的方式
那上面用了动态参数 , 下面我们也需要用 @PathVariable 注解来接收
package com.example.order.clients;
import com.example.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 1. 指定服务名称
@FeignClient("userservice")
public interface UserClient {
// 3. 声明请求方式
@GetMapping("/user/{id}")
// 4. 在参数前面加上 @PathVariable 注解
User findById(@PathVariable("id") Long id);// 2. 方法的返回值就是我们想获取到的结果 , 方法的参数就是我们要传的参数
}
1.3.4 测试
那我们可以将之前的 RestTemplate 的方式替换掉了
首先 , 需要注入刚才实现的 UserClient
然后下面我们只需要调用 userClient 的 findById 方法即可
package com.example.order.service;
import com.example.order.clients.UserClient;
import com.example.order.mapper.OrderMapper;
import com.example.order.pojo.Order;
import com.example.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1. 查询订单
Order order = orderMapper.findById(orderId);
// 2. 查询用户 ID
Long userId = order.getUserId();
// 3. 用 Feign 来实现远程调用
User user = userClient.findById(userId);
// 4. 将获取到的用户信息添加到 user 字段中
order.setUser(user);
// 5. 返回
return order;
}
}
在运行之前 , 先将我们的 bootstrap.yml 修改回 8848
成功通过 Feign 的方式调用
小结
Feign 的使用步骤
- 引入依赖
- 在启动类中添加 @EnableFeignClients 注解
- 编写 FeignClient 接口
- 使用 FeignClient 中定义的方法代替 RestTemplate
1.4 通过 POST 请求进行远程调用
第一步 : 添加 @PostMapping 注解
第二步 : 参数使用 @RequestBody 接收即可
package com.example.order.clients;
import com.example.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 1. 指定服务名称
@FeignClient("userservice")
public interface UserClient {
// 3. 声明请求方式
@PostMapping("/user/add")
// 4. 在参数前面加上 @RequestBody 注解
String add(@RequestBody User user);
}
二 . 自定义配置
Feign 可以运行自定义的配置来去覆盖默认的配置 , 那可以修改的配置如下 :
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志的级别 | 包含四种不同的级别 : + NONE + BASIC + HEADERS + FULL |
feign.codec.Decoder | 解码 | 将 HTTP 远程调用的结果进行解析 (比如 : 解析 JSON 字符串为 Java 对象) |
feign.codec.Encoder | 编码 | 将请求参数进行编码 (比如转化成二进制) , 便于通过 HTTP 请求发送 |
feign.Contract | 支持的注解格式 | 默认是 Spring MVC 的注解 |
feign.Retryer | 失败重试机制 | 请求失败的重试机制 , 一般通过 Ribbon 的重试机制 |
日志的级别分为四种 :
- NONE : 不记录任何日志信息 , 这是默认值
- BASIC : 仅记录请求的方法、URL 以及相应状态码和执行时间
- HEADERS : 在 BASIC 的基础上 , 额外记录了请求和响应的头信息
- FULL : 记录所有请求和响应的明细 , 包括 : 头信息、请求体、元数据
在非生产环境中 , 我们一般配置日志级别为 FULL , 在生产环境中 , 我们一般配置日志级别为 NONE / BASIC
那配置 Feign 日志有两种方式
2.1 配置文件方式
我们可以配置日志级别全局生效 , 也可以设置日志级别局部生效
① 全局生效
feign:
client:
config:
default: # 默认全局所有的 Feign 的客户端都设置为该级别
loggerLevel: FULL # 设置日志级别
那我们可以重启 order-service 服务 , 然后观察它的日志
日志中展示了详细的 HTTP 信息 .
② 局部生效
feign:
client:
config:
default: # 针对所有微服务来去做配置
loggerLevel: FULL # 设置日志级别
userservice: # 针对特定的微服务来去做配置
loggerLevel: BASIC
那 default 以及 userservice 配置全部存在 , 最后会执行哪个配置呢 ?
重启 user-service 服务
那这就代表最后会执行 userservice 这一层配置
2.2 Java 代码方式
我们可以声明一个配置类
然后在类上添加 @Configuration 注解 , 表示当前的类是一个配置类
然后在里面声明一个 Bean 对象 , 我们也能够看到一系列选项
package com.example.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public Logger.Level loggerLevel() {
return Logger.Level.BASIC;
}
}
① 全局配置
如果是全局配置 , 那就将他放到启动类中的 @EnableFeignClients 注解中
我们可以看一下 @EnableFeignClients 注解的选项
那我们就使用这个默认的参数
那就代表 , 所有的 Feign 都会走 FeignConfig 这个配置
② 局部配置
如果是局部配置 , 那就将他放到 @FeignClient 这个注解中
我们也可以看一下 @FeignClient 注解默认的选项
那我们就可以在选项中填写这个参数
package com.example.order.clients;
import com.example.order.config.FeignConfig;
import com.example.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 1. 指定服务名称
@FeignClient(value = "userservice", configuration = {FeignConfig.class})
public interface UserClient {
// 3. 声明请求方式
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);// 2. 方法的返回值就是我们想获取到的结果, 方法的参数就是我们要传的参数
}
那我们就需要将配置文件中配置的自定义配置注释掉
重启一下
2.3 小结
Feign 的日志配置 :
方式一是配置文件 , feign.clent.config.xxx.loggerLevel
- 如果 xxx 是 default , 则代表全局
- 如果 xxx 是服务名称 , 例如 : userservice 就代表某服务
方式二是通过 Java 代码配置 Logger.Level 这个 Bean
- 如果在 @EnableFeignClients 注解中声明 , 则代表全局
- 如果在 @FeignClient 注解中声明 , 则代表某服务
三 . 性能优化
Feign 底层的客户端实现 :
- URLConnection : 默认实现 , 不支持连接池
- Apache HttpClient : 支持连接池
- OKHttp : 支持连接池
因此 , 提高 Feign 的性能的主要手段就是使用连接池代替默认的 URLConnection
那这里我们用 Apache 的 HttpClient 来演示
3.1 引入依赖
在 order-service 的 pom.xml 中引入依赖 :
<!-- httpClient 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
3.2 配置连接池
在 order-service 的 applicatiom.yml 中添加配置
feign:
httpclient:
enabled: true # 开启 feign 对 HttpClient 的支持
max-connections: 200 # 设置最大连接数
max-connections-per-route: 50 # 设置每个路径的最大连接数
设置每个路径的最大连接数的意思就是设置每个接口的最大连接数
我们重新运行一下 , 由于这个性能提升了多少我们不太好衡量 , 所以保证不报错就可以了
3.3 小结
Feign 的优化 :
- 使用 HttpClient 或者 OKHttp 代替 URLConnection
- 引入 feign-httpClient 依赖
- 配置文件开启 httpClient 功能 , 设置连接池参数
- 日志级别生产环境尽量用 basic
四 . 最佳实践
所谓的最佳实践 , 就是在使用的过程中总结出来的经验 , 找到的最好的一种使用方式 .
我们提供给大家两种最佳的实践思路
4.1 继承方式
继承方式就是给消费者的 FeignClient 和提供者的 controller 定义统一的父接口作为标准
我们可以通过代码来了解一下
首先 , 来看 UserClient 这个接口 , 它的作用就是通过注解来声明远程调用需要用到的一系列的信息 , 比如 : 请求的方式、路径、参数、返回值类型等等 .
那这段代码的意义就是让消费者基于这些信息发送一个 HTTP 请求 , 那这个请求最终就会到达 user-service 服务中的某个实例上 .
那我们再来看一下 user-service 服务中的 controller 层的代码
那我们可以对比一下两个方法
那这两个方法非常非常相似 , 那我们可不可以对这两个方法进行一个抽取呢 ?
但是这种方式其实是存在一些问题的 , Spring 官方也提醒我们了
一般情况下 , 我们不推荐接口被服务端和客户端所共享 . 因为他会造成紧耦合 , 将来接口发生变化 , 服务端和客户端就都需要进行更改 . 而且这种方案是不对 Spring MVC 起作用的 .
4.2 抽取方式
我们可以将 FeignClient 抽取为独立模块 , 并且把接口有关的 POJO、默认的 Feign 配置都放到这个模块中 , 提供给所有消费者使用 .
Feign 的最佳实践 :
- [继承方式] : 让 controller 和 FeignClient 继承同一接口
- [抽取方式] : 将 FeignClient、POJO、Feign 的默认配置都定义到一个项目中 , 供所有消费者使用
4.3 实现抽取方式
① 抽取
首先 , 创建一个 module , 命名为 feign-api , 然后引入 feign 的 starter 依赖
然后在 feign-api 中引入 feign 的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
然后我们就需要将 order-service 中的 Feign 客户端、配置、实体类都挪动到 feign-api 模块下
然后我们将这几个类该导的包导入进来
那之后其他服务想要进行远程调用 , 只需要调用 feign-api 即可 , 所以我们的 order-service 服务中的这些内容就可以删除掉了
那此时 Order 实体类会报错
② 在 order-service 中使用 feign-api
我们只需要引入 feign-api 依赖即可
<!-- 引入 feign-api -->
<dependency>
<groupId>com.example.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
此时我们重新导入一下包即可 , 我们需要重新导入 Order 实体类、OrderService、启动类这三个部分
③ 重启测试
那我们重启 order-service 服务
他报错的信息是未发现 UserClient 对象 , 这是因为我们已经将 UserClient 移动到 feign-api 模块下了 , 现在他们已经属于不同的服务了 , 我们不能跨服务调用 .
④ 解决扫描包问题
当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围时 , 这些 FeignClient 无法使用 . 那我们有两种解决方案
- 指定 FeignClient 所在的包
@EnableFeignClients(basePackages = "com.example.feign.UserClient")
- 指定 FeignClient 字节码
@EnableFeignClients(clients = {UserClient.class})
那我们可以实现一下第二种方式
package com.example.order;
import com.example.feign.clients.UserClient;
import com.example.feign.config.FeignConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.example.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = {UserClient.class}, defaultConfiguration = {FeignConfig.class})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
那我们再重启一下
小结 :
不同包的 FeignClient 的导入有两种方式 :
- 在 @EnableFeignClients 注解中添加 basePackages , 指定 FeignClient 所在的包
- 在 @EnableFeignClients 注解中添加 clients , 指定具体 FeignClient 的字节码
五 . 扩展
5.1 Feign 调用注意事项
我们先来回顾一下目前所做的工作
order-service 远程调用 user-service 这个微服务 , 我们无需再使用 RestTemplate 这种方式了 , 我们使用 Feign 这种声明式的客户端 .
我们目前已经引入了 Feign 客户端以及 HTTP 连接池的依赖
然后在 order-service 服务的启动类中开启了 Feign 远程调用
然后我们定义了一个接口 , 在后面指定了我们要调用的微服务 , 那每个微服务也需要暴露自己的接口 .
那目前我们是传一个参数的 , 如果传两个参数呢 ?
那我们要想访问这个接口 , URL 就需要长成这个样子 : http://localhost:8081/user/testData2?userName=xx&age=18
那我们如何定义这个接口对应的 Feign 方法呢 ?
直接将这个方法体部分复制过去
此时我们重启 order-service 与 user-service 服务 , 发现 order-service 就已经报错了
那我们怎样解决这个问题呢 ?
我们需要在 Feign 部分的参数前面添加 @RequestParam(“”) 参数表示从 query string 中获取数据
此时重新运行就不报错了
小结 :
- Feign 核心能力与使用
- 替代 RestTemplate:通过声明式接口简化远程调用,解决 URL 维护复杂、代码可读性差的问题,使用步骤包括引入依赖、添加注解、定义客户端接口。
- 请求方式支持:支持 GET、POST 等多种请求方式,POST 请求需通过
@RequestBody
传递参数。
- 自定义配置与日志
- 配置类型:可自定义日志级别、编解码器、重试机制等,日志级别分为 NONE、BASIC、HEADERS、FULL 四级。
- 配置方式:支持配置文件(全局 / 局部)和 Java 代码(全局 / 局部)两种方式,优先级为局部配置 > 全局配置。
- 性能优化实践
- 连接池优化:使用 Apache HttpClient 或 OKHttp 替代默认 URLConnection,通过配置连接池参数(最大连接数、路径连接数)提升性能。
- 日志策略:生产环境建议使用 BASIC 级别减少日志开销。
- 最佳实践与扩展
- 代码复用:推荐抽取 FeignClient 为独立模块,共享 POJO 和配置,避免继承方式的紧耦合问题。
- 多参数调用:使用
@RequestParam
明确标注查询参数,确保远程调用参数解析正确。