跳转至

12 OpenFeign:服务间调用组件 OpenFeign 是怎么“隔空取物”的?

你好,我是姚秋辰。

在前面的课程中,我们借助Nacos的服务发现能力,使用WebClient实现了服务间调用。从功能层面上来讲,我们已经完美地实现了微服务架构下的远程服务调用,但是从易用性的角度来看,这种实现方式似乎对开发人员并不怎么友好。

我们来回顾一下,在前面的实战项目中,我是怎样使用WebClient发起远程调用的。

webClientBuilder.build()
    // 声明这是一个POST方法
    .post()
    // 声明服务名称和访问路径
    .uri("http://coupon-calculation-serv/calculator/simulate")
    // 传递请求参数的封装
    .bodyValue(order)
    .retrieve()
    // 声明请求返回值的封装类型
    .bodyToMono(SimulationResponse.class)
    // 使用阻塞模式来获取结果
    .block()

从上面的代码我们可以看出,为了发起一个服务请求,我把整个服务调用的所有信息都写在了代码中,从请求类型、请求路径、再到封装的参数和返回类型。编程体验相当麻烦不说,更关键的是这些代码没有很好地践行职责隔离的原则。

在业务层中我们应该关注具体的业务实现,而WebClient的远程调用引入了很多与业务无关的概念,比如请求地址、请求类型等等。从职责分离的角度来说,我们应该尽量把这些业务无关的逻辑从业务代码中剥离出去

那么,Spring Cloud中有没有一个组件,在实现远程服务调用的同时,既能满足简单易用的接入要求,又能很好地将业务无关的代码与业务代码隔离开呢?

这个可以有,今天我就来带你了解Spring Cloud中的一个叫做OpenFeign的组件,看看它是如何简化远程服务调用的,除此之外,我还会为你详细讲解这背后的底层原理。

了解OpenFeign

OpenFeign组件的前身是Netflix Feign项目,它最早是作为Netflix OSS项目的一部分,由Netflix公司开发。后来Feign项目被贡献给了开源组织,于是才有了我们今天使用的Spring Cloud OpenFeign组件。

OpenFeign提供了一种声明式的远程调用接口,它可以大幅简化远程调用的编程体验。在了解OpenFeign的原理之前,我们先来体验一下OpenFeign的最终疗效。我用了一个Hello World的小案例,带你看一下由OpenFeign发起的远程服务调用的代码风格是什么样的。

String response = helloWorldService.hello("Vincent Y.");

你可能会问,这不就是本地方法调用吗?没错!使用OpenFeign组件来实现远程调用非常简单,就像我们使用本地方法一样,只要一行代码就能实现WebClient组件好几行代码干的事情。而且这段代码不包含任何业务无关的信息,完美实现了调用逻辑和业务逻辑之间的职责分离。

那么,OpenFeign组件在底层是如何实现远程调用的呢?接下来我就带你了解OpenFeign组件背后的工作流程。

OpenFeign使用了一种“动态代理”技术来封装远程服务调用的过程,我们在上面的例子中看到的helloWorldService其实是一个特殊的接口,它是由OpenFeign组件中的FeignClient注解所声明的接口,接口中的代码如下所示。

@FeignClient(value = "hello-world-serv")
public interface HelloWorldService {

    @PostMapping("/sayHello")
    String hello(String guestName);
}

到这里你一定恍然大悟了,原来远程服务调用的信息被写在了FeignClient接口中。在上面的代码里,你可以看到,服务的名称、接口类型、访问路径已经通过注解做了声明。OpenFeign通过解析这些注解标签生成一个“动态代理类”,这个代理类会将接口调用转化为一个远程服务调用的Request,并发送给目标服务。

那么OpenFeign的动态代理是如何运作的呢?接下来,我就带你去深入了解这背后的流程。

OpenFeign的动态代理

