首先,OpenFeign是什么?他是服务调用组件,在分布式系统中讲到的三个重要子项目中,它是其中spring-cloud官方组件之一,而追其背景可以发现,它和netfilx的feign也有着很深的渊源。Feign最早是由Netfilx公司维护的,最后Netflix将其转入社区维护才有了今天的OpenFeign。
服务调用与使用
什么是服务调用呢?我们知道,分布式系统间的通讯是由网络协议进行相互通讯的,在通过服务发现组件获取到生产方的url和访问路径后,我们就已经可以使用RestTemplate进行通讯了,通过组装url、路径和参数,我们已经可以实现服务之间的调用了,那么为什么还要使用其他的服务调用组件呢?
这里就要引入一个曾经在spring中经常接触的概念——声明式调用。就如同spring中的声明式事务一样,只需要对需要调用的服务进行声明即可,下面将会进行详细说明:
如同其他spring组件一样,第一步是添加依赖:
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
第二步则是配置了,OpenFeign需要配置一个client来对应调用端的接口,从而实现调用,这里我们以调用服务为user为例:
package com.woniuxy.cloud.order.clients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient("user")
public interface UserClient {
@GetMapping("getBalance")
public double getBalance(@RequestParam("userId")int userId);
}
在以上代码中,我们创建了一个UserClient接口,并且在该接口上配置上了关键注解@FeignClient,其value值我们指明了调用服务为user,而接口中的抽象方法则是直接使用了生产者的接口方法直接用作抽象方法,通过SpringMVC的注解来标记这些方法的请求地址使得该接口的难度也大大降低。
第三步是在启动类上加上一个@EnableFeignClients的注解,通过该注解以Enable开头就能知道,该注解即为OpenFeign在spring-boot中进行自动配置的关键。
最后,我们只需要通过@Resource进行注入即可使用。
那么,OpenFeign是怎样实现的呢?
原理浅析——动态代理
在使用Client接口时我们可以发现,该接口中我们只定义了方法以及一些注解,而实现类并不是我们进行编写的,从这一点,我们很容易联想到一个类似的技术,没错,就是在使用Mybatis之后的mapper层,它也是通过配置mapper来自动生成的实现类。那么,它是怎么实现的也呼之欲出了,与Mybatis一样,它也用到了JDK动态代理。
当看到JDK动态代理的时候,整个组件的大概原理逻辑也就逐渐清晰了起来,通过反射获取到client上@FeignClient注解上所配置的服务名,调用服务发现(DiscoverClient)获取到该服务所在的地址,然后继续通过反射读取client接口中的方法,方法上的各个注解、参数,由此可以得到请求服务的uri、路径以及参数,那么,到此就算是完成了粗略的分析。
原理的底层
实际上,OpenFeign所做的事远远没有那么简单,OpenFeign不仅集成了服务发现的调用,还实现了当某个服务为一个集群时的分配任务,即负载均衡(ribbon),由于该文章主要叙述OpenFeign,所以此处暂不做叙述。
除去负载均衡不说,在服务调用的时候也并没有那么简单。
通过查看OpenFeign的文档我们可以发现,在动态代理之后,它还做了很多的工作,如下图所示:
该图来自网络,侵删
Contract——协议规范
由上图我们可以看到,在动态代理实现接口后,引入了一个Contract,通过单词的意思,这里我们把它叫做协议规范。
以官方提供的Feign示例来看,我们会发现在示例中并没有SpringMVC的注解,而是一些自定义的注解。
/** * 定义声明式接口 */ interface GitHub { /** * 声明调用方式 GET 和 调用地址,获取指定仓库贡献者列表 */ @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repository); class Contributor { String login; int contributions; } } public static void main(String[] args) { //Feign客户端初始化 GitHub github = Feign.builder() .decoder(new GsonDecoder()) .logLevel(Logger.Level.FULL) .target(GitHub.class, "https://api.github.com"); //获取并打印feign的贡献者列表。 List<GitHub.Contributor> contributors = github.contributors("OpenFeign", "feign"); contributors.forEach(contributor -> System.out.println(contributor.login + " (" + contributor.contributions + ")")); }
由此我们可以看到,在Feign初期,并没有使用PostMapping、GetMapping等SpringMVC的注解来标识请求和路径,其底层的注解为@RequestLine,通过声明调用方式以及配合@Param注解来获取参数来实现对远程服务的调用。
由此我们可以大胆的猜想,在早期的Feign并没有对Contract做太多的适配,在Feign被Spring官方纳入社区之后,才由其官方专门为OpenFeign打造出了适配SpringMvc的协议规范。在查阅资料后,这个猜想被证实的七七八八了。Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成请求协议解析,客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的 API,就可以使用面向接口的编码方式对接服务。
MethodHandler——生成Request
在完成了参数解析之后,Feign引入了一个处理器——MethodHandler。再次通过单词的意思,我们可以知道这个类叫做方法句柄,通过查阅资料我们可以知道,该类的作用也类似于反射中的Method类。
java7在JSR 292中增加了对动态类型语言的支持,使java也可以像C语言那样将方法作为参数传递,其实现在lava.lang.invoke包中。MethodHandle作用类似于反射中的Method类,但它比Method类要更加灵活和轻量级。通过MethodHandle进行方法调用一般需要以下几步:
(1)创建MethodType对象,指定方法的签名;
(2)在MethodHandles.Lookup中查找类型为MethodType的MethodHandle;
(3)传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。
而在Feign中,该类更是被其运用得淋漓尽致,它以RequestBean为模板,将前面得到的参数动态生成出Request对象。
Encoder——最终的HTTP请求
在得到Request对象之后,Feign会将得到的Request转换成Http请求发送出去,而图中intercepter以及logger等后续的步骤则是在请求转换中用于对用户自定义的操作和日志的操作了,这里不做过多的描述。
client——重试器与http的发送
在流程图最后,我们可以看到一个发送报文的Client,其实该处应分为两个模块,重试器与HTTP请求发送。
重试器从名字我们可以猜到,就是Feign在察觉到请求出现异常的时候重复发送请求的处理器,事实也是如此。在HTTP请求出现IO异常的时候,Feign会有一个最大尝试次数发送请求。
Http请求的发送则是最后一步了,Feign真正发送HTTP请求是委托给feign.Client来做的,Feign默认通过java.net.HttpURLConnection实现了feign.Client接口类,而该接口也可以进行扩展,如使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端都可以在默认请求机制上进行升级。
在此,引用一下别人的总结:
- 首先通过@EnableFeignCleints注解开启FeignCleint
- 根据Feign的规则实现接口,并加@FeignCleint注解
- 程序启动后,会进行包扫描,扫描所有的@ FeignCleint的注解的类,并将这些信息注入到ioc容器中。
- 当接口的方法被调用,通过jdk的代理,来生成具体的RequesTemplate
- RequesTemplate在生成Request
- Request交给Client去处理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp
- 最后Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡。
由于博主也是初学者,在对OpenFeign的原理概述的时候如果有地方理解偏了,欢迎指正。而在查阅这些资料的时候,也意外的发现了一篇论述OpenFeign在SpringBoot中的工作原理解析,后续也可以继续深入研究一下。
Spring Cloud OpenFeign 工作原理解析https://blog.csdn.net/lengxiao1993/article/details/103511695/