What is Feign?

Feign 是⼀个 HTTP 请求的轻量级客户端框架。通过 接口 + 注解的方式发起 HTTP 请求调用,面向接口编程,而不是像 Java 中通过封装 HTTP 请求报文的方式直接调用。服务消费方拿到服务提供方的接⼝,然后像调⽤本地接⼝⽅法⼀样去调⽤,实际发出的是远程的请求。让我们更加便捷和优雅的去调⽤基于 HTTP 的 API,被⼴泛应⽤在 Spring Cloud 的解决⽅案中。开源项目地址:Feign,官方描述如下:

Feign is a Java to HTTP client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.

Why Feign?

Feign 的首要目标就是减少 HTTP 调用的复杂性。在微服务调用的场景中,我们调用很多时候都是基于 HTTP 协议的服务,如果服务调用只使用提供 HTTP 调用服务的 HTTP Client 框架(e.g. Apache HttpComponnets、HttpURLConnection OkHttp 等),我们需要关注哪些问题呢?

相比这些 HTTP 请求框架,Feign 封装了 HTTP 请求调用的流程,而且会强制使用者去养成面向接口编程的习惯(因为 Feign 本身就是要面向接口)。

Demo

原生使用方式

以获取 Feign 的 GitHub 开源项目的 Contributors 为例,原生方式使用 Feign 步骤有如下三步(这里以使用 Gradle 进行依赖管理的项目为例):

第一步: 引入相关依赖:implementation 'io.github.openfeign:feign-core:11.0'

在项目的 build.gradle 文件的依赖声明处 dependencies 添加该依赖声明即可。

第二步: 声明 HTTP 请求接口

使用 Java 的接口和 Feign 的原生注解 @RequestLine 声明 HTTP 请求接口,从这里就可以看到 Feign 给使用者封装了 HTTP 的调用细节,极大的减少了 HTTP 调用的复杂性,只要定义接口即可。

第三步: 配置初始化 Feign 客户端

最后一步配置初始化客户端,这一步主要是设置请求地址、编码(Encoder)、解码(Decoder)等。

通过定义接口,使用注解的方式描述接口的信息,就可以发起接口调用。最后请求结果如下:

结合 Spring Cloud 使用方式

同样还是以获取 Feign 的 GitHub 开源项目的 Contributors 为例,结合 Spring Cloud 的使用方式有如下三步:

第一步: 引入相关 starter 依赖:org.springframework.cloud:spring-cloud-starter-openfeign

在项目的 build.gradle 文件的依赖声明处 dependencies 添加该依赖声明即可。

第二步: 在项目的启动类 XXXApplication 上添加 @EnableFeignClients 注解启用 Feign 客户端功能。

第三步: 创建 HTTP 调用接口,并添加声明 @FeignClient 注解。

最后一步配置初始化客户端,这一步主要是设置请求地址(url)、编码(Encoder)、解码(Decoder)等,与原生使用方式不同的是,现在我们是通过 @FeignClient 注解配置的 Feign 客户端属性,同时请求的 URL 也是使用的 Spring MVC 提供的注解。

测试类如下所示:

运行结果如下:

可以看到这里是通过 @Autowired 注入刚刚定义的接口的,然后就可以直接使用其来发起 HTTP 请求了,使用是不是很方便、简洁。

Dive into Feign

从上面第一个原生使用的例子可以看到,只是定了接口并没有具体的实现类,但是却可以在测试类中直接调用接口的方法来完成接口的调用,我们知道在 Java 里面接口是无法直接进行使用的,因此可以大胆猜测是 Feign 在背后默默生成了接口的代理实现类,也可以验证一下,只需在刚刚的测试类 debug 一下看看接口实际使用的是什么实现类:

从 debug 结果可知,框架生成了接口的代理实现类 HardCodedTarget 的对象 $Proxy14 来完成接口请求调用,和刚刚的猜测一致。Feign 主要是封装了 HTTP 请求调用,其整体架构如下:

