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注解下的接口,并通过动静代理的形式产生实现类,实现类中做负载平衡并调用其余服务。

依据下面的形容,绘制如下的表格内容:

-RibbonFeignOpenFeign
应用形式手动调用指标服务Feign的注解定义接口,调用接口就能够调用注册核心服务能够间接应用服务调用的形式调用对应的服务
作用客户端负载平衡,服务注册核心的服务调用客户端负载平衡,服务注册核心的服务调用动静代理的形式产生实现类,实现类中做负载平衡并调用其余服务
开发商NetfixSpring CloudSpring 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,能够应用上面的写法:

@Configurationpublic 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=truefeign.compression.response.enabled=true

如果须要更进一步的配置,能够应用如下的模式进行配置:

feign.compression.request.enabled=truefeign.compression.request.mime-types=text/xml,application/xml,application/jsonfeign.compression.request.min-request-size=2048

留神2048值为压缩申请的最小阈值,因为如果对于所有申请进行gzip压缩,对于小文件的性能开销要反而要更大

通过上面的配置来开启gzip压缩(压缩编码为UTF-8,默认):

feign.compression.response.enabled=truefeign.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框架是个头疼的问题,不过好在能够站在反射大哥的背地,间接暴力拜访。

参考资料:

掘金博客【十分好】

对于负载平衡的介绍起源

官网文档

联合源码再回顾官网文档提到的性能

在线代码格式化

在线画图软件