在项目初始化阶段,OpenFeign会生成一个代理类,对所有通过该接口发起的远程调用进行动态代理。我画了一个流程图,帮你理解OpenFeign的动态代理流程,你可以看一下。

上图中的步骤1到步骤3是在项目启动阶段加载完成的,只有第4步“调用远程服务”是发生在项目的运行阶段。

下面我来解释一下上图中的几个关键步骤。

首先,在项目启动阶段,OpenFeign框架会发起一个主动的扫包流程,从指定的目录下扫描并加载所有被@FeignClient注解修饰的接口。

然后,OpenFeign会针对每一个FeignClient接口生成一个动态代理对象,即图中的FeignProxyService,这个代理对象在继承关系上属于FeignClient注解所修饰的接口的实例。

接下来,这个动态代理对象会被添加到Spring上下文中,并注入到对应的服务里,也就是图中的LocalService服务。

最后,LocalService会发起底层方法调用。实际上这个方法调用会被OpenFeign生成的代理对象接管,由代理对象发起一个远程服务调用,并将调用的结果返回给LocalService。

我猜你一定很好奇:OpenFeign是如何通过动态代理技术创建代理对象的?我画了一张流程图帮你梳理这个过程,你可以参考一下。

我把OpenFeign组件加载过程的重要阶段画在了上图中。接下来我带你梳理一下OpenFeign动态代理类的创建过程。了解了这个过程,你就会更加理解下节课的实战内容。

  1. 项目加载:在项目的启动阶段,EnableFeignClients注解扮演了“启动开关”的角色,它使用Spring框架的Import注解导入了FeignClientsRegistrar类,开始了OpenFeign组件的加载过程。
  2. 扫包:FeignClientsRegistrar负责FeignClient接口的加载,它会在指定的包路径下扫描所有的FeignClients类,并构造FeignClientFactoryBean对象来解析FeignClient接口。
  3. 解析FeignClient注解:FeignClientFactoryBean有两个重要的功能,一个是解析FeignClient接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的ReflectiveFeign完成的。
  4. 构建动态代理对象:ReflectiveFeign包含了OpenFeign动态代理的核心逻辑,它主要负责创建出FeignClient接口的动态代理对象。ReflectiveFeign在这个过程中有两个重要任务,一个是解析FeignClient接口上各个方法级别的注解,将其中的远程接口URL、接口类型(GET、POST等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的MethodHandler类作为方法级别的代理;另一个重要任务是将这些MethodHandler方法代理做进一步封装,通过Java标准的动态代理协议,构建一个实现了InvocationHandler接口的动态代理对象,并将这个动态代理对象绑定到FeignClient接口上。这样一来,所有发生在FeignClient接口上的调用,最终都会由它背后的动态代理对象来承接。

MethodHandler的构建过程涉及到了复杂的元数据解析,OpenFeign组件将FeignClient接口上的各种注解封装成元数据,并利用这些元数据把一个方法调用“翻译”成一个远程调用的Request请求。

那么上面说到的“元数据的解析”是如何完成的呢?它依赖于OpenFeign组件中的Contract协议解析功能。Contract是OpenFeign组件中定义的顶层抽象接口,它有一系列的具体实现,其中和我们实战项目有关的是SpringMvcContract这个类,从这个类的名字中我们就能看出来,它是专门用来解析Spring MVC标签的。

SpringMvcContract的继承结构是SpringMvcContract->BaseContract->Contract。我这里拿一段SpringMvcContract的代码,帮助你深入理解它是如何将注解解析为元数据的。这段代码的主要功能是解析FeignClient方法级别上定义的Spring MVC注解。

// 解析FeignClient接口方法级别上的RequestMapping注解
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
   // 省略部分代码...

   // 如果方法上没有使用RequestMapping注解,则不进行解析
   // 其实GetMapping、PostMapping等注解都属于RequestMapping注解
   if (!RequestMapping.class.isInstance(methodAnnotation)
         && !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
      return;
   }

   // 获取RequestMapping注解实例
   RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
   // 解析Http Method定义,即注解中的GET、POST、PUT、DELETE方法类型
   RequestMethod[] methods = methodMapping.method();
   // 如果没有定义methods属性则默认当前方法是个GET方法
   if (methods.length == 0) {
      methods = new RequestMethod[] { RequestMethod.GET };
   }
   checkOne(method, methods, "method");
   data.template().method(Request.HttpMethod.valueOf(methods[0].name()));

   // 解析Path属性,即方法上写明的请求路径
   checkAtMostOne(method, methodMapping.value(), "value");
   if (methodMapping.value().length > 0) {
      String pathValue = emptyToNull(methodMapping.value()[0]);
      if (pathValue != null) {
         pathValue = resolve(pathValue);
         // 如果path没有以斜杠开头,则补上/
         if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
            pathValue = "/" + pathValue;
         }
         data.template().uri(pathValue, true);
         if (data.template().decodeSlash() != decodeSlash) {
            data.template().decodeSlash(decodeSlash);
         }
      }
   }

   // 解析RequestMapping中定义的produces属性
   parseProduces(data, method, methodMapping);

   // 解析RequestMapping中定义的consumer属性
   parseConsumes(data, method, methodMapping);

   // 解析RequestMapping中定义的headers属性
   parseHeaders(data, method, methodMapping);
   data.indexToExpander(new LinkedHashMap<>());
}

