前言:
我们在使用Nacos和Eureka的时候都需要使用远程调用开关RestTemplate发送http请求,但是这种方式在代码编写层面太不优雅了,因此我们可以采用Feign来代替RestTemplate发送http请求。
注:此小节同样使用订单系统和用户系统作为代码案例。
一、RestTemlate和Feign的代码
1.1、RestTemplate
在订单系统调用用户系统的接口时我们之前编写的代码如下:
public Order queryOrderById1(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2.利用RestTemplate发送http请求,查询用户
//2.1、url路径
String url = "http://userservice/user/" + order.getUserId();
//2.2、发送http请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
//3.封装user到order
order.setUser(user);
// 4.返回
return order;
}
以上代码存在的问题:
- 代码可读性差,编程体验不统一
- 参数复杂URL难以维护
总而言之:太不优雅了~
1.2、Feign
1.2.1、导入Feign的依赖
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
1.2.2、在启动类添加注解开启Feign的功能
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
public class OrderApplication {
......
}
注解里面的参数我们先不讨论,后续会描述其功能。
1.2.3、编写Feign客户端
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)//只针对userservice服务有效 全局有效在启动类的注解当中配置即可
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
这里主要是基于SpringMVC的注解来声明远程调用的信息,比如:
对比我们之前使用的RestTemplate:
1.2.4、使用Feign客户端完成远程调用
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//基于Feign发送http请求(远程调用)
User user = userClient.findById(order.getUserId());
//3.封装user到order
order.setUser(user);
// 4.返回
return order;
}
二、Feign的自定义配置
我们配置Logger来看看:
方式1:基于配置文件的方式
①全局生效
②局部生效
方式2:基于java代码的方式
先声明一个Bean
public class DefaultFeignConfiguration { @Bean public Logger.Level logLevel(){ return Logger.Level.FULL; } }
声明完成后定义该Bean的作用域,即全局生效还是局部生效。
全局生效:在SpringBoot的启动类注解@EnableFeignClients添加如下参数,表明不论远程调用哪个微服务,都将采取配置类当中的配置。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
局部生效:在Feign客户端的类注解上添加如下参数,表明谁远程调用我这个微服务,将采用这个配置文件的信息。
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
三、Feign的性能优化
优化点1:
由于Feign是一个声明式的http客户端,它能做的只是把我们的声明变成http请求,最终发http请求时还是会用到别的客户端,例如一下这几种:
建立连接池的好处在于可以减少连接创建和销毁的性能损耗。(三次握手、四次挥手)
优化点2:
降低日志的级别,比如使用basic或none。
具体优化方式:
Feign添加httpClient的支持:
- 引入依赖
<!--引入httpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- 配置连接池
feign:
httpclient:
enabled: true #支持httpClient的开关
max-connections: 200 #最大连接数
max-connections-per-route: 50 #单个请求路径的最大连接数
总结:
四、Feign的最佳实践
方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
我们先观察Feign客户端和服务提供者Controller的代码
@GetMapping("/user/{id}") User findById(@PathVariable("id") Long id);
@Slf4j @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public User queryById(@PathVariable("id") Long id) String truth) { return userService.queryById(id); } }
可以观察到,两者的内容极其相似,因此我们可不可以让他们两都实现统一的接口?怎么做?
那么问题又来了,这样又有一些弊端:
服务紧耦合
父接口参数列表中的映射不会被继承
方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
先看图:
假设orderservice和payservice将来都调用userservice提供的服务,是不是需要在自己单独的模块当中写一遍userClient,这样一来,随着微服务的数量增加,userClient被写的次数也随之剧增。 解决方案:看图
将userClient抽取出来组成一个单独的模块,以后不管哪个微服务远程调用时都从这来拿,我把实体类都给你定义了,你只管把我的依赖导入,别的你什么都不用操心,这样,不就皆大欢喜了嘛~
对比两种方式,方式一的弊端在于耦合度的问题,方式二的问题在于,假设我orderservice只需要userClient,但是我把你整个模块的依赖都到导进来了,未免有点小题大作了,我也用不了其他的哪些Client啊~因此,没有完美的解决方案,具体采用哪种还是需要结合实际选择。
方式二具体操作:
- 首先创建一个module,命名为feign-api,然后引入feign的starter依赖
- 将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
- 在order-service中引入feign-api的依赖
<!--引入feign的统一api--> <dependency> <groupId>cn.itcast.demo</groupId> <artifactId>feign-api</artifactId> <version>1.0</version> </dependency>
- 修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包
import cn.itcast.feign.clients.UserClient; import cn.itcast.feign.pojo.User; @Autowired private UserClient userClient; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); //基于Feign发送http请求(远程调用) User user = userClient.findById(order.getUserId()); //3.封装user到order order.setUser(user); // 4.返回 return order; }
- 重启测试
- 重启会失败,原因是Spring启动时并没有扫描到创建的这个模块的这些代码,因此也并不会创建他们的实例,怎么办?