👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:详解SpringCloud微服务技术栈:Nacos配置管理
📚订阅专栏:微服务技术全家桶
希望文章对你们有所帮助
之前使用RestTemplate来实现远程调用,这种方式存在了一些问题,更优的解决方式是使用Feign来实现远程调用。
这里将会讲解如何用Feign实现远程调用,并进行最佳实践。
Feign的远程调用与最佳实践
- 基于Feign实现远程调用
- 自定义配置
- 性能优化
- Feign最佳实践
- 分析
- 实现
基于Feign实现远程调用
RestTemplate的问题:
1、代码可读性差,变成体验不统一
2、参数比较复杂的url,难以维护
Feign是一个声明式的http客户端,官方地址:Feign官网
声明式在Spring中开始提到,是利用配置文件来加事务。而声明式http客户端也是类似,我们只需要把发http请求所需要的信息声明出来即可,剩下的东西都交给Feign来实现。
使用Feign的步骤:
1、引入依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、在order-service的启动类添加开启Feign的功能:加注解@EnableFeignClients
3、做声明(编写Feign客户端):
@FeignClient("userservice") //声明出服务名称
public interface UserClient {@GetMapping("/user/{id}")User findById(@PathVariable("id") Long id);
}
主要是基于SpringMVC的注解来声明远程调用的信息,如服务名称、 请求方式、请求路径、请求参数、返回值类型等,直接声明这些信息就行。
这样的方式会简单很多,注解开发太方便了,节约了很多学习成本,即便url的参数很复杂,我们利用注解开发写参数列表也是很方便的。
现在我们可以直接使用这个客户端了,直接修改order查询的接口:
@Resourceprivate OrderMapper orderMapper;@Resourceprivate UserClient userClient;public Order queryOrderById(Long orderId){//查询订单Order order = orderMapper.findById(orderId);//根据用户id来查询用户,用Feign实现远程调用User user = userClient.findById(order.getUserId());//将用户注入到order中order.setUser(user);// 4.返回return order;}
同时多次刷新网址,可以验证出Feign还集成了Ribbon负载均衡,是比较强大的。
总结Feign使用步骤:
引入依赖
添加@EnableFeignClients注解
编写FeignClient接口
使用FeignClient中定义的方法来代替RestTemplate
自定义配置
Feign可以让我们自定义配置来覆盖默认配置,一般需要配置的是日志级别的类型feign.Logger.Level。
配置Feign日志有2种方式。
方式一:配置文件
1、全局生效
feign:client:config:default:loggerLevel: FULL # 最高级别,日志中会包含发起的请求,以及远程调用等信息
2、局部生效
feign:client:config:userservice:loggerLevel: FULL
方式二:Java代码
先声明一个Bean:
public class DefaultFeignConfiguration {@Beanpublic Logger.Level LogLevel(){return Logger.Level.BASIC;}
}
想要这个类生效,需要配置。
1、全局配置:把它放到@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
2、局部配置:把它放到@FeignClient这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
重启order-service,访问网址后查看控制台的日志,除了SQL语句之外,还有关于Feign的日志信息(只有请求行和相应行)
性能优化
Feign其实性能一直很不错了,但是还是可以被优化,Feign底层的客户端实现:
URLConnection:默认实现,不支持连接池
Apache HttpClient:支持连接池
OKHttp:支持连接池
Feign底层还是会用到其他的客户端,默认使用URLConnection,但是它不支持连接池,这就会使得性能不是太好。因此更推荐使用其他两种。
Feign的性能优化主要包括:
使用连接池代替默认的URLConnection
日志级别,最好用BASIC或NONE,日志级别太高也会造成一些性能损耗
将URLConnection换成Apache HttpClient:
1、引入依赖:
<!--引入HttpClient依赖--><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId></dependency>
2、配置连接池:
feign:httpclient:enabled: true # 支持HttpClient的开关max-connections: 200 # 最大连接数max-connections-per-route: 50 # 单个路径的最大连接数
其中,最大连接数和单个路径最大连接数并不能那么容易确定,具体的连接数需要根据业务的情况,对于不同的业务,可以用jmeter进行压力测试。
Feign最佳实践
分析
方式一:继承
给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
乍一看还是有点抽象的,但是我们可以打开一下order-service的UserClient接口与user-service的UserController:
这两个的函数其实是差不多的,因为消费者要通过Feign解析网址请求,就需要带上相应的信息(请求方式、网址、参数等),而提供者则需要提供正确的方式给消费者,因此信息上两者基本上都是差不多的。
所以理论上可以定义一个统一的父接口来作为标准。
public interface UserAPI{@GetMappingUser findById(@PathVariable("id") Long id);
}
而消费者和提供者只需要分别继承和实现这个接口就可以了。
但是这种方式有一定的问题,Spring官方也不推荐让客户端和服务器端共用一个接口,因为这样的耦合度太高了。
方式二:抽取
将FeignClient抽取为独立模块,并且把接口有关的pojo、默认的Feign都放到这个模块中,提供给所有消费者使用。
解析一下,在之前的代码中,order-service的UserClient会去调用user-service中的UserController,但是如果还有很多模块的UserClient都要调用,可能就会有很多地方重复写了,因此可以专门定义一个feign-api,在里面声明UserClient,并且涉及到的实体类、默认配置都在feign-api中实现。而order-service要使用的时候,只需要引入feign-api的依赖就可以了。
实现
这里将会实现方式二,步骤如下:
1、新建module,命名为feign-api,然后引入feign的starter依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
2、将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
上面拥有的部分,在order-service里面都可以直接删除了,以后需要直接去用feign-api里面的东西就可以了。
3、在order-service中引入feign-api的依赖
<dependency><!--groupId是你创建api的这个module时候的相关包--><groupId>com.wang</groupId><artifactId>feign-api</artifactId><version>1.0</version></dependency>
4、修改order-service中的所有与上述组件有关的import部分,改成导入feign-api的包
5、重启测试
运行后发现会报错,查看报错信息:
没有找到UserClient的对象,但是编译没有报错,只是运行报错了,查看OrderService:
可以想到,Spring没有获得这个容器的对象,这种情况的发生是因为扫描包出现了问题,只是因为启动类指定的Mapper是在cn.itcast.order下面的,然而UserClient是在cn.itcast.feign下面的。
简单粗暴的解决方式是直接把MapperScan扫描范围扩大,但是这肯定是不合适的,启动类应该默认扫描的范围就是所在的包下面的,应该想想别的办法。
方式一:指定FeignClient所在包(全盘拿来)
@EnableFeignClients(basePackages = "cn.itcast.feign.clients", defaultConfiguration = DefaultFeignConfiguration.class)
方式二:指定FeignClient字节码(精准定位,推荐方案)
@EnableFeignClients(clients = {UserClient.class}, defaultConfiguration = DefaultFeignConfiguration.class)