通过上面的方法,我们可以看到,OpenFeign对RequestMappings注解的各个属性都做了解析。

如果你在项目中使用的是GetMapping、PostMapping之类的注解,没有使用RequestMapping,那么OpenFeign还能解析吗?当然可以。以GetMapping为例,它对RequestMapping注解做了一层封装。如果你查看下面关于GetMapping注解的代码,你会发现这个注解头上也挂了一个RequestMapping注解。因此OpenFeign可以正确识别GetMapping并完成加载。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
// ...省略部分代码
}

到这里,相信你已经了解了OpenFeign的工作流程,下节课我将带你进行实战项目的改造,将coupon-customer-serv中的WebClient调用替换为基于OpenFeign的远程服务调用。

总结

现在,我们来回顾一下这节课的重点内容。

今天你清楚了OpenFeign要解决的问题,我还带你了解了OpenFeign的工作流程,这里面的重点是动态代理机制。OpenFeing通过Java动态代理生成了一个“代理类”,这个代理类将接口调用转化成为了一个远程服务调用。

动态代理是各个框架经常用到的技术,也是面试中的一个核心考点。对于大多数技术人员来说,日常工作就是堆业务代码,似乎是用不上动态代理,这部分的知识点就是面试前突击一下。但如果你参与到框架类业务的研发,你会经常运用到动态代理技术。我建议你借着这次学习OpenFeign的机会,深入研究一下动态代理的应用。

如果你对OpenFeign的动态代理流程感兴趣,想要摸清楚这里面的门道,我推荐你一个很高效的学习途径:Debug。你可以在OpenFeign组件的FeignClientsRegistrar中打上一个断点,这是OpenFeign初始化的起点,然后你以Debug模式启动应用程序,当程序执行到断点处之后,你可以手动一步步跟着断点往下走,顺藤摸瓜了解OpenFeign的整个加载过程。

思考题

结合这两节课我给你讲的服务调用的知识,通过阅读OpenFeign的源码,你能描述出OpenFeign底层的实现吗?欢迎在留言区写下自己的思考,与我一起讨论。

好啦,这节课就结束啦。欢迎你把这节课分享给更多对Spring Cloud感兴趣的朋友。我是姚秋辰,我们下节课再见!