测试类代码里面只在 GitHub github = Feign.builder().target(GitHub.class, "https://api.github.com"); 用到了 Feign 框架的功能,所以我们选择从这里来深入源码,点击进入发现是 Feign 抽象类提供的方法,同样我们知道抽象类也是无法进行初始化的,所以肯定是有子类的,如果你刚刚有仔细观察上面的 debug 代码的话,可以发现有一个 ReflectiveFeign 类,这个类就是抽象类 Feign 的子类了。抽象类 feign.Feign 的部分源码如下:

public abstract class Feign {

  ...  

  public static Builder builder() {
return new Builder();
} public abstract <T> T newInstance(Target<T> target); public static class Builder { ... private final List<RequestInterceptor> requestInterceptors = new ArrayList<RequestInterceptor>();
private Logger.Level logLevel = Logger.Level.NONE;
private Contract contract = new Contract.Default();
private Client client = new Client.Default(null, null);
private Retryer retryer = new Retryer.Default();
private Logger logger = new NoOpLogger();
private Encoder encoder = new Encoder.Default();
private Decoder decoder = new Decoder.Default();
private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private Options options = new Options();
private InvocationHandlerFactory invocationHandlerFactory =
new InvocationHandlerFactory.Default();
private boolean decode404;
private boolean closeAfterDecode = true;
private ExceptionPropagationPolicy propagationPolicy = NONE;
private boolean forceDecoding = false;
private List<Capability> capabilities = new ArrayList<>(); // 设置输入打印日志级别
public Builder logLevel(Logger.Level logLevel) {
this.logLevel = logLevel;
return this;
} // 设置接口方法注解处理器(契约)
public Builder contract(Contract contract) {
this.contract = contract;
return this;
} // 设置使用的 Client(默认使用 JDK 的 HttpURLConnection)
public Builder client(Client client) {
this.client = client;
return this;
} // 设置重试器
public Builder retryer(Retryer retryer) {
this.retryer = retryer;
return this;
} // 设置请求编码器
public Builder encoder(Encoder encoder) {
this.encoder = encoder;
return this;
} // 设置响应解码器
public Builder decoder(Decoder decoder) {
this.decoder = decoder;
return this;
} // 设置 404 返回结果解码器
public Builder decode404() {
this.decode404 = true;
return this;
} // 设置错误解码器
public Builder errorDecoder(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
return this;
} // 设置请求拦截器
public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
this.requestInterceptors.clear();
for (RequestInterceptor requestInterceptor : requestInterceptors) {
this.requestInterceptors.add(requestInterceptor);
}
return this;
} public <T> T target(Class<T> apiType, String url) {
return target(new HardCodedTarget<T>(apiType, url));
} public <T> T target(Target<T> target) {
return build().newInstance(target);
} } ... }

可以看到在方法 public T target(Class apiType, String url) 中直接创建了 HardCodedTarget 对象出来,这个对象也是上面 debug 看到的对象。再继续深入,就来到了 feign.Feign 的 newInstance(Target target) 的方法了,是个抽象方法,其实现在子类 ReflectiveFeign 中,这个方法就是接口代理实现生成的地方,下面通过源码来看看实现逻辑是怎样的:

public class ReflectiveFeign extends Feign {

  ...  

  private final ParseHandlersByName targetToHandlersByName;
private final InvocationHandlerFactory factory;
private final QueryMapEncoder queryMapEncoder; ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory,
QueryMapEncoder queryMapEncoder) {
this.targetToHandlersByName = targetToHandlersByName;
this.factory = factory;
this.queryMapEncoder = queryMapEncoder;
} @SuppressWarnings("unchecked")
@Override
public <T> T newInstance(Target<T> target) {
// <类名#方法签名, MethodHandler>,key 是通过 feign.Feign.configKey(Class targetType, Method method) 生成的
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
// 将 Map<String, MethodHandler> 转换为 Map<Method, MethodHandler> 方便调用
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
// 默认方法处理器
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) {
// 跳过 Object 类定于的方法
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
// 默认方法(接口声明的默认方法)使用默认的方法处理器
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
// 接口正常声明的方法(e.g. GitHub.listContributors(String, String))
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
} // 生成 Feign 封装的 InvocationHandler
InvocationHandler handler = factory.create(target, methodToHandler);
// 基于 JDK 动态代理生成接口的代理类(e.g. Github 接口)
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
} ... }

