目录
一、需求与背景
- 在分布式系统的场景下,大多数服务的拆分要进行微服务化,各微服务的内部不可能直接依赖其它微服务的实现,而是将该微服务内的POJO 类提取到一个公共模块,供其它的微服务进行依赖。
- 为了让代码的职责更加明确,实现更加清晰,就需要将业务代码与框架使用的模版代码分离,让开发人员将精力集中到业务功能的实现上。
二、OpenFeign 远程调用技术原理
- OpenFeign 是 Netflix 开源的一个项目,可以将远程的接口调用转换成本地调用的形式,简化了开发,并且做到了业务功能代码与框架模版代码分离的效果。
- OpenFeign 的使用核心是基于
@FeignClient
、@EnableFeignClients
这2个注解。@EnableFeignClients
注解加在项目的启动类上,当项目在启动时,会扫描注解中指定的目录,会为所有使用了@FeignClient
注解的接口创建一个动态代理对象,这些被创建的动态代理对象从属于@FeignClient
注解所修饰的接口的实例,并把这些动态代理对象加载到 Spring 的 Bean 容器内,随后会被注入到需要进行远程调用的本地服务对象内。
三、项目代码演示
接下来使用一个积分项目进行演示。这个项目分为3个微服务:商品(goods)、用户(customer)、计算(caculation)。用户通过购买商品获取积分;不同的商品可能参加了不同的活动,有的商品不赠送积分;不同的用户等级可能通过购物获取的积分不同。
3.1 引入依赖
要使用
openFeign
组件,首先我们需要先引入相应的依赖。注意,使用
openFeign
还需要引入
loadBalancer
的依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
由于在父项目中已经引入了
spring-cloud-dependencies
的依赖,上述2个组件的版本号可通过版本仲裁确定,无需指定。
3.2 实现@OpenFeign注解修饰接口
实现OpenFeign组件的远程调用,首先需要实现
@FeignClient
注解修饰的接口,该接口会将本地调用转化成远程的接口调用。
@FeignClient(value ="coin-goods-svc", path ="/goods")publicinterfaceGoodsService{// 获取指定商品@GetMapping("/getGoods")GoodsInfogetGoods(@RequestParam("id")Long id);// 获取整个购物车内的商品@GetMapping("/getBatch")Map<Long,GoodsCart>getGoodsCart(@RequestParam("ids")Collection<Long> ids);}
原来我们是直接通过 webclient 直接发起的远程接口调用,内容如下:
webClientBuilder.build().get().uri("http://coin-goods-svc/goods/getGoods?id="+ id).retrieve().bodyToMono(GoodsInfo.class).block();
现在我们直接注入
GoodsService
并调取对应方法:
@AutowiredprivateGoodsService goodsService;publicIntegergetGoodsCoin(Long id){// 忽略无关逻辑// 获取指定商品GoodsInfo goods = goodsService.getGoods(id);}
现在我们不必再业务代码里面指定调用接口的URI 和 Method 了,做到了代码的职责分离。
3.3 指定 OpenFeign 远程调用接口的扫描路径
接下来,我们需要在微服务的启动类上加上
@EnableFeignClients
注解,这样服务启动时的,才会扫描指定目录下使用了
@FeignClient
注解的接口,并为其生成对应的动态代理实例。
// 省略其它注解@EnableFeignClients(basePackages ={"com.fyup"})publicclassCustomerApplication{// 忽略具体实现}
我们也可以不指定具体的扫描路径,而是直接指定要进行代理的接口,不过这种方式在
OpenFeign
远程调用接口较多时会很麻烦,不具备扩展性,不符合开闭原则。
// 通过指定具体的远程调用接口// 省略其它注解@EnableFeignClients(clients ={GoodsService.class})publicclassCustomerApplication{// 忽略具体实现}
四、OpenFeign 在日志中打印Request和Response
OpenFeign 还提供了在日志中打印远程调用细节的功能,只需要开启相应配置,并向 Spring Bean 容器内注册对应的 Bean 即可。可打印的日志分为4个级别,这里贴一下源码的截图:
如上所示,如果我们选择
Level.FULL
级别,会打印完整的 Request 和 Response 的 Header、Body。
因为 OpenFeign 组件内的日志都是以
Debug
级别输出的,所以我们需要现将对应的远程调用接口的日志输出级别打开。
logging:level:com.fyup.coin.customer.feign.GoodsService: debug
因为要打印完整的 URL、Method、以及Request 和 Response 的 Header、Body 等信息,所以还需要在配置类中注入
Level.FULL
的 Bean。实现如下:
@BeanLogger.LevelfeignLogger(){returnLogger.Level.FULL;}
五、OpenFeign 客户端超时配置
接口超时不响应会悬挂消费者请求,大量的请求超时未响应会给系统造成很大压力,在调用链路过长的情况下可能会在系统内部产生雪崩反应。
我们可以在Feign客户端的
application.yml
文件中的
feign.client.config
配置项配置Feign 客户端远程调用的超时时间。
feign.client.config.default
配置项配置全局的超时时间,通过
feign.client.config.${serviceName}
配置访问某个特定微服务的超时时间。 示例配置如下:
# 其它忽略feign:client:config:# 全局超时配置default:connectTimeout:1000readTimeout:3000# 针对coin-goods-svc服务的超时配置coin-goods-svc:connectTimeout:1000readTimeout:2000
其中,
connectTimeout
表示的是服务消费者与服务提供者建立远程连接的超时时间;
readTimeout
是指从发出请求开始,到服务端响应请求之前为客户端设置的请求超时时间。
六、使用 OpenFeign 实现服务降级
我们也可以使用
OpenFeign
组件实现服务降级,用法就是使用
@FeignClient
注解修饰远程调用接口时,使用
@FiegnClient
注解的
fallback
属性或者
fallbackFactory
属性指定降级方法的降级类即可。
6.1 实现降级
6.1.1 引入依赖
因为
OpenFeign
实现服务降级依赖于
hystrix
,因此我们需要先引入
hystrix
依赖如下:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId><version>2.2.9.RELEASE</version><exclusions><exclusion><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-netflix-ribbon</artifactId></exclusion></exclusions></dependency>
6.1.2 降级方式一
下面实现了一个降级类示例,该降级类是通过
@FiegnClient
注解的
fallback
属性来指定的:
@Slf4j@ComponentpublicclassGoodsServiceDowngradeimplementsGoodsService{// 获取指定商品@OverridepublicGoodsInfogetGoods(Long id){// 对应的远程调用方法执行失败后会调用该方法,根据业务情况自行设计实现细节,此处仅打印日志
log.info("fallback method in getGoods.");returnnull;}// 获取整个购物车内的商品@OverridepublicMap<Long,GoodsCart>getGoodsCart(Collection<Long> ids){// 对应的远程调用方法执行失败后会调用该方法,根据业务情况自行设计实现细节,此处仅打印日志
log.info("fallback method in getGoodsCart.");returnnull;}}
接下来在远程调用接口内指定降级类:
@FeignClient(value ="coin-goods-svc", path ="/goods", fallback=GoodsServiceDowngrade.class)publicinterfaceGoodsService{// 获取指定商品@GetMapping("/getGoods")GoodsInfogetGoods(@RequestParam("id")Long id);// 获取整个购物车内的商品@GetMapping("/getBatch")Map<Long,GoodsCart>getGoodsCart(@RequestParam("ids")Collection<Long> ids);}
6.1.2 降级方式二
下面实现的降级类,是通过
@FiegnClient
注解的
fallbackFactory
属性来指定的,与上面不同的是,使用降级工厂处理降级可以获取到远程调用方法失败的原因。
@Slf4j@ComponentpublicclassGoodsServiceDowngradeFactoryimplementsFallbackFactory<GoodsService>{@OverridepublicGoodsServicecreate(Throwable cause){returnnewGoodsService(){@OverridepublicGoodsInfogetGoods(Long id){// 根据业务情况自行设计实现细节,此处仅打印日志
log.info("fallback method in getGoods.", cause);returnnull;}@OverridepublicMap<Long,GoodsCart>getGoodsCart(Collection<Long> ids){
log.info("fallback method in getGoodsCart.", cause);returnnull;}};}}
下面我们在
@FeignClient
注解修饰的远程调用接口内使用我们实现的降级工厂:
@FeignClient(value ="coin-goods-svc", path ="/goods", fallbackFactory=GoodsServiceDowngradeFactory.class)publicinterfaceGoodsService{// 获取指定商品@GetMapping("/getGoods")GoodsInfogetGoods(@RequestParam("id")Long id);// 获取整个购物车内的商品@GetMapping("/getBatch")Map<Long,GoodsCart>getGoodsCart(@RequestParam("ids")Collection<Long> ids);}
注意我们在使用的时候,降级类和降级工厂同时只能选择其中一种。
6.2 开启降级配置
最后一步,开启降级的配置开关
feign.circuitbreaker.enabled: true
,使代码生效。即在
application.yml
文件中,原来的配置中加上:
# 其它忽略feign:client:config:# 全局超时配置default:connectTimeout:1000readTimeout:3000# 针对coin-goods-svc服务的超时配置coin-goods-svc:connectTimeout:1000readTimeout:2000circuitbreaker:enabled:true
版权归原作者 伏游 所有, 如有侵权,请联系我们删除。