精选留言(11)
  • ~ 👍(31) 💬(1)

    这节课好早就学完了,但是后面的思考题一直没有时间搞清楚,今天闲下来后 debug 一遍后,大概明白具体流程了。 我觉得 OpenFeign 的远程调用分为两步 1. 首先是生成动态代理类,然后将代理类作为 bean 注入到 Spring 容器中 2. 然后是在调用过程中,代理类通过进行网络请求,向调用方做请求。 动态代理的原理其实 duckduckgo 一下就能找到无数篇文章,就不多说了,这里 OpenFeign 其实底层调用的是 Feign 的方法,生成了代理类,使用的是 JDK 的动态代理,然后 bean 注入。 调用过程,就是代理类作为客户端向被调用方发送请求,接收相应的过程。其中,feign 自行封装了 JDK java.net 相关的网络请求方法,可以重点关注一下 Client 类。我之前没有了解过,以为是直接用的 netty 或者其他的网络中间件;请求过程中还有 Loadbalancer 进行负载均衡;收到响应后,还需要对响应类进行解析,才能真正取出正确的响应信息。 最后:这些天还在忙其他东西,尽管老师更一篇看一篇,但是有些还要深入了解的,就要等到把所有文章的思考题摸清楚后再一步步填坑了。比如 OpenFeign 用的自行封装的 jdk 网络组件,是否可以使用其他中间件(例如 netty)实现呢?在请求过程中实现的负载均衡 loadbalancer 是怎么工作的? 以及我在 debug 过程中发现的问题:一旦发起调用时间过长,就会报错 「stream is closed」,io 没学好的我这方面之后也得稍微努力一下;在使用 debug 启动过程中,就出现过对 feign 的动态代理类调用 hashcode 代理方法的情况,应该是 spring 某个组件调用的(我在是数据库相关),但具体是谁我也不清楚。 以上就是大体的总结和我自己记录的问题,希望以后有时间把挖的坑慢慢填上,如果有什么问题或者不对的地方,欢迎大家批评指出~

    2022-01-29

  • 卡特 👍(8) 💬(1)

    金丝雀发布在微服务之间的通过openfeign流量转发规则咋定义和实现?

    2022-01-17

  • Nico 👍(4) 💬(1)

    老师,OpenFeign是自动集成了Loadbalancer了吗?以前OpenFeign是集成的ribbon,为啥现在又改为Loadbalancer?这个是出于什么考虑

    2022-01-08

  • peter 👍(4) 💬(1)

    老师您好,我有4个问题,请指教:Q1:“构造请求路径、降级方法”,这句话中的“降级”是不是笔误? Q2:openfeign是否可以认为是webclient的一个升级版本? Q3:netflix体系中,feign是对ribbon的封装,feign的底层用的是ribbon,那么,openfeign的底层也是用webclient吗? Q4: webclient只是webflux的一个很小的部分,而且不是webflux的主要功能,对吗?

    2022-01-07

  • so long 👍(2) 💬(2)

    这个是不是和mybatis创建数据访问接口的代理类的过程差不多

    2022-01-07

  • 与路同飞 👍(1) 💬(1)

    如果公司用的是api网关去代理请求的。是不是就没有必要用openFeign了。没有服务名并且path也不一样

    2022-01-20

  • 西门吹牛 👍(0) 💬(1)

    老师,关于 OpenFeign 这样基于 HTTP 的远程调用,和 dubbo 这样基于 TCP 的远程调用,使用时候有什么选型建议吗?感觉 dubbo 的性能要好很多

    2022-05-12

  • 逝影落枫 👍(1) 💬(0)

    类似的还有retrofit 和forest吧

    2022-01-07

  • Java 👍(0) 💬(0)

    老师,openfegin被远程服务鉴权拦截了该怎么办

    2024-09-22

  • seker 👍(0) 💬(0)

    OpenFeign的动态代理流程图中,LocalService是指代前文的HelloWorldService吗?还是说LocalService是OpenFeign源码中真实存在的一个interface?

    2023-07-15

  • 第一装甲集群司令克莱斯特 👍(0) 💬(0)

    源码之下,了无秘密㊙️

    2022-01-07