尽管使用了注册中心来解决URL的硬编码等问题,但是如今使用RestTemplate
还是存在以下问题:
- 代码可读性差,编程体验不统一;
- 参数复杂URL难以维护。
Feign是一个声明式的HTTP客户端,作用于服务消费者,在服务消费者中为服务提供者创建一个HTTP远程调用。官方地址:https://github.com/OpenFeign/feign。其作用就是帮助我们优雅的实现HTTP请求的发送,解决上面提到的问题。
使用 Feign
使用Feign非常简单,大致分为以下步骤:
-
在
pom.xml
中引入Feign客户端依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
在项目的启动类上添加注解
@EnableFeignClients
以开启Feign的功能。例如为order-service
(服务消费者)开启Feign:@EnableFeignClients @MapperScan("asia.linner.demo.order.mapper") @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
-
为服务提供者编写一个
FeignClient
接口。例如在order-service
(消费者)中为user-service
(提供者)编写FeignClient
:@FeignClient("user-service") public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable Long id); }
@FeignClient
:标注该接口为FeignClient
,其value
属性指定一个客户端的服务名称。@GetMapping
:为了方便使用,Feign使用的是Spring的注解,其用法和作用与Spring中的类似。
FeignClient
可以放在项目中的clients
包下。 -
通过
FeignClient
远程调用服务。例如在order-service
(消费者)中通过FeignClient
远程调用user-service
(提供者):@Service public class OrderService { @Autowired private OrderMapper orderMapper; // 注入Feign客户端 @Autowired private UserClient userClient; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.利用Feign远程调用 User user = userClient.findById(order.getUserId()); // 3.封装User到Order order.setUser(user); // 4.返回 return order; } }
注意:使用了
FeignClient
,原本声明RestTemplate
的Bean
可以删除掉。因为使用FeignClient
并不需要RestTemplate
的Bean
。
FeignClient 配置
Feign可以修改的配置如下:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level |
修改日志级别 | Feign包含四种不同的日志级别:
|
feign.codec.Decoder |
响应结果的解析器 | HTTP远程调用的结果做解析,例如解析JSON字符串为Java对象。 |
feign.codec.Encoder |
请求参数编码 | 将请求参数编码,便于通过HTTP请求发送。 |
feign.Contract |
支持的注解格式 | 默认是SpringMVC的注解。 |
feign.Retryer |
失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试。 |
一般需要配置的是日志级别。有以下几种配置方式:
在application.yml
中对全局进行配置:
feign: # Feign配置
client: # 客户端配置
config:
default: # 默认配置(全局生效)
logger-level: FULL # 日志级别
在application.yml
中对指定的服务进行配置。例如在order-service
中对user-service
进行配置:
feign: # Feign配置
client: # 客户端配置
config:
user-service: # 指定服务进行配置
logger-level: HEADERS # 日志级别
只需要将全局默认配置中的default
改成指定的服务名称即可。
另外一种方法是创建FeignClient
配置类:
import feign.Logger;
/**
* FeignClient配置类
*/
public class DefaultFeignClientConfig {
@Bean
public Logger.Level getFeignLogLevel() {
return Logger.Level.BASIC;
}
}
注意:
FeignClient
配置类中的Logger
导入的是feign
包下的Logger
。并且在application.yml
中的配置需要注释掉,否则即使开启了配置,配置类中的配置也不会生效。因为application.yml
中的配置会将配置类中的配置覆盖掉。
创建好了FeignClient
配置类,这些配置并不会生效。因为FeignClient
配置类中并没有任何信息告诉Spring这个是个配置类。所以需要对配置类进行声明。
有两种声明方式,一种是在项目的启动类中进行声明,告诉Spring这个是FeignClient
的配置类。并且这种声明方式会在全局生效。声明FeignClient
的配置类需要在启动类中使用@EnableFeignClients
注解,并为其defaultConfiguration
属性指定该FeignClient
的配置类的class
。例如为order-service
声明该配置类:
@EnableFeignClients(defaultConfiguration = DefaultFeignClientConfig.class) // 全局默认的Feign配置
@MapperScan("asia.linner.demo.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
另外一种方式是,在某个具体服务FeignClient
的接口中进行声明。该方式使用@FeignClient
注解,并为其configuration
属性指定一个FeignClient
配置类的class
。例如在order-service
中为user-service
声明使用一个UserFeignClientConfig
配置类(假设已经创建好了该配置类):
@FeignClient(value = "user-service", configuration = UserFeignClientConfig.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable Long id);
}
记录日志会损耗一部分性能,所以除了在开发过程中使用
FULL
日志级别。在生产环境中最好使用BASIC
或NONE
日志级别以减少性能损耗(尽量使用BASIC
)。
配置连接池
每次HTTP请求,都需要三次握手去建立连接,完成后再断开连接。在高并发的情况下,这样往复地操作会造成的性能损耗是比较大的。引入连接池是为了减少这种性能的损耗。
Feign底层发起HTTP请求,依赖于其它的框架。其底层客户端实现包括:
连接池 | 说明 |
---|---|
URLConnection | 默认实现,不支持连接池 |
Apache HttpClient | 支持连接池 |
OKHttp | 支持连接池 |
提高Feign的性能主要手段就是使用HttpClient或OKHttp连接池代替默认的URLConnection。
这里选择使用HttpClient。首先在消费者中引入其依赖:
<!--HttpClient依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
然后在application.yml
中修改配置:
feign: # Feign配置
httpclient: # HttpClient配置
# 如果要使用OKHttp,在feign.okhttp中做相应的配置即可
enabled: true # 支持HttpClient的开关
# 默认是true,但是没引入依赖不会生效
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个请求路径的最大连接数
提高Feign的性能还可以对连接池客户端的最大连接数根据实际情况进行相应的配置调整。
抽取API接口
由于FeignClient
接口中编写的接口方法与其对应的提供者中的Controller的方法一致。所以可以对FeignClient
接口和Controller做一个统一的API接口抽取,然后再通过集成的方式分别去实现FeignClient
和Controller。但是这样的方法有以下缺点:
-
服务提供方、服务消费方紧耦合。
-
参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解。
在API父接口的方法参数中使用的注解不会对Spring(也就是Controller)生效。即
@PathVariable
、@RequestParam
这样的注解,在API父接口中声明了,在对应的Controller中也需要再次声明。
以user-service
为例:
-
API父接口:
public interface UserAPI { @GetMapping("/user/{id}") User findById(@PathVariable Long id); }
-
FeignClient
:@FeignClient("user-service") public interface UserClient {}
-
Controller:
@RestController public interface UserAPI { User findById(@PathVariable Long id) { /* 业务代码... */ } }
这种方法的优点是简单、实现了代码共享,遵循了面向契约的编程思想。
抽取 feign-api 模块
另外一种方式是将所有的提供者对应的FeignClient抽取为独立的模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,通过Maven引入依赖的方式提供给所有消费者使用。
假设有多个消费者都需要调用到同一个提供者。如果让消费者分别实现自己的FeignClient,不仅会有许多冗余的代码,而且也不利于维护。但是如果使用这种方式将FeignClient抽取出来,可以由实现提供者的程序员来提供对应的feign-api实现。
这样的方法也有一些缺点,在使用一个提供者的接口时,需要同时引入该提供者的所有接口和其它提供者的所有接口。
抽取feign-api
的步骤:
-
创建一个新的模块,命名为
feign-api
。 -
在
feign-api
中引入Feign的Stater依赖:<!-- Feign客户端依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
如果要默认使用
HttpClient
连接池,还需要导入其坐标:<!--HttpClient依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
注:引入坐标是为了在消费者的
pom.xml
中可以不同显式地导入HttpClient
的依赖(使用Maven的依赖传递),但是HttpClient
的配置还是得在消费者中的application.yml
中配置。如果在
feign-api
中创建application.yml
中并配置HttpClient
,这样的配置是不会在消费者中生效的。因为feign-api
没有启动类,而且消费者的启动类也不会使用feign-api
中application.yml
的配置。除非使用配置类编写对HttpClient
的配置,并且在消费者中启用这个配置类。 -
将所有消费者的
FeignClient
、POJO和DefaultFeignClientConfig
都抽取到feign-api
模块中。注:
DefaultFeignClientConfig
的抽取是对所有的FeignClient
做一个默认的配置抽取。 -
在消费者中引入
feign-api
依赖。 -
在消费者中使用
feign-api
提供的API接口。注:需要修改消费者的启动类,在消费者的启动类上使用
@EnableFeignClients
注解的basePackages
或clients
属性指定扫描的FeignClient
包或具体的FeignClient
类,让消费者的启动类能扫描到feign-api
的FeignClient
。
以order-service
(消费者)和user-service
(提供者)为例:
-
创建
feign-api
,并导入Feign依赖。 -
将原本编写在
order-service
中的UserClient
、User
和DefaultFeignClientConfig
抽取出来,放在feign-api
中对应的包下。例如:asia.linner.demo.feign
(feign-api
的包名)下的包结构:clients
:UserClient.java
pojo
:User.java
config
:DefaultFeignClientConfig.java
抽取完成后,原本在
order-service
中的UserClient
、User
和DefaultFeignClientConfig
都可以删除。但是需要注意复制在feign-api
中的UserClient
、User
和DefaultFeignClientConfig
它们的包名要改成feign-api
的包名。 -
在
order-service
中导入feign-api
:<!--引入抽取的feign-api模块--> <dependency> <groupId>asia.linner.demo</groupId> <artifactId>feign-api</artifactId> <version>1.0</version> </dependency>
在
order-service
中的Feign依赖可以删除;如果有在feign-api
中导入并配置HttpClient,HttpClient的依赖也可以删除。需要注意引入order-service
中的UserClient
、User
和DefaultFeignClientConfig
它们的包名要改成feign-api
的包名。 -
让
order-service
的启动类扫描FeignClient
。因为
feign-api
和order-service
的包名并不相同(如asia.linner.demo.feign
和asia.linner.demo.order
),所以在没有扫描包指定的情况下order-service
的启动类并不能扫描到feign-api
中的UserClient
,所以会导致order-service
中的UserClient
注入失败。Feign的
@EnableFeignClients
注解提供了两种方式来让消费者的启动类扫描到FeignClient
:-
basePackages
:@EnableFeignClients(defaultConfiguration = DefaultFeignClientConfig.class, basePackages = "asia.linner.demo.feign.clients" // 扫描整个clients包 )
-
clients
:@EnableFeignClients(defaultConfiguration = DefaultFeignClientConfig.class, clients = {UserClient.class} // 指定需要加载的FeignClient接口 )
clients
属性的类型是一个class
数组,所以可以指定多个FeignClient
。推荐使用该方式。
在上述方法中选一种,然后修改
order-service
的启动类即可。 -
评论