天天看点

SpringCloud 微服务接口调用组件 - OpenFeign 简介

作者:程序猿不相信眼泪

前言

本篇文章是 SpringCloud 专栏的第一篇,后面会逐步更新自己工作中使用到的微服务组件,以及所遇到的坑和自己填坑过程中的理解,在自己成长的过程中也希望对其他人有帮助,让大家能够少踩坑!

版本介绍

本专栏所有文章使用的版本均为 SpringCloud 2020.0.5、SpringBoot 2.5.0

OpenFeign 简介

OpenFeign 是一个声明式的 Rest 接口客户端,就把它理解成 HttpClient!能够实现服务接口的远程调用。也就是说假设现在你的微服务集群有 A,B 两个服务,那么你可以在 A 服务中定义一个接口,通过 OpenFeign 的相关注解,它能够帮你实现调用 A 的接口方法(interface)会自动调用远程的 B 服务暴露的 Rest 接口,使得 A 调用远程服务 B 的接口就像在调用本地方法一样。示例代码:

@FeignClient(
    contextId = "AuthClient",
    name = "AUTH",//注册中心的服务名
    path = "/auth",//项目路径前缀,没有就不填
    configuration = {FeignClientDecoderConfiguration.class, FeignRequestInterceptor.class}//自定义的配置
)
public interface AuthClient {

  @GetMapping("/getReviewerIds")
  List<Long> getReviewerIds(@RequestParam("adminId") Long adminId);
}
复制代码           

这样我们从 Spring 中拿到 Bean 调用 getReviewerIds 方法,即可请求到远程服务 AUTH 暴露的 Rest 接口。

@Autowired private AuthClient client;
public void demo(){
    List<Long> reviewerIds = client.getReviewerIds(1L);
}
复制代码           

具体 demo 我们可以参考官网给的示例 Feign Using Eureka。

为什么要用 OpenFeign

我始终坚持一个理念,那就是我们用技术一定不要为了用而用,要知道用了有什么好处,解决了什么问题,又引入了什么问题(自己学习除外)。

如果你一直投身于一个涉及业务交互的自研产品,那么大概率会体会到一个项目的演变过程,项目初期单体应用已经完全能够满足业务需求,随着业务量不断扩大,业务功能不断迭代。我们需要对一个庞大的系统进行业务模块的拆分、甚至系统拆分。如下图

SpringCloud 微服务接口调用组件 - OpenFeign 简介

当拆分出多个系统时就会涉及到系统之间的调用,想一想在 Feign、OpenFeign 之前,我们要调用远程接口通常是用 HttpClient、RestTemplate,对比三者

HttpClient RestTemplate OpenFeign
负载均衡 需要通过服务提供方的 Nginx 等实现 可以用Ribbon等实现负载均衡 通过服务发现,有官方提供的丰富负载均衡策略
编码 需要手动构建 url、请求体/请求参数、解析响应等,代码臃肿。每个服务调用都要写这段代码 和 HttpClient区别不大 支持 SpringMVC 注解,声明接口定义即可,代码量极少
设计思想 - - 面向接口
熔断、降级 - - 生态圈有丰富的支持组件

OpenFeign 与 SpringMVC

实际上如果你是从老版本一路升级过来的你就会发现 Feign 是对 RestTemplate + Ribbon 做了一次封装,而 OpenFeign 又在 Feign 的基础上进行了封装,使其支持 SpringMVC 相关注解和相关的消息转换器 HttpMessageConverters。

OpenFeign 完美结合 SpringMVC 定义 Controller 的注解,@GetMapping、@PathVariable、@RequestBody、@RequestParam 等等 SpringMVC 支持的注解。唯一有区别的是当以 Get 方式传递 Pojo 对象时,也使用 SpringMVC 时,要把 url 后面的问号参数映射成对象时,提供了一个新的注解叫做 @SpringQueryMap。

至于负载均衡,在 2020.x 版本的 SpringCloud 中已经移除了 ribbon 使用 SpringCloud-LoadBalance 实现负载均衡。

@FeignClient 配置简介

虽然内容官网都有,但是就我的经历对于新手来说官方文档(尤其是 Spring 的)并不友好,所以还是简单提一下。

配置方式一

查看 @FeignClient 源码,发现 configuration 属性的注释上写着默认值是 FeignClientsConfiguration

SpringCloud 微服务接口调用组件 - OpenFeign 简介

再查看这个类发现解码器、重试、熔断降级等都帮我们定义好了。其实非常简单就是用 @ConditionalOnMissingBean 去定义了相关 Bean 。

这样一来我们就知道了如果想自己定义配置那么写一个配置类作为 @FeignClient 的 configuration 属性值即可,例如上面示例代码中的 AuthClient 的两个自定义配置。

值得注意的是我们自己写的配置类最好不要加 @Configuration 注解,因为我们都知道标注 @Configuration 代表将这个类对象被 Spring 容器管理,这可能会对应用有全局影响。可能有不熟悉 Spring 的人会说,不对呀,你看官方自定义的 FeignClientsConfiguration 都用 @Configuration 标注了,但要注意的是 FeignClientsConfiguration 并没有被 @Import 或者被 Spring 扫描,所以它不会被 Spring 管理。但是我们写的配置都是在主类路径包下面的,默认会被 Spring 扫描到。

这里有一个最简单的判断某个配置类是否交给 Spring 管理了的方法,IDEA 开发环境下,直接在主类用 @Autowired 引入,如果有波浪线提示就说明它现在没有被 Spring 管理。

SpringCloud 微服务接口调用组件 - OpenFeign 简介

配置方式二

