详解Feign的实现原理
目录
一、什么是Feign
二、为什么用Feign
三、实例
3.1、原生使用方式
3.2、结合 Spring Cloud 使用方式
四、探索Feign
五、总结
一、什么是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.
二、为什么用Feign
Feign 的首要目标就是减少 HTTP 调用的复杂性。在微服务调用的场景中,我们调用很多时候都是基于 HTTP 协议的服务,如果服务调用只使用提供 HTTP 调用服务的 HTTP Client 框架(e.g. Apache HttpComponnets、HttpURLConnection OkHttp 等),我们需要关注哪些问题呢?
相比这些 HTTP 请求框架,Feign 封装了 HTTP 请求调用的流程,而且会强制使用者去养成面向接口编程的习惯(因为 Feign 本身就是要面向接口)。
三、实例
3.1、原生使用方式
以获取 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)等。
通过定义接口,使用注解的方式描述接口的信息,就可以发起接口调用。最后请求结果如下:
3.2、结合 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 请求了,使用是不是很方便、简洁。
四、探索Feign
从上面第一个原生使用的例子可以看到,只是定了接口并没有具体的实现类,但是却可以在测试类中直接调用接口的方法来完成接口的调用,我们知道在 Java 里面接口是无法直接进行使用的,因此可以大胆猜测是 Feign 在背后默默生成了接口的实现类,也可以验证一下,只需在刚刚的测试类 debug 一下看看接口实际使用的是什么实现类:
从 debug 结果可知,框架生成了接口的实现类 HardCodedTarget 的对象 $Proxy14 来完成接口请求调用,和刚刚的猜测一致。Feign 主要是封装了 HTTP 请求调用,其整体架构如下:
测试类代码里面只在 GitHub github = Feign.builder().target(GitHub.class, " 用到了 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 完成请求,对响应进行解码以及异常处理操作,整体流程如下:
五、总结
Feign 通过给我们定义的目标接口(比如例子中的 GitHub)生成一个 HardCodedTarget 类型的对象,由 JDK 动态实现,生成的时候会根据注解来生成一个对应的 Map<Method, MethodHandler>,这个 Map 被 InvocationHandler 持有,接口方法调用的时候,进入 InvocationHandler 的 invoke 方法(为什么会进入这里?JDK 动态的基础知识)。
然后根据调用的方法从 Map<Method, MethodHandler> 获取对应的 MethodHandler,然后通过 MethodHandler 根据指定的 client 来完成对应处理, MethodHandler 中的实现类 DefaultMethodHandler 处理默认方法(接口的默认方法)的请求处理的,SynchronousMethodHandler 实现类是完成其它方法的 HTTP 请求的实现,这就是 Feign 的主要核心流程。以上是 Feign 框架实现的核心流程介绍。
以上就是详解Feign的实现原理的详细内容,更多关于Feign原理的资料请关注无名其它相关文章!
同类资源
- EHTTP易语言网站开发库内测版
EHTTP易语言网站开发库内测版例子源代码,是由c++专门为易语言开发的网站开发库。...
- 联众答题HTTP模块2.0开源
联众答题HTTP模块2.0开源例子源代码,应该是1.0版本的不怎么好用。...
- EHTTP易语言网站开发框架
EHTTP易语言网站开发框架例子源代码,eHttp是由C++编写的网站开发框架。...
- 基于HPSocket一键搭建HTTP服务,支持POST、GET
易语言基于HPSocket一键搭建HTTP服务,支持POST、GET例子源代码,调用的模块源码也给出。...
- TCP发送HTTP请求
易语言TCP发送HTTP请求例子源代码,其实很简单,自己构造HTTP头就可以了,之前没找到现在分享出来。...
- http多线程注册测试模板
易语言http多线程注册测试模板例子源代码,试着用易写了个多线程注册模板,总体感觉,挺方便。...
- Http多线程下载文件源码
易语言Http多线程下载文件例子源代码,根据网上开源的代码修改。...
- WinhttpApi类Zlibwapi封装模块
WinhttpApi类Zlibwapi封装模块例子源代码,本模块将Winhttp的常用Api封装为了一个HTTP请求类,不再是之前开源...
- 一行代码调用鱼刺http
一行代码调用鱼刺http例子源代码,给一个老弟写的,他想用鱼刺,但是自己又搞不懂,于是就诞生了这么个东西。...
- 支持https/自动更新/多文件更新/update/md5比对/自动创建文件夹
易语言支持https/自动更新/多文件更新/update/md5比对/自动创建文件夹例子源代码,在测试启动器中也加入了...
- http下载文件可同时下载多个
易语言http下载文件可同时下载多个例子源代码,经过修改使用线程的方式可以让其下载多个文件。...
- HttpHelper苏飞万能框架V2.2
HttpHelper苏飞万能框架V2.2本文件感兴趣的可以参考一下,最新苏飞HttpHelper万能框架V2.2有源码。...