总体流程就是在方法 T newInstance(Target target) 生成一个含有 FeignInvocationHandler 的代理对象,FeignInvocationHandler 对象会持有 Map<Method, MethodHandler> map,代理对象调用的时候进入 FeignInvocationHandler#invoke 方法,根据调用的方法来获取对应 MethodHandler,然后再 MethodHandler 完成对方法的处理(处理 HTTP 请求等)。

下面再深入 MethodHandler,看看是如何完成对方法 HTTP 请求处理的,MethodHandler 是一个接口定义在 feign.InvocationHandlerFactory 接口中(P.S. 基础知识点,接口是可以在内部定义内部接口的哦),有两个实现类分别为 DefaultMethodHandler 和 SynchronousMethodHandler,第一个 DefaultMethodHandler 用来处理接口的默认方法,第二个是用来处理正常的接口方法的,一般情况下都是由该类来处理的。


final class SynchronousMethodHandler implements MethodHandler { ... @Override
public Object invoke(Object[] argv) throws Throwable {
// 获取 RequestTemplate 将请求参数封装成请求模板
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
// 请求重试器
Retryer retryer = this.retryer.clone();
while (true) {
try {
// 执行请求并解码后返回
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
// 发生重试异常则进行重试处理
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
} Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
// 从请求模板 RequestTemplate 构造请求参数对象 Request
Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
} Response response;
long start = System.nanoTime();
try {
// 通过 client(Apache HttpComponnets、HttpURLConnection OkHttp 等)执行 HTTP 请求调用,默认是 HttpURLConnection
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 12
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); if (decoder != null)
// 对返回结果进行解码操作
return decoder.decode(response, metadata.returnType()); CompletableFuture<Object> resultFuture = new CompletableFuture<>();
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
metadata.returnType(),
elapsedTime); try {
if (!resultFuture.isDone())
throw new IllegalStateException("Response handling not done"); return resultFuture.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause != null)
throw cause;
throw e;
}
} ... }

至此,Feign 的核心实现流程介绍完毕,从代码上看 feign.SynchronousMethodHandler 的操作相对比较简单,主要是通过 client 完成请求,对响应进行解码以及异常处理操作,整体流程如下:

Summary

Feign 通过给我们定义的目标接口(比如例子中的 GitHub)生成一个 HardCodedTarget 类型的代理对象,由 JDK 动态代理实现,生成代理的时候会根据注解来生成一个对应的 Map<Method, MethodHandler>,这个 Map 被 InvocationHandler 持有,接口方法调用的时候,进入 InvocationHandler 的 invoke 方法(为什么会进入这里?JDK 动态代理的基础知识)。

然后根据调用的方法从 Map<Method, MethodHandler> 获取对应的 MethodHandler,然后通过 MethodHandler 根据指定的 client 来完成对应处理, MethodHandler 中的实现类 DefaultMethodHandler 处理默认方法(接口的默认方法)的请求处理的,SynchronousMethodHandler 实现类是完成其它方法的 HTTP 请求的实现,这就是 Feign 的主要核心流程,源码已上传 Github。以上是 Feign 框架实现的核心流程介绍,Spring Cloud 是如何整合 Feign 的呢?请看下篇博文,敬请期待。

