Spring Cloud openFeign 学习【3.0.2 版本】
前言
内容分为 openFeign 大抵的应用和源码的集体解读,外面参考了不少其余优良博客作者的内容,很多中央根本算是鹦鹉学舌了,不过还是顺着源码读了一遍加深了解。
openFeign 是什么?
Feign 是一个 申明性 web 服务客户端。它使编写 web 服务客户机更加容易,要应用 Feign,须要创立一个接口并对其进行正文。它具备可插入正文反对,包含 Feign 正文和 JAX-RS 正文。
Feign 还反对 可插拔编码器和解码器。Spring Cloud 减少了对 Spring MVC 注解的反对,并反对应用 Spring Web 中默认应用的雷同 HttpMessageConverters。
Spring Cloud 集成了 Eureka、Spring Cloud CircuitBreaker 和 Spring Cloud LoadBalancer,在应用 Feign 时提供一个 负载平衡的 http 客户端。
如何学习?
框架最大的意义在于应用,其实最好的教程就是边做边参考官网的文档学习。
官网文档目录地址
官网 openFeign 的文档
利用场景?
能够看到 openFeign 作为服务的调用直达,负责服务之间的连贯和申请转发的操作。OpenFeign 作为编写服务调用反对组件在 spring cloud 中占有极为重要的地位。
和 RPC 的通信框架不同,openFeign 应用了传统的 http 作为传输构造。
在以往应用 Ribbon 的时候,服务调用通常应用的是手动调用,这须要破费大量的人工协调工夫。当初通过 openFeign 把服务调用“本地化”。调用其余的服务的接口 API 像调用本地办法一样。这样既不须要频繁的改变接口,又能够管制服务的调用,而不会导致服务提供方的变动而“生效”。
Ribbon、Feign 和 OpenFeign 的区别
Ribbon、Feign 和 OpenFeign 的区别
Ribbon
Ribbon 是 Netflix 开源的 基于 HTTP 和 TCP等协定负载平衡组件
Ribbon 能够用来做 客户端负载平衡,调用注册核心的服务
Ribbon 的应用须要代码里 手动调用指标服务,请参考官网示例:官网示例
Feign
Feign 是 Spring Cloud 组件中的一个 轻量级 RESTful 的 HTTP 服务 客户端。
Feign 内置了 Ribbon,用来做 客户端负载平衡 ,去调用 服务注册核心的服务。
Feign 的应用形式是:应用 Feign 的注解 定义接口,调用这个接口,就能够调用服务注册核心的服务。
Feign 反对的注解和用法请参考官网文档:官网文档。
Feign 自身不反对 Spring MVC 的注解,它有一套本人的注解。
OpenFeign
OpenFeign 是 Spring Cloud 在 Feign 的根底上反对了 Spring MVC 的注解,如 @RequesMapping 等等。OpenFeign 的 @FeignClient 能够解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动静代理的形式产生实现类,实现类中做负载平衡并调用其余服务。
依据下面的形容,绘制如下的表格内容:
– | Ribbon | Feign | OpenFeign |
---|---|---|---|
应用形式 | 手动调用指标服务 | Feign 的注解定义接口,调用接口就能够调用注册核心服务 | 能够间接应用服务调用的形式调用对应的服务 |
作用 | 客户端负载平衡,服务注册核心的服务调用 | 客户端负载平衡,服务注册核心的服务调用 | 动静代理的形式产生实现类,实现类中做负载平衡并调用其余服务 |
开发商 | Netfix | Spring Cloud | Spring Cloud |
特点 | 基于 HTTP 和 TCP等协定负载平衡组件 | 轻量级 RESTful 的 HTTP 服务 客户端。依附自我实现的注解进行申请解决 | 反对了 Spring MVC 的注解的 轻量级 RESTful 的 HTTP 服务 客户端 |
目前状况 | 保护中 | 进行保护 | 保护中 |
openFeign 减少了那些性能:
- 可插拔的注解反对,包含 Feign 注解和 JSX-RS 注解。
- 反对可插拔的 HTTP 编码器和解码器。
- 反对 Hystrix 和它的 Fallback。
- 反对 Ribbon 的负载平衡。
- 反对 HTTP 申请和响应的压缩。
openFeign 的 client 实现方替换:
- 能够应用 http client 替换,并且 openFeign 提供了良好的配置,能够反对 httpclient 的细节化配置。
- 应用 okHttpClient, 能够实现 okhttpClient 实现自定义的 httpclient 注入模式,然而会呈现肯定的问题。
应用形式:
1. 增加依赖
依照 maven 的依赖治理,咱们须要应用此形式进行解决
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${feign.version}</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
2. 开启注解 @EnableFeignClients
application
启动类 须要增加对应的配置:@EnableFeignClients
用于容许拜访。
spring cloud feign 的默认配置:
Spring Cloud OpenFeign 默认为假装提供以下 bean(
BeanType
beanName:)ClassName
:
Decoder
feignDecoder:(ResponseEntityDecoder
蕴含SpringDecoder
)Encoder
feignEncoder:SpringEncoder
Logger
feignLogger:Slf4jLogger
MicrometerCapability
micrometerCapability:如果feign-micrometer
在类门路上并且MeterRegistry
可用Contract
feignContract:SpringMvcContract
Feign.Builder
feignBuilder:FeignCircuitBreaker.Builder
Client
feignClient:如果在类门路FeignBlockingLoadBalancerClient
上应用 Spring Cloud LoadBalancer,则应用。如果它们都不在类门路上,则应用默认的假装客户端。
3. yml 减少配置:
yml 文件外部的文件内容如下:
feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:
- com.example.FooCapability
- com.example.BarCapability
metrics.enabled: false
4. 具体应用:
更多的用法请依据网上材料或者官网文档,上面列举一些具体的配置或者应用办法:
如果 openFeign 的名称发生冲突,须要应用
contextId
对于避免 bean 的名称抵触@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
上下文继承
如果将 FeignClient 配置为不从父上下文继承 bean,能够应用上面的写法:
@Configuration
public class CustomConfiguration{
@Bean
public FeignClientConfigurer feignClientConfigurer() {return new FeignClientConfigurer() {
@Override
public boolean inheritParentConfiguration() {return false;}
};
}
}
留神:默认状况下 feign 不会对与斜杠进行编码,如果要对斜杠编码,须要应用如下形式:
feign.client.decodeSlash:false
日志输入
feign 的默认日志输入等级如下:
logging.level.project.user.UserClient: DEBUG
上面是日志打印的内容:
NONE
:默认不记录任何日志(默认设置)BASIC
:只记录和申请以及响应工夫相干的日志信息HEADERS
:记录根本信息以及申请和响应 头FULL
: 记录申请和响应的头、主体和元数据。(所有信息记录)
开启压缩
能够通过如下配置,开始 http 压缩:
feign.compression.request.enabled=true
feign.compression.response.enabled=true
如果须要更进一步的配置,能够应用如下的模式进行配置:
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
留神 2048 值为压缩申请的最小阈值,因为如果对于所有申请进行 gzip 压缩,对于小文件的性能开销要反而要更大
通过上面的配置来开启 gzip 压缩(压缩编码为 UTF-8,默认):
feign.compression.response.enabled=true
feign.compression.response.useGzipDecoder=true
5. 附录:
yml 相干配置表:
这部分配置能够间接参考官网的解决:yml 相干配置表
openFeign 的源码解读
上面为借助文章了解和本人看源码的总结。整个调用过程还是比拟好了解的。因为说白了自身就是对于一次 http 申请的形象和封装而已。不过这部分用到了很多的设计模式,比方随处可见的建造者模式和策略模式。同时这一块的设计应用大量的包拜访构造闭包,所以要对其进行二次开发会略微麻烦一些,然而应用反射这些屏障根本算是形同虚设了。
参考资料:掘金【【图文】Spring Cloud OpenFeign 源码解析】:https://juejin.cn/post/684490…
feign 工作流程图
工作流程概览
这里次要介绍一次 openFeign 申请调用的流程,对于注解解决以及组件注册的局部放到了文章的结尾局部。
-
Feign实例化newInstance()
+ 实例化 **SyncronizedMethodHandler** 以及 **ParseHandlersByName**,注入到 **ReflectFeign** 对象。
- 构建 ParseHandlersByName 对象,对于参数进行转化
-
构建 Contract 对象,对于申请参数进行校验和解析
- 实例化 SpringMvcContract 对象(继承自 Contract 对象)
- 调用parseAndValidateMetadata() 解决和校验数据类型
- 通过 jdk 动静代理Proxy 创立动静代理对象MethodInvocationHandler,调用动静代理对象的invoke() 办法
-
代理类 SyncronizedInvocationHandler 构建 requestTeamplate对象,并发送申请
- 调用 create() 构建申请实体对象
- 对于申请参数进行 encode() 操作
- 构建 client 对象,执行申请
- 返回申请后果
- 获取申请后果,申请实现
详解 openFeign 工作流程(重点)
1. Feign 实例化 – newInstance()
当服务通过 feign 调用另一个服务的时候,在 Fegin.builder 对象中,会调用结构器结构一个 Fegin 实例,上面是 feign.Feign.Builder#build 的代码内容:
public Feign build() {
// 构建外围组件和相干内容
Client client = Capability.enrich(this.client, capabilities);
Retryer retryer = Capability.enrich(this.retryer, capabilities);
List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
.map(ri -> Capability.enrich(ri, capabilities))
.collect(Collectors.toList());
Logger logger = Capability.enrich(this.logger, capabilities);
Contract contract = Capability.enrich(this.contract, capabilities);
Options options = Capability.enrich(this.options, capabilities);
Encoder encoder = Capability.enrich(this.encoder, capabilities);
Decoder decoder = Capability.enrich(this.decoder, capabilities);
InvocationHandlerFactory invocationHandlerFactory =
Capability.enrich(this.invocationHandlerFactory, capabilities);
QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
// 初始化 SynchronousMethodHandler.Factory 工厂,后续应用该工厂生成代理对象的办法
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
// 申请参数解析对象以及参数解决对象。负责依据申请类型构建对应的申请参数处理器
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
// 这里的 ReflectiveFeign 是整个外围的局部
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
}
执行 ReflectiveFeign 构建之后,会立马执行该 Fegin 子类的 ReflectiveFeign#newInstance()
办法。
public <T> T target(Target<T> target) {return build().newInstance(target);
}
这里设计的比拟奇妙。然而并不是特地难以了解
上面是 `ReflectiveFeign#newInstance` 办法的代码:
public <T> T newInstance(Target<T> target) {
// ParseHandlersByName::apply 办法构建申请参数解析模板和验证 handler 是否无效
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
// 对于办法 handler 进行解决
for (Method method : target.type().getMethods()) {if (method.getDeclaringClass() == Object.class) {continue;} else if (Util.isDefault(method)) {DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
// 创立接口代理对象。factory 在父类 build 办法进行初始化
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
// 绑定代理对象
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
上面就下面这段代码进行深刻的分析。
2. ParseHandlersByName 参数解析解决 – apply()
ReflectiveFeign#newInstance()
当中首先执行的是 feign.ReflectiveFeign.ParseHandlersByName
对象的 aplly()
办法,进行参数解析和参数解析构建器的构建。同时能够留神到,如果发现 method handler 没有在 feign 中找到对应配置,会抛出 IllegalStateException
异样。
public Map<String, MethodHandler> apply(Target target) {
// 2.1 大节进行解说
List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
// 依据申请参数的类型,实例化不同的申请参数构建器
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
// form 表单提交模式
buildTemplate =
new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else if (md.bodyIndex() != null) {
// 一般编码模式解决
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else {buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
}
if (md.isIgnored()) {result.put(md.configKey(), args -> {throw new IllegalStateException(md.configKey() + "is not a method handled by feign");
});
} else {result.put(md.configKey(),
factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
}
}
return result;
}
}
2.1 Contract 办法参数注解解析和校验 – parseAndValidateMetadata()
此办法的作用是:调用以解析链接到 HTTP 申请的类中的办法。
默认实例化对象为:<font color=’red’>SpringMvcContract</font>
因为这部分波及子父类的调用以及多个外部办法的调用并且办法内容较多,上面先介绍下 ** 父类 ** 的 `parseAndValidateMetadata()` 大抵的代码工作流程。
- 查看 handler 是否为 单继承 (单实现接口),并且 不反对参数化类型。否则将会抛出异样
-
遍历所有的外部办法
- 如果是静态方法跳过以后循环
- 获取 method 对象以及指标 class,执行外部办法
parseAndValidateMetadata()
外部办法为解决注解办法和参数内容,感兴趣能够自行理解源代码
- 查看是否为重写办法,如果是则抛出异样
Overrides unsupported
依据下面的介绍,上面看一下具体的逻辑代码:
public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
targetType.getSimpleName());
checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
targetType.getSimpleName());
if (targetType.getInterfaces().length == 1) {checkState(targetType.getInterfaces()[0].getInterfaces().length == 0,
"Only single-level inheritance supported: %s",
targetType.getSimpleName());
}
final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
for (final Method method : targetType.getMethods()) {if (method.getDeclaringClass() == Object.class ||
(method.getModifiers() & Modifier.STATIC) != 0 ||
Util.isDefault(method)) {continue;}
// 调用外部办法, 解决注解办法和参数信息
final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
metadata.configKey());
result.put(metadata.configKey(), metadata);
}
return new ArrayList<>(result.values());
}
2.2 SpringMvcContract 办法参数注解解析和校验
因为大部分的细节解决工作由父类实现:
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {processedMethods.put(Feign.configKey(targetType, method), method);
// 应用父类办法获取 MethodMetadata
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
RequestMapping classAnnotation = findMergedAnnotation(targetType,
RequestMapping.class);
if (classAnnotation != null) {
// produces - use from class annotation only if method has not specified this
// produces - 只有当办法未指定时才从类正文产生
if (!md.template().headers().containsKey(ACCEPT)) {parseProduces(md, method, classAnnotation);
}
// consumes -- use from class annotation only if method has not specified this
// consumes - 只有当 method 没有指定时才应用 from 类正文
if (!md.template().headers().containsKey(CONTENT_TYPE)) {parseConsumes(md, method, classAnnotation);
}
// headers -- class annotation is inherited to methods, always write these if
// present
// headers -- 类注解被继承到办法,如果有的话,肯定要写下来
parseHeaders(md, method, classAnnotation);
}
return md;
}
3. 创立接口动静代理
上面依据一个动静代理的结构图来了解 feign 是如何实现创立接口的代理对象的。
首先 target 就是咱们想要调用的指标服务的办法,在进过 contract 的注解解决之后,会交给 proxy 对象创立代理对象:
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
在这里的第一行代码利用工厂构建一个 InvocationHandler
实例,而后再应用 proxy.newInstance
依据代理指标办法对象的类型构建接口代理对象。
而 invocationHandler
的构建操作由 InvocationHandlerFactory
工厂构建而成,而工厂的构建细节又由 ReflectiveFeign.FeignInvocationHandler
实现。最终返回 FeignInvocationHandler 实现动静代理的后续操作。
static final class Default implements InvocationHandlerFactory {
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
}
}
创立接口代理对象之后,会执行 FeignInvocationHandler 的 invoke()
办法,
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {return false;}
} else if ("hashCode".equals(method.getName())) {return hashCode();
} else if ("toString".equals(method.getName())) {return toString();
}
// 通过 dispatch 获取所有办法的 handler 的援用,执行具体的 handler 办法
return dispatch.get(method).invoke(args);
}
这里波及了一个数据结构:
Map<Method, MethodHandler> methodToHandler
,也是动静代理的外围局部MehtodHandler 是一个 LinkedHashMap 的数据结构,他存储的了所有的办法对应接口代理对象的映射。
此属性由
new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
创立。
3.1 接口代理对象调用 feign.SynchronousMethodHandler#invoke()
申请逻辑
到了这一步,就是代理对象执行具体申请逻辑的局部了,这一部分包含创立一个申请模板,参数解析,依据参数配置 client,申请编码和申请解码,以及拦截器等等 ….. 波及的内容比拟多。这个大节作为 1 - 3 这三个局部的一个分割线。
4. SynchronousMethodHandler 动静代理对象解决详解
首先咱们看下整改 SynchronousMethodHandler 的invoke()
解决代码逻辑:
这里还是比拟容易了解的,最开始先过偶见一个 requestTemplate 模板,同时构建申请的相干 option,复制一个重试器配置给以后的线程应用。而后是外围的 executeAndDecode()
对于申请进行解码和返回后果,如果整个申请执行过程呈现重试异样,则尝试调用重试器进行解决,如果重试仍然失败,则抛出未受查看的异样或者抛出受查看的异样。最初依据日志的配置注销判断日志的打印和解决。
public Object invoke(Object[] argv) throws Throwable {
// 构建申请解决模板
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;
}
}
}
上面是浏览源码时长期做的局部笔记,大抵浏览即可。
- 通过
methodHandlerMap
散发到不同的申请实现处理器当中默认走
SynchronousMethodHandler
解决不同的申请
- 构建
requestTemplate
模板- 构建
requestOptions
配置- 获取重试器
Retry
应用 while(true) 进行无线循环. 执行申请并且对于申请的 template 和申请参数进行 decode 解决
调用拦截器对于申请进行拦挡解决(应用了责任链模式)
BasicAuthRequestInterceptor
:默认的调用权限验证拦挡FeignAcceptGzipEncodingInterceptor
gzip 编码解决开关连接器。用于判断是否容许开启 gzip 压缩FeignContentGzipEncodingInterceptor
:申请报文内容 gzip 压缩拦挡处理器如果日志的配置等级不为 none,进行对应日志级别的输入
执行
client.execute()
办法,发送 http 申请
- 应用
response.toBuilder
对于响应内容进行构建起的解决(留神源代码外面标注后续版本会废除这种形式? 为什么要废除? 那里不好)对于返回后果解码,调用
AsyncResponseHandler.handlerResponse
对于后果进行解决
这里的判断逻辑比拟多,判断的程序如下:
- 如果返回类型为
Response.class
- 如果
Body
内容为 null,执行 complete 调用这里应用了CompletableFuture 异步调用解决执行后果。保障整个处理过程是异步执行并且返回的
- CompletableFuture.complete()、
- CompletableFuture.completeExceptionally 只能被调用一次须要留神。
如果长度为空或者长度超过 ** 缓存后果最大长度。** 须要设置 ` shouldClose` 为 **false**,并且同样执行 complete 调用
如果返回状态大于 200 并且小于300
- 如果是 void 返回类型,间接调用
complete
- 否则对于返回后果进行解码,是否须要敞开依据解码之后的后果状态决定(没看懂)
- 如果是 404 并且返回值不为 void,则错误处理办法
- 如果上述都不满足,依据返回后果的错误信息封装谬误后果,并且依据谬误后果构建谬误对象。最初通过:
resultFuture.completeExceptionally
进行解决非凡解决:如果下面的所有判断出现异常信息,除开 io 异样须要二次封装解决之外,都会触发默认的
comoleteExceptionally
办法抛出一个终止异步线程的调用. + 验证工作是否实现,如果没有实现工作,调用
resultFuture.join()
办法将会在以后线程抛出一个未受查看的异样。
- 如果抛出异样,应用 retry 进行定时重试
4.1 构建 RequestTemplate 模板
作用是应用传递给办法调用的参数来创立申请模板。次要内容为申请的各种 url 解决包含参数解决,url 参数解决,对于迭代参数进行开展等等操作。这部分细节解决比拟多,因为篇幅无限这里挑重点讲一下:RequestTemplate template = resolve(argv, mutable, varBuilder);
这个办法,这里会依据当时定义的参数处理器解决参数,具体的代码如下:
RequestTemplate resolve(Object[] argv,
RequestTemplate mutable,
Map<String, Object> variables) {return mutable.resolve(variables);
}
外部调用的是 mutable
对象的 resolve
办法,那么它又是如何解决申请的呢?
依据不同的参数申请模板进行解决:
feign 通过不同的参数申请模板提供多样化的参数申请解决。上面先看一下具体的结构图:
这里很显著应用了策略模式,代码先依据参数找到具体的参数申请解决对象对于参数进行自定义的解决,在解决实现之后,调用 super.resolve()进行其余内容对立解决(模板办法)。设计的非常优良并且奇妙,上面是对应的办法签名:
`feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>)`
这里可能会有疑难,这个 BuildTemplateByResolvingArgs 是在哪里被初始化的?
BuildTemplateByResolvingArgs buildTemplate; // 依据申请参数的类型,实例化不同的申请参数构建器 if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { // form 表单提交模式 buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else if (md.bodyIndex() != null) { // 一般编码模式解决 buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else { // 应用默认的解决模板 buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target); }
解答:其实早在第二步 ParseHandlersByName 这一步就对于整个申请解决模板进行确认,同时代理对象也会沿用此解决模板保障申请的幂等性.
申请参数解决细节比照:
如果是 form 表单 提交的参数:
Map<String, Object> formVariables = new LinkedHashMap<String, Object>();
for (Entry<String, Object> entry : variables.entrySet()) {if (metadata.formParams().contains(entry.getKey())) {formVariables.put(entry.getKey(), entry.getValue());
}
}
如果 form 格局,个别会将 map 转为formVariables 的格局,留神外部应用的是 linkedhashmap 进行解决的
如果是 Body 的解决形式:
Object body = argv[metadata.bodyIndex()];
checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
留神:
- 这部分后续的版本可能会减少更多的解决模式,所有以最新的源码为准。留神文章题目申明的版本
- 对于格式化的呢绒
对于报文数据编码和解码的细节:
加密的工作是在: requestTemplate当中实现的,并且是在 BuildTemplateByResolvingArgs#resolve 中进行解决,依据不同的申请参数类型进行轻微的加密操作调整,然而代码根本相似.
上面是 Encoder 接口的默认实现:
class Default implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {if (bodyType == String.class) {template.body(object.toString());
} else if (bodyType == byte[].class) {template.body((byte[]) object, null);
} else if (object != null) {
throw new EncodeException(format("%s is not a type supported by this encoder.", object.getClass()));
}
}
}
- 如果是字符串类型,则调用对象的 tostring 办法
- 如果是字节数组则转为字节数组进行存储
- 如果对象为空,则抛出加密 encode 异样
说完了加密,天然也要说下解码的动作如何解决的,上面是默认的解码接口的实现 <font color=’gray’>(留神父类是 StringDecoder 而不是 Decoder)</font>:
public class Default extends StringDecoder {
@Override
public Object decode(Response response, Type type) throws IOException {
// 这里的硬编码感觉挺突兀的,不晓得是否为设计有失误还是单纯程序员偷懒。// 比拟偏向于退出 if(response == null) return null; 这一段代码
if (response.status() == 404 || response.status() == 204)
return Util.emptyValueOf(type);
if (response.body() == null)
return null;
if (byte[].class.equals(type)) {return Util.toByteArray(response.body().asInputStream());
}
return super.decode(response, type);
}
}
这里很奇怪竟然用了硬编码的模式。(老外编码总是非常自在)当返回状态为 404 或者 204 的时候。则依据对象的数据类型构建相干的数据类型默认值,如果是对象则返回一个空对象
- 204 编码代表了空文件的申请
- 200 代表胜利响应申请
最初一行示意如果类型都不合乎状况下应用父类 StringDecoder 字符串的类型解码的操作,如果字符串无奈解码,则抛出异样信息。感兴趣能够看下 StringDecoder#decode()
的实现细节,这里不再展现。
如果产生谬误,如何对错误信息进行编码?
public Exception decode(String methodKey, Response response) {FeignException exception = errorStatus(methodKey, response);
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;
}
- 依据错误信息和办法签名,构建异样对象
- 应用重试编码进行返回申请头的解决动作,开启失败之后的稍后重试操作
- 如果稍后重试失败,则抛出相干异样
- 返回异样信息
4.2 option 配置获取
代码比较简单,这里间接开展了,如果没有调用参数,返回默认的 option 陪孩子,否则依照制订条件构建 Options 配置
Options findOptions(Object[] argv) {if (argv == null || argv.length == 0) {return this.options;}
return Stream.of(argv)
.filter(Options.class::isInstance)
.map(Options.class::cast)
.findFirst()
.orElse(this.options);
}
4.3 构建重试器
重试器这部分会调用一个叫做 clone()的办法,留神这个 clone 办法是被重写过的,应用的是默认实现的重试器。另外,集体认为这个办法的起名容易造成误会,集体比拟偏向于构建一个叫做 new Default()
的构造函数。
public Retryer clone() {return new Default(period, maxPeriod, maxAttempts);
}
重试器比拟重要的办法是对于异样之后的重试操作,上面是对应的方代码
public void continueOrPropagate(RetryableException e) {if (attempt++ >= maxAttempts) {throw e;}
long interval;
if (e.retryAfter() != null) {interval = e.retryAfter().getTime() - currentTimeMillis();
if (interval > maxPeriod) {interval = maxPeriod;}
if (interval < 0) {return;}
} else {interval = nextMaxInterval();
}
try {Thread.sleep(interval);
} catch (InterruptedException ignored) {Thread.currentThread().interrupt();
throw e;
}
sleptForMillis += interval;
}
这里的重试距离依照 1.5 的倍数 进行重试,如果超过重试设置的最大因子数则进行重试。
4.4 申请发送和后果解决
当进行下面的根底配置之后紧接着就是执行申请的发送操作了,在发送只求之前还有一步要害的操作:拦截器解决
这里会遍历当时配置的拦截器,对于申请模板做最初的解决操作。
Request targetRequest(RequestTemplate template) {for (RequestInterceptor interceptor : requestInterceptors) {interceptor.apply(template);
}
return target.apply(template);
}
对于日志输入级别的管制
执行申请这部分代码当中,会呈现比拟多相似上面的代码。
if (logLevel != Logger.Level.NONE) {logger.logRequest(metadata.configKey(), logLevel, request);
}
对于日志输入的级别依据如下的内容:
public enum Level {
/**
* No logging.
不进行打印,也是默认配置
*/
NONE,
/**
* Log only the request method and URL and the response status code and execution time.
只记录申请办法和 URL 以及响应状态代码和执行工夫。*/
BASIC,
/**
* Log the basic information along with request and response headers.
记录根本信息以及申请和响应头。*/
HEADERS,
/**
* Log the headers, body, and metadata for both requests and responses.
记录申请和响应的头、主体和元数据。*/
FULL
}
client 发送申请(重点)
这里同样截取了 feign.SynchronousMethodHandler#executeAndDecode
的局部代码,毫无疑问最要害的局部是 client.execute(request, options)
办法。上面是对应的代码内容:
Response response;
long start = System.nanoTime();
try {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);
上面是 client 对象的继承结构图:
依据下面的结构图,简略阐明 client 的默认实现:
- 申请方策略实现,定义顶层接口 client,在默认的状况下应用 Default 类作为实现类。通过子类proxied 对象实现 java.net 的URL 申请形式。也就是说即便没有任何的辅助三方工具,也能够通过此办法 api 模仿构建 http 申请。
- 能够应用 okhttp 和httpclient 高性能实现进行代替,须要引入对应的 feign 接入实现。
client 对应的 Default 代码逻辑:
- 构建申请 URL 对象HttpUrlConnection
- 如果是 Http 申请对象,能够依据条件设置 ssl 或者域名签名
- 设置 http 根本申请参数
- 收集 Header 信息,设置 GZIP 压缩编码
- 设置 accept:*/*
- 查看是否开启外部缓冲,如果设置了则依照指定长度缓冲
代码调用的外围局部,默认依照 java.net 的httpconnection 进行解决。应用原始的网络 IO 流进行申请的解决,效率比拟低上面是对应的具体实现代码:
public Response execute(Request request, Options options) throws IOException {HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
通过数据转化和申请发送之后上面依据后果进行响应内容的封装和解决:
// 申请后果解决
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {int status = connection.getResponseCode();
String reason = connection.getResponseMessage();
// 状态码异样解决
if (status < 0) {throw new IOException(format("Invalid status(%s) executing %s %s", status,
connection.getRequestMethod(), connection.getURL()));
}
// 申请头的解决
Map<String, Collection<String>> headers = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null) {headers.put(field.getKey(), field.getValue());
}
}
Integer length = connection.getContentLength();
if (length == -1) {length = null;}
InputStream stream;
// 对于状态码 400 以上的内容进行错误处理
if (status >= 400) {stream = connection.getErrorStream();
} else {stream = connection.getInputStream();
}
// 构建返回后果
return Response.builder()
.status(status)
.reason(reason)
.headers(headers)
.request(request)
.body(stream, length)
.build();}
小插曲:对于 reason 属性(能够跳过)
查看源代码的时候无意间看到这里有一个集体比拟在意的点,上面是 respose 中有一个叫做 reason 的字段:
/** * Nullable and not set when using http/2 * 作者如下阐明 在 http2 中能够不设置改属性 * See https://github.com/http2/http2-spec/issues/202 */ public String reason() {return reason;}
看到这一段登时有些好奇 为什么不须要设置 reason,当然 github 下面也有相似的发问。
这个老哥是在 2013 年是这么答复的,直白翻译就是:关我卵事!
然而事件没有完结,前面又有人具体的进行了发问
原文 i'm curious what was the logical reason for dropping the reason phrase? i was using the reason phrase as a title for messages presented to a user in the web browser client. i think most users are accustomed to such phrases, "Bad Request", "Not Found", etc. Now I will just have to write a mapping from status codes to my own reason phrases in the client. 机翻:我很好奇,放弃 "reason" 这个词的逻辑起因是什么? 我应用“reason”作为在 web 浏览器客户端向用户出现的音讯的题目。我认为大多数用户习惯于这样的短语,“谬误申请”,“未找到”等。当初我只须要在客户机中编写一个从状态代码到我本人的理由短语的映射。
而后预计是受不了各种发问,上文的 mnot 五年后给出了一个明确的答复:
起因短语——即便在 HTTP/1.1 中——也不能保障端到端携带; 实现能够 (也的确) 疏忽它并替换本人的值(例如,200 总是“OK”,不论在网络上产生什么)。思考到这一点,再加上携带额定字节的开销,将其从线路上删除是有意义的。
为了证实他的说法,从 >https://www.w3.org/Protocols/… w3c 的网站中找到的如下的阐明:
The Status-Code is intended for use by automata and the Reason-Phrase is intended for the human user. The client is not required to examine or display the Reason- Phrase. 状态代码用于自动机,而起因短语用于人类用户。客户端不须要查看或显示起因 - 短语。
这一段来源于 Http1.1 的标准形容。
所以有时候能从源码发掘出不少的故事,挺乏味的
FeignBlockingLoadBalancerClient 作为负载平衡应用:
这个类相当于 openFeign 和 ribbon 的直达类,将 openfeign 的申请转接给 ribbon 实现负载平衡。到这里会有一个疑难:client 是如何做出抉择应用 ribbon 还是 spring cloud 的呢的呢?
其实认真想想不难理解,负载平衡必定是在 spring bean 初始化的时候实现的。FeignClientFactoryBean是整个实现的要害。
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware
上面是 org.springframework.cloud.openfeign.FeignClientFactoryBean#getTarget
办法代码
@Override
public Object getObject() **throws** Exception {return getTarget();
}
/**
\* @param <T> the target type of the Feign client 客户端的指标类型
\* @return a {@link Feign} client created with the specified data and the context 指定数据或者上下文
\* information
*/
<T> T getTarget() {FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
// 如果 URL 为空,默认会尝试应用 **
if (!StringUtils.hasText(url)) {if (!name.startsWith("http")) {url = "http://" + name;}
else {url = name;}
url += cleanPath();
// ** 默认应用 ribbon 作为负载平衡,如果没有找到,会抛出异样 **
return (T) loadBalance(builder, context,
new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {url = "http://" + url;}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
// 依据以后的零碎设置实例化不同的负载均衡器
if (client != null) {if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,but ribbon is on the classpath, so unwrap
// 不是负载平衡,因为咱们有一个 url,然而 ribbon 在类门路上,所以开展
client = ((LoadBalancerFeignClient) client).getDelegate();}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url, but Spring Cloud LoadBalancer is on the classpath, so unwrap
// 不是负载平衡,因为咱们有一个 url,但 Spring Cloud LoadBalancer 是在类门路上,所以开展
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(type, name, url));
}
下面的内容形容了一个负载均衡器的初始化的残缺过程。也证实了spring cloud 应用 ribbon 作为默认的初始化,感兴趣能够全局搜寻一下这一段异样,间接阐明默认应用的是 ribbon 作为负载平衡:
throw new IllegalStateException("No Feign Client for defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
拓展:
在 feign.Client.Default#convertAndSend(),有一段如下的代码设置
connection.setChunkedStreamingMode(8196);
如果在代码中禁用 ChunkedStreamMode,与设置 4096 的代码相比有什么成果?
这样做的后果是整个输入都被缓冲,直到敞开为止,这样 Content-length 标头能够被首先设置和发送,这减少了很多提早和内存。对于大文件,不倡议应用。
答案起源:HttpUrlConnection.setChunkedStreamingMode 的成果
对于编解码的解决
这一部分请浏览 4.1 局部的 对于报文数据编码和解码的细节 局部内容
至此一个根本的调用流程根本就算是实现了。
openFeign 整体调用链路图
先借(偷)一张参考资料的图来看下整个 openFeign 的链路调用:
上面是集体依据材料本人画的图:
openFeign 注解解决流程
咱们先看下开启 openFeign 的形式注解:@EnableFeignClients
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {}
留神这里的一个注解 @Import(FeignClientsRegistrar.class)
。毫无疑问,实现的细节在FeignClientsRegistrar.class
外部:
剔除掉其余的逻辑和细节,要害代码在这一块:
for (String basePackage : basePackages) {
//….
registerFeignClient(registry, annotationMetadata, attributes);
//….
}
这里调用了 registerFeignClient
注册 feign,依据注解配置扫描失去响应的 basepakage,如果没有配置,则默认依照注解所属类的门路进行扫描。
上面的代码依据扫描的后果注入相干的 bean 信息,比方 url,path,name,回调函数等。最初应用 BeanDefinitionReaderUtils 对于 bean 的办法和内容进行注入。
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {String className = annotationMetadata.getClassName();
//bean 配置
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
// has a default, won't be null
// 如果未配置会存在默认的配置
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {alias = qualifier;}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias});
// 注册 Bean
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
看完了根本的注册机制,咱们再来看看 Bean 是如何实现主动注入的:这里又牵扯到另一个注解 -@FeignAutoConfiguration
@FeignAutoConfiguration 简略介绍
对于 feign 的注入,在此类中提供了两种的模式:
- 如果存在HystrixFeign,则应用 HystrixTargeter 办法。
-
如果不存在,此时会实例化一个DefaultTargeter 作为默认的实现者
具体的操作代码如下:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "feign.hystrix.HystrixFeign") protected static class HystrixFeignTargeterConfiguration { @Bean // 优先应用 Hystrix @ConditionalOnMissingBean public Targeter feignTargeter() {return new HystrixTargeter(); } } @Configuration(proxyBeanMethods = false) // 如果不存在 Hystrix,则应用默认的 tagerter @ConditionalOnMissingClass("feign.hystrix.HystrixFeign") protected static class DefaultFeignTargeterConfiguration { @Bean @ConditionalOnMissingBean public Targeter feignTargeter() {return new DefaultTargeter(); } }
温习一下 springboot 几个外围的注解代表的含意:
- @ConditionalOnBean // 当给定的在 bean 存在时, 则实例化以后 Bean
- @ConditionalOnMissingBean // 当给定的在 bean 不存在时, 则实例化以后 Bean
- @ConditionalOnClass // 当给定的类名在类门路上存在,则实例化以后 Bean
- @ConditionalOnMissingClass // 当给定的类名在类门路上不存在,则实例化以后 Bea
对于 HystrixInvocationHandler 的 invoke 办法:
Feign.hystrix.HystrixInvocationHandler
当中执行的 invoke 实际上还是 SyncronizedMethodHandler 办法
HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
外部代码同时还应用了命令模式的命令 HystrixCommand 进行封装。因为不是本文重点,这里不做扩大。
HystrixCommand 这个对象又是拿来干嘛的?
简介:用于包装代码,将执行具备潜在危险的性能 (通常是指通过网络的服务调用) 与故障和提早容忍,统计和性能指标捕捉,断路器和隔板性能。这个命令实质上是一个阻塞命令,但如果与
observe()
一起应用,它提供了一个可察看对象外观。实现接口:
HystrixObservable
/HystrixInvokableInfo
HystrixInvokableInfo
: 存储命令接口的标准,子类要求实现
HystrixObservable
: 变成观察者反对非阻塞调用
总结
第一次总结源码,更多的是参考网上的材料顺着他人的思路本人去一点点看的。(哈哈,闻道有先后,术业有专攻)如果有谬误欢送指出。
不同于 spring 那简单层层形象,openFeign 的学习和“模拟”价值更具备意义,很多代码一眼就能够看到设计模式的影子,比拟适宜本人练手和学习进步集体的编程技巧。
另外,openFeign 应用了很多的包拜访构造,这对于在此基础上二次扩大的 sentianl 框架是个头疼的问题,不过好在能够站在反射大哥的背地,间接暴力拜访。
参考资料:
掘金博客【十分好】
对于负载平衡的介绍起源
官网文档
联合源码再回顾官网文档提到的性能
在线代码格式化
在线画图软件