Spring Cloud openFeign学习【3.0.2版本】

Spring Cloud openFeign学习【3.0.2版本】

前言

​ 内容分为openFeign大体的使用和源码的我的解读,里面参考了很多其余优秀博客做者的内容,不少地方基本算是拾人牙慧了,不过仍是顺着源码读了一遍加深理解。html

openFeign 是什么?

​ Feign是一个声明性web服务客户端。它使编写web服务客户机更加容易,要使用Feign,须要建立一个接口并对其进行注释。它具备可插入注释支持,包括Feign注释和JAX-RS注释。java

​ Feign还支持可插拔编码器和解码器。Spring Cloud增长了对Spring MVC注解的支持,并支持使用Spring Web中默认使用的相同HttpMessageConverters。git

​ Spring Cloud集成了Eureka、Spring Cloud CircuitBreaker和Spring Cloud LoadBalancer,在使用Feign时提供一个负载均衡的http客户端程序员

如何学习?

​ 框架最大的意义在于使用,其实最好的教程就是边作边参考官方的文档学习。github

官方文档目录地址web

官方openFeign的文档spring

应用场景?

​ 能够看到openFeign做为服务的调用中转,负责服务之间的链接和请求转发的操做。OpenFeign做为编写服务调用支持组件在spring cloud中占有极为重要的位置。编程

​ 和RPC的通讯框架不一样,openFeign使用了传统的http做为传输结构。json

​ 在以往使用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增长了那些功能:

  1. 可插拔的注解支持,包括Feign注解和JSX-RS注解。
  2. 支持可插拔的HTTP编码器和解码器。
  3. 支持Hystrix和它的Fallback。
  4. 支持Ribbon的负载均衡。
  5. 支持HTTP请求和响应的压缩。

openFeign的client实现方替换:

  1. 可使用http client 替换,而且openFeign 提供了良好的配置,能够支持httpclient的细节化配置。
  2. 使用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(BeanTypebeanName :)ClassName

  • DecoderfeignDecoder :(ResponseEntityDecoder包含SpringDecoder
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • MicrometerCapabilitymicrometerCapability:若是feign-micrometer在类路径上而且MeterRegistry可用
  • Contract feignContract: SpringMvcContract
  • Feign.Builder feignBuilder: FeignCircuitBreaker.Builder
  • ClientfeignClient:若是在类路径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()`大体的代码工做流程。
  1. 检查handler是否为单继承(单实现接口),而且不支持参数化类型。不然将会抛出异常
  2. 遍历全部的内部方法

    1. 若是是静态方法跳过当前循环
    2. 获取method对象以及目标class,执行内部方法parseAndValidateMetadata()
    内部方法为处理注解方法和参数内容,感兴趣能够自行了解源代码
  3. 检查是否为重写方法,若是是则抛出异常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动态代理对象处理详解

​ 首先咱们看下整改SynchronousMethodHandlerinvoke()处理代码逻辑:

​ 这里仍是比较容易理解的,最开始先过偶见一个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; } } }

下面是阅读源码时临时作的部分笔记,大体浏览便可。

  1. 经过methodHandlerMap 分发到不一样的请求实现处理器当中
  2. 默认走SynchronousMethodHandler 处理不一样的请求

    • 构建requestTemplate 模板
    • 构建requestOptions 配置
    • 获取重试器Retry
  3. 使用while(true) 进行无线循环. 执行请求而且对于请求的template和请求参数进行decode处理

    • 调用拦截器对于请求进行拦截处理(使用了责任链模式)

      • BasicAuthRequestInterceptor:默认的调用权限验证拦截
      • FeignAcceptGzipEncodingInterceptor gzip编码处理开关链接器。用于判断是否容许开启gzip压缩
      • FeignContentGzipEncodingInterceptor:请求报文内容gzip压缩拦截处理器
    若是日志的配置等级不为none,进行对应日志级别的输出
  4. 执行 client.execute() 方法,发送http请求

    • 使用response.toBuilder 对于响应内容进行构建起的处理(注意源代码里面标注后续版本会废弃这种方式? 为何要废弃? 那里很差
  5. 对于返回结果解码,调用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() 方法将会在当前线程抛出一个未受检查的异常。

  1. 若是抛出异常,使用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());

注意:

  1. 这部分后续的版本可能会增长更多的处理形式,一切以最新的源码为准。注意文章标题声明的版本
  2. 对于格式化的呢绒
关于报文数据编码和解码的细节:

​ 加密的工做是在: 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())); } } }
  1. 若是是字符串类型,则调用对象的tostring 方法
  2. 若是是字节数组则转为字节数组进行存储
  3. 若是对象为空,则抛出加密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; }
  1. 根据错误信息和方法签名,构建异常对象
  2. 使用重试编码进行返回请求头的处理动做,开启失败以后的稍后重试操做
  3. 若是稍后重试失败,则抛出相关异常
  4. 返回异常信息

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的默认实现:

  1. 请求方策略实现,定义顶层接口 client,在默认的状况下使用Default 类做为实现类。经过子类proxied对象实现 java.netURL请求方式。也就是说即便没有任何的辅助三方工具,也能够经过此方法api模拟构建http请求。
  2. 可使用okhttphttpclient 高性能实现进行替代,须要引入对应的feign接入实现。

client对应的Default代码逻辑:

  • 构建请求URL对象HttpUrlConnection
  • 若是是Http请求对象,能够根据条件设置ssl或者域名签名
  • 设置http基本请求参数
  • 收集Header信息,设置GZIP压缩编码
  • 设置accept:*/*
  • 检查是否开启内部缓冲,若是设置了则按照指定长度缓冲

​ 代码调用的核心部分,默认按照java.nethttpconnection 进行处理。使用原始的网络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框架是个头疼的问题,不过好在能够站在反射大哥的背后,直接暴力访问。

参考资料:

掘金博客【很是好】

关于负载均衡的介绍来源

官方文档

结合源码再回顾官方文档提到的功能

在线代码格式化

在线画图软件