本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent

首先,我们给出官方文档中的组件结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jrW9fLUy-1633609229588)(http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/OpenFeign/feign/master/src/docs/overview-mindmap.iuml)]

官方文档中的组件,是以实现功能为维度的,我们这里是以源码实现为维度的(因为之后我们使用的时候,需要根据需要定制这些组件,所以需要从源码角度去拆分分析),可能会有一些小差异。

负责解析类元数据的 Contract

OpenFeign 是通过代理类元数据来自动生成 HTTP API 的,那么到底解析哪些类元数据,哪些类元数据是有效的,是通过指定 Contract 来实现的,我们可以通过实现这个 Contract 来自定义一些类元数据的解析,例如,我们自定义一个注解:

//仅可用于方法上
@java.lang.annotation.Target(METHOD)
//指定注解保持到运行时
@Retention(RUNTIME)
@interface Get {//请求 uriString uri();
}

这个注解很简单,标注了这个注解的方法会被自动封装成 GET 请求,请求 uri 为 uri() 的返回。

然后,我们自定义一个 Contract 来处理这个注解。由于 MethodMetadata 是 final 并且是 package private 的,所以我们只能继承 Contract.BaseContract 去自定义注解解析:

//外部自定义必须继承 BaseContract,因为里面生成的 MethodMetadata 的构造器是 package private 的
static class CustomizedContract extends Contract.BaseContract {@Overrideprotected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {//处理类上面的注解,这里没用到}@Overrideprotected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) {//处理方法上面的注解Get get = method.getAnnotation(Get.class);//如果 Get 注解存在,则指定方法 HTTP 请求方式为 GET,同时 uri 指定为注解 uri() 的返回if (get != null) {data.template().method(Request.HttpMethod.GET);data.template().uri(get.uri());}}@Overrideprotected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {//处理参数上面的注解,这里没用到return false;}
}

然后,我们来使用这个 Contract:

interface HttpBin {@Get(uri = "/get")String get();
}public static void main(String[] args) {HttpBin httpBin = Feign.builder().contract(new CustomizedContract()).target(HttpBin.class, "http://www.httpbin.org");//实际上就是调用 http://www.httpbin.org/getString s = httpBin.get();
}

一般的,我们不会使用这个 Contract,因为我们业务上一般不会自定义注解。这是底层框架需要用的功能。比如在 spring-mvc 环境下,我们需要兼容 spring-mvc 的注解,这个实现类就是 SpringMvcContract

编码器 Encoder 与解码器 Decoder

编码器与解码器接口定义:

public interface Decoder {Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
}
public interface Encoder {void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
}

OpenFeign 可以自定义编码解码器,我们这里使用 FastJson 自定义实现一组编码与解码器,来了解其中使用的原理。

/*** 基于 FastJson 的反序列化解码器*/
static class FastJsonDecoder implements Decoder {@Overridepublic Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {//读取 bodybyte[] body = response.body().asInputStream().readAllBytes();return JSON.parseObject(body, type);}
}/*** 基于 FastJson 的序列化编码器*/
static class FastJsonEncoder implements Encoder {@Overridepublic void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {if (object != null) {//编码 bodytemplate.header(CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());template.body(JSON.toJSONBytes(object), StandardCharsets.UTF_8);}}
}

然后,我们通过 http://httpbin.org/anything 来测试,这个链接会返回我们发送的请求的一切元素。

interface HttpBin {@RequestLine("POST /anything")Object postBody(Map<String, String> body);
}public static void main(String[] args) {HttpBin httpBin = Feign.builder().decoder(new FastJsonDecoder()).encoder(new FastJsonEncoder()).target(HttpBin.class, "http://www.httpbin.org");Object o = httpBin.postBody(Map.of("key", "value"));
}

查看响应,可以看到我们发送的 json body 被正确的接收到了。

目前,OpenFeign 项目中的编码器以及解码器主要实现包括:

序列化 需要额外添加的依赖 实现类
直接转换成字符串,默认的编码解码器 feign.codec.Encoder.Defaultfeign.codec.Decoder.Default
gson feign-gson feign.gson.GsonEncoderfeign.gson.GsonDecoder
xml feign-jaxb feign.jaxb.JAXBEncoderfeign.jaxb.JAXBDecoder
json (jackson) feign-jackson feign.jackson.JacksonEncoderfeign.jackson.JacksonDecoder

我们在 Spring Cloud 环境中使用的时候,在 Spring MVC 中是有统一的编码器以及解码器的,即 HttpMessageConverters,并且通过胶水项目做了兼容,所以我们统一用 HttpMessageConverters 指定自定义编码解码器就好。

请求拦截器 RequestInterceptor

RequestInterceptor 的接口定义:

public interface RequestInterceptor {void apply(RequestTemplate template);
}

可以从接口看出,RequestInterceptor 其实就是对于 RequestTemplate 进行额外的操作。对于每次请求,都会经过所有的 RequestInterceptor 处理。

举个例子,我们可以对于每个请求加上特定的 Header:

interface HttpBin {//发到这个链接的所有请求,响应会返回请求中的所有元素@RequestLine("GET /anything")String anything();
}static class AddHeaderRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {//添加 headertemplate.header("test-header", "test-value");}
}public static void main(String[] args) {HttpBin httpBin = Feign.builder().requestInterceptor(new AddHeaderRequestInterceptor()).target(HttpBin.class, "http://www.httpbin.org");String s = httpBin.anything();
}

执行程序,可以在响应中看到我们发送请求中添加的 header。

Http 请求客户端 Client

OpenFeign 底层的 Http 请求客户端是可以自定义的,OpenFeign 针对不同的 Http 客户端都有封装,默认的是通过 Java 内置的 Http 请求 API。我们来看下 Client 的接口定义源码:

public interface Client {/*** 执行请求* @param request HTTP 请求* @param options 配置选项* @return* @throws IOException*/Response execute(Request request, Options options) throws IOException;
}

Request 是 feign 中对于 Http 请求的定义,Client 的实现需要将 Request 转换成对应底层的 Http 客户端的请求并调用合适的方法进行请求。Options 是一些请求通用配置,包括:

public static class Options {//tcp 建立连接超时private final long connectTimeout;//tcp 建立连接超时时间单位private final TimeUnit connectTimeoutUnit;//请求读取响应超时private final long readTimeout;//请求读取响应超时时间单位private final TimeUnit readTimeoutUnit;//是否跟随重定向private final boolean followRedirects;
}

目前,Client 的实现包括以下这些:

底层 HTTP 客户端 需要添加的依赖 实现类
Java HttpURLConnection feign.Client.Default
Java 11 HttpClient feign-java11 feign.http2client.Http2Client
Apache HttpClient feign-httpclient feign.httpclient.ApacheHttpClient
Apache HttpClient 5 feign-hc5 feign.hc5.ApacheHttp5Client
Google HTTP Client feign-googlehttpclient feign.googlehttpclient.GoogleHttpClient
Google HTTP Client feign-googlehttpclient feign.googlehttpclient.GoogleHttpClient
jaxRS feign-jaxrs2 feign.jaxrs2.JAXRSClient
OkHttp feign-okhttp feign.okhttp.OkHttpClient
Ribbon feign-ribbon feign.ribbon.RibbonClient

错误解码器相关

可以指定错误解码器 ErrorDecoder,同时还可以指定异常抛出策略 ExceptionPropagationPolicy.

ErrorDecoder 是读取 HTTP 响应判断是否有错误需要抛出异常使用的:

public interface ErrorDecoder {public Exception decode(String methodKey, Response response);
}

只有响应码不为 2xx 的时候,才会调用配置的 ErrorDecoderdecode 方法。默认的 ErrorDecoder 的实现是:

public static class Default implements ErrorDecoder {@Overridepublic Exception decode(String methodKey, Response response) {//将不同响应码包装成不同的异常FeignException exception = errorStatus(methodKey, response);//提取 Retry-After 这个 HTTP 响应头,如果存在这个响应头则将异常封装为 RetryableException//对于 RetryableException,在后面的分析我们会知道如果抛出这个异常会触发重试器的重试Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));if (retryAfter != null) {return new RetryableException(response.status(),exception.getMessage(),response.request().httpMethod(),exception,retryAfter,response.request());}return exception;}}

可以看出, ErrorDecoder 是可能给异常封装一层异常的,这有时候对于我们在外层捕捉会造成影响,所以可以通过指定 ExceptionPropagationPolicy 来拆开这层封装。ExceptionPropagationPolicy 是一个枚举类:

public enum ExceptionPropagationPolicy {//什么都不做NONE, //是否将 RetryableException 的原始 exception 提取出来作为异常抛出//目前只针对 RetryableException 生效,调用 exception 的 getCause,如果不为空就返回这个 cause,否则返回原始 exceptionUNWRAP,;
}

接下来看个例子:

interface TestHttpBin {//请求一定会返回 500@RequestLine("GET /status/500")Object get();
}static class TestErrorDecoder implements ErrorDecoder {@Overridepublic Exception decode(String methodKey, Response response) {//获取错误码对应的 FeignExceptionFeignException exception = errorStatus(methodKey, response);//封装为 RetryableExceptionreturn new RetryableException(response.status(),exception.getMessage(),response.request().httpMethod(),exception,new Date(),response.request());}
}public static void main(String[] args) {TestHttpBin httpBin = Feign.builder().errorDecoder(new TestErrorDecoder())//如果这里没有指定为 UNWRAP 那么下面抛出的异常就是 RetryableException,否则就是 RetryableException 的 cause 也就是 FeignException.exceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP).target(TestHttpBin.class, "http://httpbin.org");httpBin.get();
}

执行后可以发现抛出了 feign.FeignException$InternalServerError: [500 INTERNAL SERVER ERROR] during [GET] to [http://httpbin.org/status/500] [TestHttpBin#get()]: [] 这个异常。

针对 RetryableException 的重试器 Retryer

在调用发生异常的时候,我们可能希望按照一定策略进行重试,抽象这种重试策略一般包括:

  • 对于哪些异常会重试
  • 什么时候重试,什么时候结束重试,例如重试 n 次以后

对于那些异常会重试,这个由 ErrorDecoder 决定。如果异常需要被重试,就把它封装成 RetryableException,这样 Feign 就会使用 Retryer 进行重试。对于什么时候重试,什么时候结束重试,这些就是 Retryer 需要考虑的事情:

public interface Retryer extends Cloneable {/*** 判断继续重试,或者抛出异常结束重试*/void continueOrPropagate(RetryableException e);/*** 对于每次请求,都会调用这个方法创建一个新的同样配置的 Retryer 对象*/Retryer clone();
}

我们来看一下 Retryer 的默认实现:

class Default implements Retryer {//最大重试次数private final int maxAttempts;//初始重试间隔private final long period;//最大重试间隔private final long maxPeriod;//当前重试次数int attempt;//当前已经等待的重试间隔时间和long sleptForMillis;public Default() {//默认配置,初始重试间隔为 100ms,最大重试间隔为 1s,最大重试次数为 5this(100, SECONDS.toMillis(1), 5);}public Default(long period, long maxPeriod, int maxAttempts) {this.period = period;this.maxPeriod = maxPeriod;this.maxAttempts = maxAttempts;//当前重试次数从 1 开始,因为第一次进入 continueOrPropagate 之前就已经发生调用但是失败了并抛出了 RetryableExceptionthis.attempt = 1;}// visible for testing;protected long currentTimeMillis() {return System.currentTimeMillis();}public void continueOrPropagate(RetryableException e) {//如果当前重试次数大于最大重试次数则if (attempt++ >= maxAttempts) {throw e;}long interval;//如果指定了 retry-after,则以这个 header 为准决定等待时间if (e.retryAfter() != null) {interval = e.retryAfter().getTime() - currentTimeMillis();if (interval > maxPeriod) {interval = maxPeriod;}if (interval < 0) {return;}} else {//否则,通过 nextMaxInterval 计算interval = nextMaxInterval();}try {Thread.sleep(interval);} catch (InterruptedException ignored) {Thread.currentThread().interrupt();throw e;}//记录一共等待的时间sleptForMillis += interval;}//每次重试间隔增长 50%,直到最大重试间隔long nextMaxInterval() {long interval = (long) (period * Math.pow(1.5, attempt - 1));return interval > maxPeriod ? maxPeriod : interval;}@Overridepublic Retryer clone() {//复制配置return new Default(period, maxPeriod, maxAttempts);}
}

默认的 Retryer 功能也比较丰富,用户可以参考这个实现更适合自己业务场景的重试器。

每个 HTTP 请求的配置 Options

无论是哪种 HTTP 客户端,都需要如下几个配置:

  • 连接超时:这个是 TCP 连接建立超时时间
  • 读取超时:这个是收到 HTTP 响应之前的超时时间
  • 是否跟随重定向
    OpenFeign 可以通过 Options 进行配置:
public static class Options {private final long connectTimeout;private final TimeUnit connectTimeoutUnit;private final long readTimeout;private final TimeUnit readTimeoutUnit;private final boolean followRedirects;
}

例如我们可以这么配置一个连接超时为 500ms,读取超时为 6s,跟随重定向的 Feign:

Feign.builder().options(new Request.Options(500, TimeUnit.MILLISECONDS, 6, TimeUnit.SECONDS, true
))

我们这一节详细介绍了 OpenFeign 的各个组件,有了这些知识,其实我们自己就能实现 Spring-Cloud-OpenFeign 里面的胶水代码。其实 Spring-Cloud-OpenFeign 就是将这些组件以 Bean 的形式注册到 NamedContextFactory 中,供不同微服务进行不同的配置。

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

SpringCloud升级之路2020.0.x版-26.OpenFeign的组件相关推荐

  1. SpringCloud升级之路2020.0.x版-43.为何 SpringCloudGateway 中会有链路信息丢失

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Sp ...

  2. SpringCloud升级之路2020.0.x版-13.UnderTow 核心配置

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford Undertow ...

  3. SpringCloud升级之路2020.0.x版-12.UnderTow 简介与内部原理

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 在我们的项目中,我 ...

  4. SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(3)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续分析上一节提到的 WebHandler.加入 Spring Cloud Sleut ...

  5. 2021-08-05SpringCloud升级之路2020.0.x版-5.所有项目的parent与spring-framework-common说明

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 源代码文件:htt ...

  6. Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖

    1. 背景知识.需求描述与公共依赖 1.1. 背景知识 & 需求描述 Spring Cloud 官方文档说了,它是一个完整的微服务体系,用户可以通过使用 Spring Cloud 快速搭建一个 ...

  7. note8 升级android9,三星note8 N9500一键ADB升级One UI 9.0内测版

    三星note8 N9500一键ADB升级One UI 9.0内测版是一款三星N9500 ADB一键升级9.0底包和刷机教程,三星 note8 安卓Pie,One UI .自己动手丰衣足食,抢不到官方资 ...

  8. 三星android安卓版本怎么升级,三星更改安卓9.0升级计划三款机型将可以升级到安卓9.0正式版...

    描述 近日有网友发现三星中国更新了安卓9.0系统升级公告,和本月初那个公告相比,最明显的变化是国行Galaxy S9.S9+以及Galaxy Note9的升级时间提前到2019年1月,而原本计划是今年 ...

  9. 华为 android 5.0系统下载地址,华为emui5.0升级公告-emui 5.0官方版下载v5.0 官方最新版-西西软件下载...

    emui5.0是关于华为最新的开发的一个手机的系统,对比其他的安卓系统来说,emui5.0的使用的界面可以说是十分的简洁,而且使用起来的体验也是十分的流畅,让用户能够享受到一个很不错的操作系统的体验, ...

最新文章

  1. 图解一次手动杀马过程
  2. python filter函数 字符串_Python数组条件过滤filter函数使用示例
  3. java 克隆的作用_关于java中克隆的学习(一)
  4. 基于tiny4412的Linux内核移植 -- MMA7660驱动移植(九)
  5. 如何单元测试Java的private方法
  6. 怎样设置 vmware 开放一个网络端口,使网络上的电脑能访问这个端口
  7. 比尔·盖茨推荐2020年度五本好书 你想读哪本?
  8. php多维数组key交换,php 根据key计算多维数组的和功能实例
  9. Java SE 6 中实现 Cookie 功能
  10. Mysql调试存储过程最简单的方法
  11. vue-admin-better前端页面-菜单-权限配置
  12. 如何升级composer
  13. 安卓测试二(Espresso)
  14. Premiere Pro CS4\CS5\CS6\CC2015\CC2017\CC2018\CC2019软件安装教程
  15. 个人防骗大全精选(1)
  16. 回顾过去展望未来之2015
  17. oracle 根据sid psid,如何获得所有windows用户的SID
  18. matlab 读取脉冲数,已知一段波形,求脉冲个数,用代码实现
  19. 设计师必备特效生成器合集 2022背景快速制作指南
  20. python求平衡点的几种方法

热门文章

  1. 麒麟软件商店使用错误码提示及应对方案
  2. 2分钟带您了解熟悉冲压模具
  3. 【Python强化】pandas处理excel数据
  4. ArcMap中对道路图层的标注
  5. 异构计算机 桌面,一种解决异构操作系统的复合桌面虚拟化架构及方法_2
  6. 面试考代码,居然翻车了!
  7. java GUI开发中关于卡片式布局详细步骤讲解
  8. xadmin中写ajax,关于xadmin后台下拉框修改为ajax模糊搜索问题
  9. Final Cut Pro X Logic Pro X: 1 Audio Post Workflow Final Cut Pro X和Logic Pro X:1音频后期工作流程 Lynda课程中
  10. SpringBoot中Velocity动态模版引擎