SpringCloud升级之路2020.0.x版-26.OpenFeign的组件
本系列代码地址: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.Default 和 feign.codec.Decoder.Default
|
gson | feign-gson |
feign.gson.GsonEncoder 和 feign.gson.GsonDecoder
|
xml | feign-jaxb |
feign.jaxb.JAXBEncoder 和 feign.jaxb.JAXBDecoder
|
json (jackson) | feign-jackson |
feign.jackson.JacksonEncoder 和 feign.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 的时候,才会调用配置的 ErrorDecoder
的 decode
方法。默认的 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的组件相关推荐
- SpringCloud升级之路2020.0.x版-43.为何 SpringCloudGateway 中会有链路信息丢失
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Sp ...
- SpringCloud升级之路2020.0.x版-13.UnderTow 核心配置
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford Undertow ...
- SpringCloud升级之路2020.0.x版-12.UnderTow 简介与内部原理
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 在我们的项目中,我 ...
- SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(3)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续分析上一节提到的 WebHandler.加入 Spring Cloud Sleut ...
- 2021-08-05SpringCloud升级之路2020.0.x版-5.所有项目的parent与spring-framework-common说明
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 源代码文件:htt ...
- Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖
1. 背景知识.需求描述与公共依赖 1.1. 背景知识 & 需求描述 Spring Cloud 官方文档说了,它是一个完整的微服务体系,用户可以通过使用 Spring Cloud 快速搭建一个 ...
- note8 升级android9,三星note8 N9500一键ADB升级One UI 9.0内测版
三星note8 N9500一键ADB升级One UI 9.0内测版是一款三星N9500 ADB一键升级9.0底包和刷机教程,三星 note8 安卓Pie,One UI .自己动手丰衣足食,抢不到官方资 ...
- 三星android安卓版本怎么升级,三星更改安卓9.0升级计划三款机型将可以升级到安卓9.0正式版...
描述 近日有网友发现三星中国更新了安卓9.0系统升级公告,和本月初那个公告相比,最明显的变化是国行Galaxy S9.S9+以及Galaxy Note9的升级时间提前到2019年1月,而原本计划是今年 ...
- 华为 android 5.0系统下载地址,华为emui5.0升级公告-emui 5.0官方版下载v5.0 官方最新版-西西软件下载...
emui5.0是关于华为最新的开发的一个手机的系统,对比其他的安卓系统来说,emui5.0的使用的界面可以说是十分的简洁,而且使用起来的体验也是十分的流畅,让用户能够享受到一个很不错的操作系统的体验, ...
最新文章
- 图解一次手动杀马过程
- python filter函数 字符串_Python数组条件过滤filter函数使用示例
- java 克隆的作用_关于java中克隆的学习(一)
- 基于tiny4412的Linux内核移植 -- MMA7660驱动移植(九)
- 如何单元测试Java的private方法
- 怎样设置 vmware 开放一个网络端口,使网络上的电脑能访问这个端口
- 比尔·盖茨推荐2020年度五本好书 你想读哪本?
- php多维数组key交换,php 根据key计算多维数组的和功能实例
- Java SE 6 中实现 Cookie 功能
- Mysql调试存储过程最简单的方法
- vue-admin-better前端页面-菜单-权限配置
- 如何升级composer
- 安卓测试二(Espresso)
- Premiere Pro CS4\CS5\CS6\CC2015\CC2017\CC2018\CC2019软件安装教程
- 个人防骗大全精选(1)
- 回顾过去展望未来之2015
- oracle 根据sid psid,如何获得所有windows用户的SID
- matlab 读取脉冲数,已知一段波形,求脉冲个数,用代码实现
- 设计师必备特效生成器合集 2022背景快速制作指南
- python求平衡点的几种方法
热门文章
- 麒麟软件商店使用错误码提示及应对方案
- 2分钟带您了解熟悉冲压模具
- 【Python强化】pandas处理excel数据
- ArcMap中对道路图层的标注
- 异构计算机 桌面,一种解决异构操作系统的复合桌面虚拟化架构及方法_2
- 面试考代码,居然翻车了!
- java GUI开发中关于卡片式布局详细步骤讲解
- xadmin中写ajax,关于xadmin后台下拉框修改为ajax模糊搜索问题
- Final Cut Pro X Logic Pro X: 1 Audio Post Workflow Final Cut Pro X和Logic Pro X:1音频后期工作流程 Lynda课程中
- SpringBoot中Velocity动态模版引擎