聊聊 Feign 的实现原理的更多相关文章

  1. 聊聊Vim的工作原理

    聊聊Vim的工作原理 日常里一直在用Vim这个编辑器,前阵子学习关于Linux中的fd(文件描述符)时,发现vim的进程描述符会比上一个自动加一,后续了解到vim的工作原理后,解开了这个疑问,所以记录 ...

  2. 聊聊jstack的工作原理

    实现一个jstack 在聊Jstack得工作原理前呢,不如让我们先写一个简单的jstack玩玩.不用怕,很简单的,就几行代码的事,看: public class MyJstack { public s ...

  3. Feign自动装配原理

    spring.factories 按照以往的惯例,在研究源码的时候,我们先看一下spring.factories文件下自动装配的类FeignAutoConfiguration,其中比较重要的东西有这么 ...

  4. 【学亮编程手记】Spring Cloud三大组件Eureka/Feign/Histrix的原理及使用

  5. 【一起学源码-微服务】Feign 源码三:Feign结合Ribbon实现负载均衡的原理分析

    前言 前情回顾 上一讲我们已经知道了Feign的工作原理其实是在项目启动的时候,通过JDK动态代理为每个FeignClinent生成一个动态代理. 动态代理的数据结构是:ReflectiveFeign ...

  6. Feign Client 原理和使用

    Feign Client 原理和使用 一块石头 ​ 公众号:好奇心森林 ​关注他 创作声明:内容包含虚构创作 6 人赞同了该文章 最近一个新项目在做后端HTTP库技术选型的时候对比了Spring We ...

  7. 菜鸟学SSH(十五)——简单模拟Hibernate实现原理

    之前写了Spring的实现原理,今天我们接着聊聊Hibernate的实现原理,这篇文章只是简单的模拟一下Hibernate的原理,主要是模拟了一下Hibernate的Session类.好了,废话不多说 ...

  8. SpringCloud Feign的分析

    Feign是一个声明式的Web Service客户端,它使得编写Web Serivce客户端变得更加简单.我们只需要使用Feign来创建一个接口并用注解来配置它既可完成. @FeignClient(v ...

  9. springcloud 入门 5 (feign源码分析)

    feign:(推荐使用) Feign是受到Retrofit,JAXRS-2.0和WebSocket的影响,它是一个jav的到http客户端绑定的开源项目. Feign的主要目标是将Java Http ...

随机推荐

  1. 通过CRM系统改变传统工作模式

    在现在这个互联网时代,同行业的竞争越发激烈,因此许多企业都选择使用CRM来提高企业的销售业绩.CRM客户关系管理系统是能够优化企业的销售流程.维护良好的客户关系.对销售流程进行管理的强大工具.但是很多 ...

  2. Linux查看登录日志 last命令 查看当前登录用户

    Linux查看登录日志 linux 发布于 31 分钟前   lastlog 打印系统账号最近一次的登录记录情况,解析的是/var/log/lastlog文件,它是一个data file类型的文件,文 ...

  3. python中类属性和数据属性的解释

    python中的类叫class object,类的实例叫instance object. 类 Class Objects 类拥有两种操作,1.类属性 attribute references 2.实例 ...

  4. Ansible_管理playbook实现配置并行

    一.使用forks在Ansible中配置并行 1.Aniable运行play机制 1️⃣:当Ansible处理playbook时,会按顺序运行每个play.确定play的主机列表之后,Ansible将 ...

  5. inux软件安装管理之——dpkg与apt-*详解

    inux软件安装管理之--dpkg与apt-*详解 Nosee123关注 0.5922017.09.12 17:47:44字数 3,894阅读 8,565 [Linux软件安装管理系列]- - 传送门 ...

  6. C语言编译器开发之旅(开篇)

    编译器写作之旅   最近在Github上看到一个十分有趣的项目acwj(A Compiler Writing Journey),一个用C语言编写编译器的项目.身为一个程序员,这在我看来是一件十分酷的事 ...

  7. 搞定Redis(一)Redis的安装和五大基本数据类型

    一.Redis概述及安装 1.概述: 1.1.Redis是一个开源的key - value存储系统. 1.2.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串). ...

  8. Tensorflow 2.0 搭建神经网络(局部)

    前向传播 tensorflow.keras 搭建网络时,内部的网络可以直接完成所有层的前向计算.全连接Dense() 层,最后一层的神经元的个数需要和最后一层线性函数 w x + b 的维度对应上,中 ...

  9. YOLOv4 资源环境配置和测试样例效果

    YOLOv4 资源环境配置和测试样例效果 基本环境:cuda=10.0,cudnn>=7.0, opencv>=2.4 一.下载yolov4 git clone https://githu ...

  10. 第四代自动泊车从APA到AVP技术

    第四代自动泊车从APA到AVP技术 前言 自动泊车是指汽车自动泊车入位不需要人工控制,系统能够自动帮你将车辆停入车位,在倒车入库中可谓是驾驶者的一项利器.当我们找到一个理想的停车地点,只需轻轻启动按钮 ...