官方还给我们提供了一种配置方式,在配置文件中配置,查看 FeignClientProperties 源码发现有一个成员变量

private Map<String, FeignClientConfiguration> config = new HashMap<>();
复制代码           

key 是 FeignClient 的名字,value 是具体配置

feign:
    client:
        config:
            default: # @FeignClient 的名字,default 代表所有 FeignClient 的默认配置
                connectTimeout: 5000
                readTimeout: 5000
                loggerLevel: basic
            feignName: #指定单独的 FeignClient 进行配置(如果有contextId 这里填 contextId 值,否则填 name 属性值)
                connectTimeout: 1000
                readTimeout: 1000
                loggerLevel: FULL
复制代码           

配置文件的优先级要高于配置类,如果同时配置了 @FeignClient 的 configuration 和 配置文件,将会以配置文件优先。

实现原理推测

首先我们想想它是如何从一个 @FeignClient 标注的 interface 方法就能实现调用到远程服务接口的?让我们联想到 MyBatis 的 @Mapper 。有没有想过 @Mapper 定义的接口方法为什么能自动映射到 xml 文件的 SQL 语句?

我相信大家都会知道是通过代理嘛,肯定是框架给弄一个代理类出来,去实现了定义的接口,实现类里面其实大概就是使用 HttpClient、RestTemplate 远程调用的那一套代码,只是对于使用者来说是无感知的,因为都被封装好了。

SpringCloud 微服务接口调用组件 - OpenFeign 简介

那么第二个问题就出现了,@FeignClient 是怎么知道我们要调用哪一台服务的,我总得知道我要调用的服务所在机器 IP、port 等信息吧?所以肯定有一个地方是存储了服务提供方的服务实例信息,例如 实例集群数、每台实例的 IP 地址、端口等。那么很明显就是注册中心了,以 eureka 为例

SpringCloud 微服务接口调用组件 - OpenFeign 简介

@FeignClient 中的 name、value 属性值就是注册中心的服务名,表明当前接口绑定的远程服务。

我们的微服务应用启动后都会把自己的信息注册到注册中心,当 OpenFeign 进行服务调用时,会根据要调用的服务名去从本地寻找已缓存的注册中心服务提供者信息。

@FeignClient 接口放在消费端还是服务端

标题想表达的意思是,例如简介中的 AuthClient 类以及相关 XxxRequest、XxxResponse 是定义在 A 服务中?还是定义在 B 服务中的某个模块然后打一个 jar 包出来由 A 服务来引用?两者我都用过,其实经历下来我更倾向于前者。假如你用后者的话,让我们来感受一下开发一个 Feign 接口实现的步骤:

  1. 在 B 服务中写 Controller 实现
  2. 在 B 服务中定义 FeignClient 接口
  3. 在 B 服务中修改 jar 版本 +1,打一个 jar 包到本地仓库
  4. 在 A 服务中修改依赖 jar 版本,刷新 maven/gradle

乍一看不麻烦是吧?但是你要知道我们开发中经常会出现丢参数、缺响应属性等情况,一旦有任何小问题,都要重新走一遍上述流程。。。。

而对于前者所不好的地方无非是 XxxRequest、XxxResponse 类冗余了一份,但其实并没有什么问题,因为对于 Feign 来说请求和响应的 BO 类并不需要字段完全一致,它的解码器会智能的解析响应并封装到你的 XxxResponse 接收类中。

当然也有不同的观点支持并且坚持后者的,对此我只能说也不是不行

SpringCloud 微服务接口调用组件 - OpenFeign 简介

其实对于这个问题我在 V2EX 上发现了一个评论,我觉得说的还是蛮有道理的,贴一下

你这么理解就明白了,这个类 XxxRequest、XxxResponse 等,仅仅是你的 A 服务为了映射请求结果而本地自定义的一个映射数据结构,这个映射数据结构和 B 服务可以说是没关系的。所以你当然应该放在 A 这里。
你很纠结无非是你觉得这个东西似乎是可以复用的,所以纠结放 A 还是放 B,以及是不是要抽出来做个公共依赖。我很久以前也很纠结这个东西,但是踩了太多坑以后我的想法就变了,高内聚低耦合本质的意义,就是把和一个服务(组件,应用,包,等等等等)相关的代码全部包在一起,不要和外界有牵扯,你有牵扯就会引发修改时的依赖地狱。

引入 OpenFeign 带来的问题

引入一个新的技术组件,第一个要面对的问题就是需要有人力去学习研究,倒不是学习如何使用,因为这玩意随便看看官方文档就能用起来了,而是要在遇到生产问题时能够结合源码准确定位出问题,或者在组件基础上能够进行自定义的扩展去满足公司的业务需求,这通常需要有一定经验和能力的程序员。

其次引入 OpenFeign 带来的一个最直接的问题就是分布式事务问题,然而实际上这并不是 OpenFeign 带来的,而是升级到微服务系统不得不面对的一个问题,当前 Spring 的事务只能支持应用内回滚,当涉及到跨应用交互就不适用了。一个最简单的例子,用户下单使用优惠券。

@Transactional
public void submit(){
    //前置业务...
   couponClient.useCoupon(request);//跨服务使用优惠券成功
   //后置业务... throw 了一个 RunTimeException
}
复制代码           

订单服务调用优惠券服务,优惠券被成功使用了,但是订单服务报错,订单被回滚了,但是优惠券无法回滚,还是被使用了。

结语

本篇文章简单介绍了 OpenFeign ,这玩意用起来真的非常简单。后面会陆续介绍生产环境中使用 OpenFeign 遇到的问题以及解决方案。

原文链接:https://juejin.cn/post/7122405099904712711

来源:稀土掘金