openFeign 远程调用
# OpenFeign 远程调用
# 1. 问题分析
在传统的微服务架构中,我们通常使用 RestTemplate
进行服务间的 HTTP 调用。来看一下之前使用 RestTemplate
发起远程调用的代码:
存在的问题:
- URL 地址被硬编码到代码中,后期维护和修改不方便。
- 服务消费者无法直接感知服务提供者的状态,例如健康状态、异常等信息。
- 若服务提供者出现问题,服务消费者会抛出异常,导致用户看到异常页面,影响用户体验。
RestTemplate
方式的代码显得不够优雅,是否有更好的解决方案可以提高代码的可读性和可维护性?
# 2. 了解 Feign
Feign
是一个声明式的 HTTP 客户端,GitHub 地址:https://github.com/OpenFeign/feign (opens new window)。
它可以帮助我们通过简单的接口声明方式,实现服务间的 HTTP 请求调用,解决上述问题。
# 3. 项目整合 Feign
# 3.1 引入依赖
在 order-service
服务的 pom.xml
文件中引入 Feign 的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2
3
4
说明:引入的 Feign 依赖为我们提供了声明式的 HTTP 客户端,简化了微服务之间的远程调用。
# 3.2 添加注解
在 order-service
的启动类中添加 @EnableFeignClients
注解以开启 Feign 的功能:
@SpringBootApplication
@EnableFeignClients // 启用 Feign 客户端
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
2
3
4
5
6
7
说明:
@EnableFeignClients
注解用于启用 Feign 客户端,它会扫描并注册当前包及其子包下的所有使用了@FeignClient
注解的接口。
# 3.3 编写 Feign 客户端接口
创建一个 Feign 客户端接口,用于声明远程调用的服务:
package com.cisyam.order.client;
import com.cisyam.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 用户服务的 Feign 客户端接口
*/
@FeignClient("userservice") // 指定要调用的服务名称,Feign 会自动根据服务名称调用对应的服务
public interface UserClient {
/**
* 通过用户 ID 获取用户信息
* @param id 用户 ID
* @return 用户信息
*/
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
说明:
@FeignClient("userservice")
:表示这个接口是一个 Feign 客户端,它负责调用名为userservice
的微服务,Feign 会自动去注册中心(如 Nacos)查找这个服务。@GetMapping("/user/{id}")
:Feign 接口的声明方式与 SpringMVC 类似,指定请求的路径、请求方式以及路径参数。- Feign 通过这种声明式接口的方式,将远程服务调用简化为本地方法调用,提升了代码的可读性和可维护性。
# 3.4 使用 Feign 客户端
修改 order-service
中的 OrderService
类,将原先使用 RestTemplate
进行远程调用的代码替换为 Feign 客户端:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient; // 注入 Feign 客户端
public Order queryOrderById(Long orderId) {
// 1. 查询订单
Order order = orderMapper.findById(orderId);
// 2. 通过 Feign 客户端调用用户服务,获取用户信息
User user = userClient.findById(order.getUserId());
// 3. 封装用户信息到订单中
order.setUser(user);
// 4. 返回订单信息
return order;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
重点说明
- Feign 客户端
userClient
会根据我们定义的接口声明,自动生成对应的实现,在调用findById
方法时,它会发起 HTTP 请求,调用远程的userservice
服务的/user/{id}
接口。 - 这样,我们不需要自己去构造 URL,也不需要自己发起 HTTP 请求,Feign 已经帮我们完成了这些操作,代码更加简洁、可维护。
# 3.5 Feign 的工作原理
当我们使用 Feign 时,UserClient
只是一个接口,本身并没有实现类。Feign 在运行时会动态生成这个接口的实现类,具体的调用逻辑由 Feign 负责。
当我们调用 userClient.findById(id)
时,Feign 会:
- 根据
@FeignClient
中指定的服务名称(userservice
),从注册中心获取该服务的地址。 - 构造 HTTP 请求,根据接口方法中的注解(如
@GetMapping("/user/{id}")
)确定请求的路径和参数。 - 发起 HTTP 请求,并将返回结果封装为接口中定义的返回值类型。
具体的业务逻辑(如查询数据库)是在被调用的服务(如 userservice
)中实现的。例如,在 userservice
中可能有一个 UserController
,负责处理这个请求并从数据库中查询用户信息。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User findById(@PathVariable Long id) {
return userService.findById(id);
}
}
2
3
4
5
6
7
8
9
10
11
12
# 3.6 Feign使用小结
使用 Feign 的步骤如下:
- 引入依赖:在
pom.xml
中添加 Feign 相关依赖。 - 添加注解:在启动类中添加
@EnableFeignClients
注解,启用 Feign。 - 编写 Feign 客户端接口:通过
@FeignClient
注解声明远程调用的服务。 - 使用 FeignClient 中定义的方法代替
RestTemplate
:在业务逻辑中调用 Feign 接口,实现远程调用。
通过 Feign,我们可以将微服务之间的 HTTP 调用简化为本地方法调用,减少了硬编码的 URL,使代码更加优雅易懂。
提示
下面这种写法只适合于当前包下的远程调用,也就是说被调用的服务必须在当前包下。
默认情况下,
@EnableFeignClients
会扫描当前包及其子包下的@FeignClient
接口。如果远程调用的接口在其他包下,你可以通过指定basePackages
属性来指定需要扫描的包。@EnableFeignClients(basePackages = "com.example.otherpackage")
1这样就可以扫描其他包下的 Feign 客户端接口。
# 4. 自定义配置
Feign 提供了丰富的自定义配置选项,常见的配置类型如下表所示:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的日志级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果解析器 | HTTP 远程调用的结果解析器,例如解析 JSON 字符串为 Java 对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过 HTTP 请求发送 |
feign.Contract | 支持的注解格式 | 默认支持 SpringMVC 的注解 |
feign.Retryer | 失败重试机制 | 请求失败的重试机制,默认不启用,不过会使用 Ribbon 的重试 |
一般情况下,Feign 默认的配置已经能够满足大部分需求。但在需要自定义时,只需创建自定义的 @Bean
,覆盖默认的 Bean
即可。
# 4.1 配置文件方式
可以通过配置文件来修改 Feign 的日志级别,支持针对单个服务或所有服务进行配置。
# 针对单个服务的配置
在配置文件 application.yml
中,可以指定某个服务的 Feign 日志级别:
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别为 FULL
2
3
4
5
# 针对所有服务的配置
如果需要全局生效,可以使用 default
配置:
feign:
client:
config:
default: # 全局配置,作用于所有服务
loggerLevel: FULL # 日志级别为 FULL
2
3
4
5
# 日志级别说明
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法、URL、响应状态码和执行时间。
- HEADERS:在 BASIC 的基础上,额外记录请求和响应的头信息。
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
# 4.2 基于 Java 代码的配置方式
除了在配置文件中设置外,还可以基于 Java 代码修改 Feign 的日志级别。
# 创建自定义配置类
首先,声明一个配置类,并在其中配置 Feign 的日志级别:
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // 声明为配置类
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.BASIC; // 设置 Feign 日志级别为 BASIC
}
}
2
3
4
5
6
7
8
9
10
11
12
# 配置全局生效
如果希望配置全局生效,可以将其放到启动类的 @EnableFeignClients
注解中:
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class) // 全局生效的配置
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
2
3
4
5
6
7
# 配置局部生效
如果只希望在某个 Feign 客户端中使用自定义配置,可以将配置类放到对应的 @FeignClient
注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class) // 局部生效的配置
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
2
3
4
5
说明:
- 全局生效:将配置类放到
@EnableFeignClients
注解中,适用于所有 Feign 客户端。- 局部生效:将配置类放到
@FeignClient
注解中,仅对指定的 Feign 客户端生效。
# 4.3 配置日志级别的使用场景
通过自定义日志级别,可以灵活调试远程调用的细节。在开发阶段,通常使用 FULL
级别,以便查看详细的请求和响应信息;在生产环境中,可以使用 BASIC
级别,以减少日志量。
# 4.4 配置方式总结
- 配置文件方式:适合大范围的统一配置,可以通过
application.yml
设置全局或特定服务的配置。 - Java 代码方式:适合更加精细化的配置,可以选择全局或局部生效,并能结合复杂的逻辑进行动态调整。
# 5. Feign使用优化
# 5. Feign 使用优化
在 Feign 的默认实现中,底层发起 HTTP 请求使用的是 URLConnection
,这种实现不支持连接池,因此性能较低。为了提升 Feign 的性能,可以通过替换底层 HTTP 客户端来实现连接池功能。常用的底层客户端实现包括:
- URLConnection(默认):不支持连接池,性能较低。
- Apache HttpClient:支持连接池,性能更佳。
- OKHttp:支持连接池,且在高并发下表现良好。
在这里,我们将使用 Apache HttpClient 进行优化。
# 5.1 引入依赖
首先,在 order-service
的 pom.xml
文件中引入 Apache HttpClient 依赖:
<!--引入 Apache HttpClient 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2
3
4
5
说明:
feign-httpclient
依赖是 Feign 与 Apache HttpClient 的集成包,提供了支持连接池的 HTTP 客户端。
# 5.2 配置连接池
在 order-service
的 application.yml
文件中添加连接池相关配置:
feign:
client:
config:
default: # 全局配置
loggerLevel: BASIC # 日志级别,BASIC 表示记录基本的请求和响应信息
httpclient:
enabled: true # 开启 Feign 对 HttpClient 的支持
max-connections: 200 # 最大的连接数,用于限制整个应用可同时打开的 HTTP 连接数量
max-connections-per-route: 50 # 每个路径(服务)的最大连接数,用于限制单个服务的最大连接数
2
3
4
5
6
7
8
9
重点说明:
- max-connections:限制整个应用可以同时保持的最大 HTTP 连接数。设置合理的值可以避免系统资源耗尽。
- max-connections-per-route:限制每个服务(或路径)的最大连接数,可以防止单个服务占用过多资源,保证应用的稳定性。
# 5.3 验证优化结果
为了验证 Feign 是否正确使用了 Apache HttpClient,可以在 FeignClientFactoryBean
的 loadBalance
方法中设置断点进行调试:
以 Debug 方式启动 order-service
服务,查看调试信息,可以看到 Feign 底层使用的客户端已经替换为 Apache HttpClient
:
# 5.4 Feign 优化总结
通过以上步骤,我们可以有效提升 Feign 的性能,关键优化点包括:
日志级别设置:
- 日志级别尽量设置为
BASIC
,记录基本的请求和响应信息,避免记录过多的冗余信息,影响性能。
- 日志级别尽量设置为
替换 HTTP 客户端:
- 使用 Apache HttpClient 或 OKHttp 代替默认的
URLConnection
,可以启用连接池,提升高并发场景下的性能。 - 步骤:
- 引入
feign-httpclient
依赖。 - 在配置文件中开启
httpclient
功能,并设置连接池参数。
- 引入
- 使用 Apache HttpClient 或 OKHttp 代替默认的