精华内容
下载资源
问答
  • Hystrix、Feign技术底层实现原理
    千次阅读
    2020-05-26 11:55:30

    一. Feign的设计原理

    1.1 Feign是什么

           Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign被广泛应用在Spring Cloud 的解决方案中,是学习基于Spring Cloud 微服务架构不可或缺的重要组件。

    1.2 Feign解决了什么问题

           封装了Http调用流程,更适合面向接口化的变成习惯。在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。

    1.3 Feign是如何设计的

          Feign的远程调用基本流程,大致如图所示:

           

     PHASE 1. 基于面向接口的动态代理方式生成实现类

           在使用feign 时,会定义对应的接口类,在接口类上使用Http相关的注解,标识HTTP请求参数信息;在Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:

            

      PHASE 2. 根据Contract协议规则,解析接口类的注解信息,解析成内部表现

               

     

      默认Contract 实现

     Feign 默认有一套自己的协议规范,规定了一些注解,可以映射成对应的Http请求,如官方的一个例子:

    public interface GitHub {
      
      @RequestLine("GET /repos/{owner}/{repo}/contributors")
      List<Contributor> getContributors(@Param("owner") String owner, @Param("repo") String repository);
      
      class Contributor {
        String login;
        int contributions;
      }
    }
    
    
    

     上述的例子中,尝试调用GitHub.getContributors("foo","myrepo")的的时候,会转换成如下的HTTP请求:

    GET /repos/foo/myrepo/contributors
    HOST XXXX.XXX.XXX

    基于Spring MVC的协议规范SpringMvcContract:

    当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务:

      

    当然,目前的Spring MVC的注解并不是可以完全使用的,有一些注解并不支持,如@GetMapping,@PutMapping 等,仅支持使用@RequestMapping 等,另外注解继承性方面也有些问题。

    PHASE 3. 基于 RequestBean,动态生成Request

    这一步很简单,就是根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request 对象。

    PHASE 4. 使用Encoder 将Bean转换成 Http报文正文(消息解析和转码逻辑)

    Feign 最终会将请求转换成Http 消息发送出去,传入的请求对象最终会解析成消息体,如下所示:   

     

    在接口定义上Feign做的比较简单,抽象出了Encoder 和decoder 接口:

    public interface Encoder {
      /** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
      Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
    
      /** 
     * 将实体对象转换成Http请求的消息正文中 
     */
      void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
    
      /** * Default implementation of {@code 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()));
          }
        }
      }
    }

     

    public interface Encoder {
      /** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
      Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
    
      /** 
     * 将实体对象转换成Http请求的消息正文中 
     */
      void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
    
      /** * Default implementation of {@code 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()));
          }
        }
      }
    }

    目前Feign 有以下实现:

    Encoder/ Decoder 实现

    说明

    JacksonEncoder,JacksonDecoder基于 Jackson 格式的持久化转换协议
    GsonEncoder,GsonDecoder基于Google GSON 格式的持久化转换协议
    SaxEncoder,SaxDecoder基于XML 格式的Sax 库持久化转换协议
    JAXBEncoder,JAXBDecoder基于XML 格式的JAXB 库持久化转换协议
    ResponseEntityEncoder,ResponseEntityDecoderSpring MVC 基于 ResponseEntity< T > 返回格式的转换协议
    SpringEncoder,SpringDecoder基于Spring MVC HttpMessageConverters 一套机制实现的转换协议 ,应用于Spring Cloud 体系中

    PHASE 5. 拦截器负责对请求和返回进行装饰处理

    在请求转换的过程中,Feign 抽象出来了拦截器接口,用于用户自定义对请求的操作:

    public interface RequestInterceptor {
    
      /** 
     * 可以在构造RequestTemplate 请求时,增加或者修改Header, Method, Body 等信息
     */
      void apply(RequestTemplate template);
    }

    比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器:

    public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
    
        protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
            super(properties);
        }
    
        /** 
     * {@inheritDoc}
     */
        @Override
        public void apply(RequestTemplate template) {
            // 在Header 头部添加相应的数据信息
            addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
                    HttpEncoding.DEFLATE_ENCODING);
        }
    }


    PHASE 6. 日志记录

    在发送和接收请求的时候,Feign定义了统一的日志门面来输出日志信息 , 并且将日志的输出定义了四个等级:

    级别

    说明

    NONE不做任何记录
    BASIC只记录输出Http 方法名称、请求URL、返回状态码和执行时间
    HEADERS记录输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息
    FULL记录Request 和Response的Header,Body和一些请求元数据

    PHASE 7 . 基于重试器发送HTTP请求

    Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求,重试器有如下几个控制参数:

    重试参数

    说明

    默认值

    period初始重试时间间隔,当请求失败后,重试器将会暂停 初始时间间隔(线程 sleep 的方式)后再开始,避免强刷请求,浪费性能100ms
    maxPeriod当请求连续失败时,重试的时间间隔将按照:long interval = (long) (period * Math.pow(1.5, attempt - 1)); 计算,按照等比例方式延长,但是最大间隔时间为 maxPeriod, 设置此值能够避免 重试次数过多的情况下执行周期太长1000ms
    maxAttempts最大重试次数5

    以下是Feign重试的核心代码逻辑:

    final class SynchronousMethodHandler implements MethodHandler {
    
      @Override
      public Object invoke(Object[] argv) throws Throwable {
       //根据输入参数,构造Http 请求。
        RequestTemplate template = buildTemplateFromArgs.create(argv);
        // 克隆出一份重试器
        Retryer retryer = this.retryer.clone();
        // 尝试最大次数,如果中间有结果,直接返回
        while (true) {
          try {
            return executeAndDecode(template);
          } catch (RetryableException e) {
            retryer.continueOrPropagate(e);
            if (logLevel != Logger.Level.NONE) {
              logger.logRetry(metadata.configKey(), logLevel);
            }
            continue;
          }
        }
      }

    PHASE 8. 发送Http请求

    Feign 真正发送HTTP请求是委托给 feign.Client 来做的:

    public interface Client {
    
      /** 
     * 执行Http请求,并返回Response * @param request safe to replay.  
     */
      Response execute(Request request, Options options) throws IOException;
      }

    Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端,目前我们荐彩等项目内部使用的就是OkHttp3作为Http 客户端。

     

    二: Hystrix的设计原理

           Hystrix的三大设计原则:资源隔离、熔断器和命令模式。

    2.1 资源隔离

         在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如: 商品详情展示服务会依赖商品服务, 价格服 务, 商品评论服务. 如图所示:

          

            Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩. 如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务 的调用.

          

          线程池隔离的几点好处:

     

    • 使用超时返回的机制,避免同步调用服务时,调用时间过长,无法释放,导致资源耗尽的情况
    • 服务方可以控制请求数量,请求过多,可以直接拒绝,达到快速失败的目的;
    • 请求排队,线程池可以维护执行队列,将请求压到队列中处理

     

           但是使用线程池隔离也有一些弊端, 线程池隔离模式,会根据服务划分出独立的线程池,系统资源的线程并发数是有限的,当线程数过多,系统话费大量的CPU时间来做线程上下文切换的无用操作,反而降低系统性能;如果线程池隔离的过多,会导致真正用于接收用户请求的线程就相应地减少,系统吞吐量反而下降;在实践上,应当对像远程方法调用,网络资源请求这种服务时间不太可控的场景下使用线程池隔离模式处理。

    2.2  熔断器模式

    熔断器模式定义了熔断器开关相互转换的逻辑:

     

    服务的健康状况 = 请求失败数 / 请求总数. 熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
    当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况高于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.
    当熔断器开关打开时, 请求被禁止通过.
    当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.

    2.3 命令模式

     

            Hystrix使用命令模式(继承HystrixCommand类或者是HystrixObservableCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback).
    同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数. 如下代码所示:

    public class Service1HystrixCommand extends HystrixCommand<Response> {
    private Service1 service;
    private Request request;

    public Service1HystrixCommand(Service1 service, Request request){
    supper(
    Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"))
    .andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query"))
    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool"))
    .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
    .withCoreSize(20))//服务线程池数量
    .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
    .withCircuitBreakerErrorThresholdPercentage(60)//熔断器关闭到打开阈值
    .withCircuitBreakerSleepWindowInMilliseconds(3000)//熔断器打开到关闭的时间窗长度
    ))
    this.service = service;
    this.request = request;
    );
    }

    @Override
    protected Response run(){
    return service1.call(request);
    }

    @Override
    protected Response getFallback(){
    return Response.dummy();
    }
    }

    在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能.

     

    2.4 Hystrix的内部处理逻辑

     下图为Hystrix服务调用的内部逻辑:

     

    1.构建Hystrix的Command对象, 调用执行方法.

    2.Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.

    3.若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法.

    4.若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.

    5.若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.

    6.若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.

    7.若服务执行成功, 返回正常结果.

    8.若服务降级方法getFallback执行成功, 则返回降级结果.

    9.若服务降级方法getFallback执行失败, 则抛出异常.

    2.5 Spring Cloud 下 Hystrix使用要注意的问题

    • Hystrix配置无法动态调节生效。Hystrix框架本身是使用的Archaius框架完成的配置加载和刷新,但是集成自 Spring Cloud下,无法有效地根据实时监控结果,动态调整熔断和系统参数
    • 线程池和Command之间的配置比较复杂,在Spring Cloud在做feigin-hystrix集成的时候,还有些BUG,对command的默认配置没有处理好,导致所有command占用公共的command线程池,没有细粒度控制,还需要做框架适配调整
    更多相关内容
  • Spring Cloud-Feign设计原理

    万次阅读 多人点赞 2018-09-26 15:42:50
    什么是FeignFeign 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数...

    什么是Feign?

    Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
    Feign被广泛应用在Spring Cloud 的解决方案中,是学习基于Spring Cloud 微服务架构不可或缺的重要组件。
    开源项目地址:
    https://github.com/OpenFeign/feign

    Feign解决了什么问题?

    封装了Http调用流程,更适合面向接口化的变成习惯
    在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如下:

    调用方 Client框架 服务方 构造Http请求URL 填写Http请求头信息 填写消息报文信息 发送Http请求 处理请求,返回结 返回报文 提取报文信息,转换成对应的Java bean 根据Bean中 的定义,业务处理 调用方 Client框架 服务方

    Feign是如何设计的?

    在这里插入图片描述

    PHASE 1. 基于面向接口的动态代理方式生成实现类

    在使用feign 时,会定义对应的接口类,在接口类上使用Http相关的注解,标识HTTP请求参数信息,如下所示:

    interface GitHub {
      @RequestLine("GET /repos/{owner}/{repo}/contributors")
      List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
    }
    
    public static class Contributor {
      String login;
      int contributions;
    }
    
    public class MyApp {
      public static void main(String... args) {
        GitHub github = Feign.builder()
                             .decoder(new GsonDecoder())
                             .target(GitHub.class, "https://api.github.com");
      
        // Fetch and print a list of the contributors to this library.
        List<Contributor> contributors = github.contributors("OpenFeign", "feign");
        for (Contributor contributor : contributors) {
          System.out.println(contributor.login + " (" + contributor.contributions + ")");
        }
      }
    }
    

    在Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:
    在这里插入图片描述

     public class ReflectiveFeign extends Feign{
      ///省略部分代码
      @Override
      public <T> T newInstance(Target<T> target) {
        //根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
    
        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)));
          }
        }
        InvocationHandler handler = factory.create(target, methodToHandler);
        // 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
    
        for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
          defaultMethodHandler.bindTo(proxy);
        }
        return proxy;
      }
      //省略部分代码
    

    PHASE 2. 根据Contract协议规则,解析接口类的注解信息,解析成内部表现:

    在这里插入图片描述

    Feign 定义了转换协议,定义如下:

    /**
     * Defines what annotations and values are valid on interfaces.
     */
    public interface Contract {
    
      /**
       * Called to parse the methods in the class that are linked to HTTP requests.
       * 传入接口定义,解析成相应的方法内部元数据表示
       * @param targetType {@link feign.Target#type() type} of the Feign interface.
       */
      // TODO: break this and correct spelling at some point
      List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
    }
    
    默认Contract 实现

    Feign 默认有一套自己的协议规范,规定了一些注解,可以映射成对应的Http请求,如官方的一个例子:

    public interface GitHub {
      
      @RequestLine("GET /repos/{owner}/{repo}/contributors")
      List<Contributor> getContributors(@Param("owner") String owner, @Param("repo") String repository);
      
      class Contributor {
        String login;
        int contributions;
      }
    }
    
    

    上述的例子中,尝试调用GitHub.getContributors(“foo”,“myrepo”)的的时候,会转换成如下的HTTP请求:

    GET /repos/foo/myrepo/contributors
    HOST XXXX.XXX.XXX
    

    Feign 默认的协议规范

    注解接口Target使用说明
    @RequestLine方法上定义HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表达式,可以通过在方法参数上使用@Param 自动注入
    @Param方法参数定义模板变量,模板变量的值可以使用名称的方式使用模板注入解析
    @Headers类上或者方法上定义头部模板变量,使用@Param 注解提供参数值的注入。如果该注解添加在接口类上,则所有的请求都会携带对应的Header信息;如果在方法上,则只会添加到对应的方法请求上
    @QueryMap方法上定义一个键值对或者 pojo,参数值将会被转换成URL上的 query 字符串上
    @HeaderMap方法上定义一个HeaderMap, 与 UrlTemplate 和HeaderTemplate 类型,可以使用@Param 注解提供参数值

    具体FeignContract 是如何解析的,不在本文的介绍范围内,详情请参考代码:
    https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Contract.java

    基于Spring MVC的协议规范SpringMvcContract:

    当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务:
    在这里插入图片描述

    我们团队内部就是按照这种思路,结合Spring Boot Starter 的特性,定义了服务端starter,
    服务消费者在使用的时候,只需要引入Starter,就可以调用服务。这个比较适合平台无关性,接口抽象出来的好处就是可以根据服务调用实现方式自有切换:

    1. 可以基于简单的Http服务调用;
    2. 可以基于Spring Cloud 微服务架构调用;
    3. 可以基于Dubbo SOA服务治理

    这种模式比较适合在SaSS混合软件服务的模式下自有切换,根据客户的硬件能力选择合适的方式部署,也可以基于自身的服务集群部署微服务

    至于Spring Cloud 是如何实现 协议解析的,可参考代码:
    https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java

    当然,目前的Spring MVC的注解并不是可以完全使用的,有一些注解并不支持,如@GetMapping,@PutMapping 等,仅支持使用@RequestMapping 等,另外注解继承性方面也有些问题;具体限制细节,每个版本能会有些出入,可以参考上述的代码实现,比较简单。

    Spring Cloud 没有基于Spring MVC 全部注解来做Feign 客户端注解协议解析,个人认为这个是一个不小的坑。在刚入手Spring Cloud 的时候,就碰到这个问题。后来是深入代码才解决的… 这个应该有人写了增强类来处理,暂且不表,先MARK一下,是一个开源代码练手的好机会。

    PHASE 3. 基于 RequestBean,动态生成Request

    根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request 对象:
    在这里插入图片描述

    PHASE 4. 使用Encoder 将Bean转换成 Http报文正文(消息解析和转码逻辑)

    Feign 最终会将请求转换成Http 消息发送出去,传入的请求对象最终会解析成消息体,如下所示:
    在这里插入图片描述

    在接口定义上Feign做的比较简单,抽象出了Encoder 和decoder 接口:

    public interface Encoder {
      /** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */
      Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
    
      /**
       * Converts objects to an appropriate representation in the template.
       *  将实体对象转换成Http请求的消息正文中
       * @param object   what to encode as the request body.
       * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD}
       *                 indicates form encoding.
       * @param template the request template to populate.
       * @throws EncodeException when encoding failed due to a checked exception.
       */
      void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
    
      /**
       * Default implementation of {@code 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()));
          }
        }
      }
    }
    
    public interface Decoder {
    
      /**
       * Decodes an http response into an object corresponding to its {@link
       * java.lang.reflect.Method#getGenericReturnType() generic return type}. If you need to wrap
       * exceptions, please do so via {@link DecodeException}.
       *  从Response 中提取Http消息正文,通过接口类声明的返回类型,消息自动装配
       * @param response the response to decode 
       * @param type     {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of
       *                 the method corresponding to this {@code response}.
       * @return instance of {@code type}
       * @throws IOException     will be propagated safely to the caller.
       * @throws DecodeException when decoding failed due to a checked exception besides IOException.
       * @throws FeignException  when decoding succeeds, but conveys the operation failed.
       */
      Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
    
      /** Default implementation of {@code Decoder}. */
      public class Default extends StringDecoder {
    
        @Override
        public Object decode(Response response, Type type) throws IOException {
          if (response.status() == 404) 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);
        }
      }
    }
    
    

    目前Feign 有以下实现:

    Encoder/ Decoder 实现说明
    JacksonEncoder,JacksonDecoder基于 Jackson 格式的持久化转换协议
    GsonEncoder,GsonDecoder基于Google GSON 格式的持久化转换协议
    SaxEncoder,SaxDecoder基于XML 格式的Sax 库持久化转换协议
    JAXBEncoder,JAXBDecoder基于XML 格式的JAXB 库持久化转换协议
    ResponseEntityEncoder,ResponseEntityDecoderSpring MVC 基于 ResponseEntity< T > 返回格式的转换协议
    SpringEncoder,SpringDecoder基于Spring MVC HttpMessageConverters 一套机制实现的转换协议 ,应用于Spring Cloud 体系中
    PHASE 5. 拦截器负责对请求和返回进行装饰处理

    在请求转换的过程中,Feign 抽象出来了拦截器接口,用于用户自定义对请求的操作:

    public interface RequestInterceptor {
    
      /**
       * 可以在构造RequestTemplate 请求时,增加或者修改Header, Method, Body 等信息
       * Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
       */
      void apply(RequestTemplate template);
    }
    

    比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器:

    public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
    
    	/**
    	 * Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
    	 *
    	 * @param properties the encoding properties
    	 */
    	protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
    		super(properties);
    	}
    
    	/**
    	 * {@inheritDoc}
    	 */
    	@Override
    	public void apply(RequestTemplate template) {
    		//  在Header 头部添加相应的数据信息
    		addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
    				HttpEncoding.DEFLATE_ENCODING);
    	}
    }
    
    PHASE 6. 日志记录

    在发送和接收请求的时候,Feign定义了统一的日志门面来输出日志信息 , 并且将日志的输出定义了四个等级:

    级别说明
    NONE不做任何记录
    BASIC只记录输出Http 方法名称、请求URL、返回状态码和执行时间
    HEADERS记录输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息
    FULL记录Request 和Response的Header,Body和一些请求元数据
    public abstract class Logger {
    
      protected static String methodTag(String configKey) {
        return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('(')))
            .append("] ").toString();
      }
    
      /**
       * Override to log requests and responses using your own implementation. Messages will be http
       * request and response text.
       *
       * @param configKey value of {@link Feign#configKey(Class, java.lang.reflect.Method)}
       * @param format    {@link java.util.Formatter format string}
       * @param args      arguments applied to {@code format}
       */
      protected abstract void log(String configKey, String format, Object... args);
    
      protected void logRequest(String configKey, Level logLevel, Request request) {
        log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url());
        if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
    
          for (String field : request.headers().keySet()) {
            for (String value : valuesOrEmpty(request.headers(), field)) {
              log(configKey, "%s: %s", field, value);
            }
          }
    
          int bodyLength = 0;
          if (request.body() != null) {
            bodyLength = request.body().length;
            if (logLevel.ordinal() >= Level.FULL.ordinal()) {
              String
                  bodyText =
                  request.charset() != null ? new String(request.body(), request.charset()) : null;
              log(configKey, ""); // CRLF
              log(configKey, "%s", bodyText != null ? bodyText : "Binary data");
            }
          }
          log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
        }
      }
    
      protected void logRetry(String configKey, Level logLevel) {
        log(configKey, "---> RETRYING");
      }
    
      protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response,
                                                long elapsedTime) throws IOException {
        String reason = response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ?
            " " + response.reason() : "";
        int status = response.status();
        log(configKey, "<--- HTTP/1.1 %s%s (%sms)", status, reason, elapsedTime);
        if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
    
          for (String field : response.headers().keySet()) {
            for (String value : valuesOrEmpty(response.headers(), field)) {
              log(configKey, "%s: %s", field, value);
            }
          }
    
          int bodyLength = 0;
          if (response.body() != null && !(status == 204 || status == 205)) {
            // HTTP 204 No Content "...response MUST NOT include a message-body"
            // HTTP 205 Reset Content "...response MUST NOT include an entity"
            if (logLevel.ordinal() >= Level.FULL.ordinal()) {
              log(configKey, ""); // CRLF
            }
            byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            bodyLength = bodyData.length;
            if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
              log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
            }
            log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
            return response.toBuilder().body(bodyData).build();
          } else {
            log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
          }
        }
        return response;
      }
    
      protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
        log(configKey, "<--- ERROR %s: %s (%sms)", ioe.getClass().getSimpleName(), ioe.getMessage(),
            elapsedTime);
        if (logLevel.ordinal() >= Level.FULL.ordinal()) {
          StringWriter sw = new StringWriter();
          ioe.printStackTrace(new PrintWriter(sw));
          log(configKey, sw.toString());
          log(configKey, "<--- END ERROR");
        }
        return ioe;
      }
    
    PHASE 7 . 基于重试器发送HTTP请求

    Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求,以下是Feign核心
    代码逻辑:

    final class SynchronousMethodHandler implements MethodHandler {
    
      // 省略部分代码
    
      @Override
      public Object invoke(Object[] argv) throws Throwable {
       //根据输入参数,构造Http 请求。
        RequestTemplate template = buildTemplateFromArgs.create(argv);
        // 克隆出一份重试器
        Retryer retryer = this.retryer.clone();
        // 尝试最大次数,如果中间有结果,直接返回
        while (true) {
          try {
            return executeAndDecode(template);
          } catch (RetryableException e) {
            retryer.continueOrPropagate(e);
            if (logLevel != Logger.Level.NONE) {
              logger.logRetry(metadata.configKey(), logLevel);
            }
            continue;
          }
        }
      }
    

    重试器有如下几个控制参数:

    重试参数说明默认值
    period初始重试时间间隔,当请求失败后,重试器将会暂停 初始时间间隔(线程 sleep 的方式)后再开始,避免强刷请求,浪费性能100ms
    maxPeriod当请求连续失败时,重试的时间间隔将按照:long interval = (long) (period * Math.pow(1.5, attempt - 1)); 计算,按照等比例方式延长,但是最大间隔时间为 maxPeriod, 设置此值能够避免 重试次数过多的情况下执行周期太长1000ms
    maxAttempts最大重试次数5

    具体的代码实现可参考:
    https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java

    PHASE 8. 发送Http请求

    Feign 真正发送HTTP请求是委托给 feign.Client 来做的:

    public interface Client {
    
      /**
       * Executes a request against its {@link Request#url() url} and returns a response.
       *  执行Http请求,并返回Response
       * @param request safe to replay.
       * @param options options to apply to this request.
       * @return connected response, {@link Response.Body} is absent or unread.
       * @throws IOException on a network error connecting to {@link Request#url()}.
       */
      Response execute(Request request, Options options) throws IOException;
      }
    

    Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端,我们项目内部使用的就是OkHttp3作为Http 客户端。

    如下是Feign 的默认实现,供参考:

    public static class Default implements Client {
    
        private final SSLSocketFactory sslContextFactory;
        private final HostnameVerifier hostnameVerifier;
    
        /**
         * Null parameters imply platform defaults.
         */
        public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
          this.sslContextFactory = sslContextFactory;
          this.hostnameVerifier = hostnameVerifier;
        }
    
        @Override
        public Response execute(Request request, Options options) throws IOException {
          HttpURLConnection connection = convertAndSend(request, options);
          return convertResponse(connection).toBuilder().request(request).build();
        }
    
        HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
          final HttpURLConnection
              connection =
              (HttpURLConnection) new URL(request.url()).openConnection();
          if (connection instanceof HttpsURLConnection) {
            HttpsURLConnection sslCon = (HttpsURLConnection) connection;
            if (sslContextFactory != null) {
              sslCon.setSSLSocketFactory(sslContextFactory);
            }
            if (hostnameVerifier != null) {
              sslCon.setHostnameVerifier(hostnameVerifier);
            }
          }
          connection.setConnectTimeout(options.connectTimeoutMillis());
          connection.setReadTimeout(options.readTimeoutMillis());
          connection.setAllowUserInteraction(false);
          connection.setInstanceFollowRedirects(true);
          connection.setRequestMethod(request.method());
    
          Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
          boolean
              gzipEncodedRequest =
              contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
          boolean
              deflateEncodedRequest =
              contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);
    
          boolean hasAcceptHeader = false;
          Integer contentLength = null;
          for (String field : request.headers().keySet()) {
            if (field.equalsIgnoreCase("Accept")) {
              hasAcceptHeader = true;
            }
            for (String value : request.headers().get(field)) {
              if (field.equals(CONTENT_LENGTH)) {
                if (!gzipEncodedRequest && !deflateEncodedRequest) {
                  contentLength = Integer.valueOf(value);
                  connection.addRequestProperty(field, value);
                }
              } else {
                connection.addRequestProperty(field, value);
              }
            }
          }
          // Some servers choke on the default accept string.
          if (!hasAcceptHeader) {
            connection.addRequestProperty("Accept", "*/*");
          }
    
          if (request.body() != null) {
            if (contentLength != null) {
              connection.setFixedLengthStreamingMode(contentLength);
            } else {
              connection.setChunkedStreamingMode(8196);
            }
            connection.setDoOutput(true);
            OutputStream out = connection.getOutputStream();
            if (gzipEncodedRequest) {
              out = new GZIPOutputStream(out);
            } else if (deflateEncodedRequest) {
              out = new DeflaterOutputStream(out);
            }
            try {
              out.write(request.body());
            } finally {
              try {
                out.close();
              } catch (IOException suppressed) { // NOPMD
              }
            }
          }
          return connection;
        }
    
        Response convertResponse(HttpURLConnection connection) 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<String, Collection<String>>();
          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;
          if (status >= 400) {
            stream = connection.getErrorStream();
          } else {
            stream = connection.getInputStream();
          }
          return Response.builder()
                  .status(status)
                  .reason(reason)
                  .headers(headers)
                  .body(stream, length)
                  .build();
        }
      }
    

    Feign 的性能怎么样?

    Feign 整体框架非常小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。
    如上所述,由于默认情况下,Feign采用的是JDK的HttpURLConnection,所以整体性能并不高,刚开始接触Spring Cloud 的同学,如果没注意这些细节,可能会对Spring Cloud 有很大的偏见。
    我们项目内部使用的是OkHttp3 作为连接客户端。
    系统的压测方案后续在贴出来,有兴趣的同学可以持续关注~

    另外作者已开通微信订阅号,精品文章同步更新,欢迎关注~
    亦山札记公众号

    展开全文
  • Spring Cloud Feign相关原理分析

    千次阅读 2019-05-24 12:31:49
    1.认识Spring Cloud Feign 在使用Spring Cloud构建的微服务架构中,Feign用来提供声明式的服务客户端定义方式。 Feign是一个伪客户端(不做任何的请求处理),它通过处理注解生成request,以此来简化HTTP API的开发。 ...

    1.认识Spring Cloud Feign
    在使用Spring Cloud构建的微服务架构中,Feign用来提供声明式的服务客户端定义方式。
    Feign是一个伪客户端(不做任何的请求处理),它通过处理注解生成request,以此来简化HTTP API的开发。

    2.定义的服务接口是如何注册到Spring容器的?
    这里我们需要关注两个注解:
    @FeignClient:使用在我们定义的服务接口上,表示该接口是一个Feign客户端;
    @EnableFeignClients:使用在我们具体服务应用的启动类上。

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
      //...
    }

    @EnableFeignClients注解导入了FeignClientsRegistrar类,该类的核心方法如下:
    public void registerBeanDefinitions(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }

    public void registerFeignClients(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);

        Set<String> basePackages;

        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
                FeignClient.class);

        final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            scanner.addIncludeFilter(annotationTypeFilter);
            basePackages = getBasePackages(metadata);
        }
        else {
            final Set<String> clientClasses = new HashSet<>();
            basePackages = new HashSet<>();
            for (Class<?> clazz : clients) {
                basePackages.add(ClassUtils.getPackageName(clazz));
                clientClasses.add(clazz.getCanonicalName());
            }
            AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
                @Override
                protected boolean match(ClassMetadata metadata) {
                    String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                    return clientClasses.contains(cleaned);
                }
            };
            scanner.addIncludeFilter(
                    new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
        }

        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    // verify annotated class is an interface
                    AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                    AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                    Assert.isTrue(annotationMetadata.isInterface(),
                            "@FeignClient can only be specified on an interface");

                    Map<String, Object> attributes = annotationMetadata
                            .getAnnotationAttributes(
                                    FeignClient.class.getCanonicalName());

                    String name = getClientName(attributes);
                    registerClientConfiguration(registry, name,
                            attributes.get("configuration"));

                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }

    private void registerFeignClient(BeanDefinitionRegistry registry,
                AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        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();

        boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null

        beanDefinition.setPrimary(primary);

        String qualifier = getQualifier(attributes);
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                new String[] { alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

    通过分析源码,我们可以知道,@EnableFeignClients注解的作用就是通过FeignClientsRegistrar,最终将使用了@FeignClient注解的接口全部注入到Spring容器中。

    3.Feign是如何进行服务调用的?

    a.Feign.class的newInstance(Target<T> target)方法将会为被@FeignClient注解的接口生成一个代理的实例
    /**
    * Returns a new instance of an HTTP API, defined by annotations in the {@link Feign Contract},
    * for the specified {@code target}. You should cache this result.
    */
    public abstract <T> T newInstance(Target<T> target);

    Feign.class的实现类ReflectiveFeign.class具体实现了这个方法,它的newInstance方法为指定的Feign接口生成代理对象
    /**
    * creates an api binding to the {@code target}. As this invokes reflection, care should be taken
    * to cache the result.
    */
    @SuppressWarnings("unchecked")
    @Override
    public <T> T newInstance(Target<T> target) {
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

        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)));
          }
        }
        //将要被代理的方法全部在methodToHandler中;factory.create()默认返回的就是ReflectiveFeign.FeignInvocationHandler对象
        InvocationHandler handler = factory.create(target, methodToHandler);
        //使用jdk的动态代理为指定的Feign接口生成代理对象
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
            new Class<?>[] {target.type()}, handler);

        for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
          defaultMethodHandler.bindTo(proxy);
        }
        return proxy;
    }

    b.代理对象把功能的实现委托给MethodHandler接口的invoke()方法
    ReflectiveFeign.FeignInvocationHandler对象的invoke方法:
    @Override
    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();
      }

      //也就是MethodHandler.invoke方法   其实现类SynchronousMethodHandler.class提供了具体的invoke方法
      return dispatch.get(method).invoke(args);
    }

    c.SynchronousMethodHandler类进行拦截处理,根据传入参数构建http请求模板,把具体的请求委托给专门发送网络请求的类去具体执行
    @Override
    public Object invoke(Object[] argv) throws Throwable {
        //根据参数构建http请求模板
        RequestTemplate template = buildTemplateFromArgs.create(argv);
        Retryer retryer = this.retryer.clone();
        while (true) {
          try {
            return executeAndDecode(template);
          } 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;
          }
        }
    }

    Object executeAndDecode(RequestTemplate template) throws Throwable {
        Request request = targetRequest(template);

        if (logLevel != Logger.Level.NONE) {
          logger.logRequest(metadata.configKey(), logLevel, request);
        }

        Response response;
        long start = System.nanoTime();
        try {
          //Client对象真正执行发送请求的任务
          response = client.execute(request, options);
        } 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);

        boolean shouldClose = true;
        try {
          if (logLevel != Logger.Level.NONE) {
            response =
                logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
          }
          if (Response.class == metadata.returnType()) {
            if (response.body() == null) {
              return response;
            }
            if (response.body().length() == null ||
                response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
              shouldClose = false;
              return response;
            }
            // Ensure the response body is disconnected
            byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            return response.toBuilder().body(bodyData).build();
          }
          if (response.status() >= 200 && response.status() < 300) {
            if (void.class == metadata.returnType()) {
              return null;
            } else {
              Object result = decode(response);
              shouldClose = closeAfterDecode;
              return result;
            }
          } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
            Object result = decode(response);
            shouldClose = closeAfterDecode;
            return result;
          } else {
            throw errorDecoder.decode(metadata.configKey(), response);
          }
        } catch (IOException e) {
          if (logLevel != Logger.Level.NONE) {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
          }
          throw errorReading(request, response, e);
        } finally {
          if (shouldClose) {
            ensureClosed(response.body());
          }
        }
      }
      
    d.Client组件最终发送request请求以及接收response响应
    Client的实现类默认是Client.Default,该类由HttpURLConnnection实现网络请求,另外还支持ApacheHttpClient、Okhttp.
    @Configuration
    class DefaultFeignLoadBalancedConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                                  SpringClientFactory clientFactory) {
            return new LoadBalancerFeignClient(new Client.Default(null, null),
                    cachingFactory, clientFactory);
        }
    }

    4.Feign请求的负载均衡是如何实现的?

    a.由FeignRibbonClientAutoConfiguration.class自动配置类可知,向容器注入的是LoadBalancerFeignClient(这个类与Ribbon研究中的LoadBalancerClient类在功能上有相似性),即负载均衡客户端。
    @Configuration
    class DefaultFeignLoadBalancedConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                                  SpringClientFactory clientFactory) {
            return new LoadBalancerFeignClient(new Client.Default(null, null),
                    cachingFactory, clientFactory);
        }
    }

    b.当Client组件发送request请求之前,也就是执行Client.execute()方法时,具体的请求会被LoadBalancerFeignClient处理。LoadBalancerFeignClient接着又把处理逻辑交给AbstractLoadBalancerAwareClient的executeWithLoadBalancer()方法.
    org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient的execute()方法:
    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        try {
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                    this.delegate, request, uriWithoutHost);

            IClientConfig requestConfig = getClientConfig(options, clientName);
            return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
                    requestConfig).toResponse();
        }
        catch (ClientException e) {
            IOException io = findIOException(e);
            if (io != null) {
                throw io;
            }
            throw new RuntimeException(e);
        }
    }

    c.AbstractLoadBalancerAwareClient的executeWithLoadBalancer()方法把负载均衡的任务交给LoadBalancerCommand的submit方法处理
    com.netflix.client.AbstractLoadBalancerAwareClient的executeWithLoadBalancer()方法:
    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        try {
            return command.submit(
                new ServerOperation<T>() {
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ClientException) {
                throw (ClientException) t;
            } else {
                throw new ClientException(e);
            }
        }
        
    }

    d.LoadBalancerCommand的submit方法委托LoadBalancerContext.getServerFromLoadBalancer从负载均衡器里面获取一个服务实例
    com.netflix.loadbalancer.reactive.LoadBalancerCommand.class
    private Observable<Server> selectServer() {
        return Observable.create(new OnSubscribe<Server>() {
            @Override
            public void call(Subscriber<? super Server> next) {
                try {
                    //这很关键,委托负载均衡器取得一个Server实例
                    Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                    next.onNext(server);
                    next.onCompleted();
                } catch (Exception e) {
                    next.onError(e);
                }
            }
        });
    }

    public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
            //...
            ILoadBalancer lb = getLoadBalancer();
            if (host == null) {
                // Partial URI or no URI Case
                // well we have to just get the right instances from lb - or we fall back
                if (lb != null){
                    Server svc = lb.chooseServer(loadBalancerKey);
                }
            //...
    }

    展开全文
  • 本篇文章仅介绍Feign的核心机制,包括如何交由Spring容器托管、动态代理机制等内容,不会...其实,Feign底层依赖于Java的动态代理机制,对原生Java Socket或者Apache HttpClient进行封装,实现了基于Http协议的远程.

    本篇文章仅介绍Feign的核心机制,包括如何交由Spring容器托管、动态代理机制等内容,不会过分深入细节。

    1、什么是Feign?

    这里套用Feign官方Github上的介绍:“Feign是一个灵感来自于Retrofit、JAXRS-2.0、WebSocket的Java Http客户端,Feign的主要目标是降低大家使用Http API的复杂性”。

    其实,Feign底层依赖于Java的动态代理机制,对原生Java Socket或者Apache HttpClient进行封装,实现了基于Http协议的远程过程调用。当然,Feign还在此基础上实现了负载均衡、熔断等机制。

    2、为什么要使用Feign?

    • 声明式Http Client相对于编程式Http Client代码逻辑更加简洁,不需要处理复杂的编码请求和响应,只需要像调用本地方法即可,提高编码效率
    • 集中管理Http请求方法,代码边界更加清晰
    • 更好的集成负载均衡、熔断降级等功能

    3、Feign依赖注入原理

    使用过Feign的同学都知道,@EnableFeignClients注解是开启Fiegn功能的关键,我们通常会在该注解中添加FeignClient的所在包,以便Spring容器能够扫描到所有的FeignClient,并进行托管。后面我们便可以使用@Autowired注解自动导入了。

    @SpringBootApplication
    @EnableFeignClients(basePackages = {"com.**.feign"})
    public class Application {}
    

    该注解样式也是很多第三方包集成Springboot所使用的套路:一般都是开启该注解后,Springboot便可以自动装载第三方包所指定的Class,我们便可以直接使用第三方包所提供的功能,非常方便。

    接下来不会详细介绍自动装载的部分,而是直接给出自动装载的主脉络,看看Spring容器到底装载了什么bean。

    3.1、Feign自动装载

    首先进入@EnableFeignClients源码中,查看该注解导入了什么Registrar注册器,这个注册器便是自动装载的关键。

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {...}
    

    从源码中可以看出,@EnableFeignClients 注解导入的是自定义的FeignClientsRegistrar类。

    这种类型的注册器一般会继承Spring中的ImportBeanDefinitionRegistrar接口,并在registerBeanDefinitions实现方法中向Spring容器注册一些bean,以达到自动注入第三方功能的目的。

    // [1] 继承ResourceLoaderAware和EnvironmentAware
    class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            // [2] 注册默认的Feign配置
            registerDefaultConfiguration(metadata, registry);
            // [3] 注册所有定义的FeignClient
            registerFeignClients(metadata, registry);
        }
    }
    
    • [1] 从类的定义中,我们可以发现还实现了ResourceLoaderAware、EnvironmentAware两个Spring钩子接口,那么该注册类必然持有资源加载器和Spring的环境变量等信息,这个不过多叙述。
    • [2] 该方法会从@EnableFeignClients注解中提取defaultConfiguration这个key和对应的value,并把它当作默认的Feign配置注册到Spring容器中。如果没有该key,则不做任何处理。
    • [3] registerFeignClients方法会扫描@EnableFeignClients注解的basePackages,注册所有的FeignClient,下面详细介绍。
    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
        Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
        // [1] 获取clients属性
        final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            // [2] 如果clients属性为null,则获取basePackages属性,扫描其中的所有client
            ClassPathScanningCandidateComponentProvider scanner = getScanner();
            scanner.setResourceLoader(this.resourceLoader);
            scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
            Set<String> basePackages = getBasePackages(metadata);
            for (String basePackage : basePackages) {
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        } else {
            // [3] 如果clients属性不为null,则直接注入
            for (Class<?> clazz : clients) {
                candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
            }
        }
        // [4] 遍历所有的clients,将其封装为BeanDefinition注册进Spring容器中
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                // verify annotated class is an interface
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
        
                Map<String, Object> attributes = annotationMetadata
                    .getAnnotationAttributes(FeignClient.class.getCanonicalName());
        
                String name = getClientName(attributes);
                registerClientConfiguration(registry, name, attributes.get("configuration"));
        
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
    

    registerFeignClients方法的代码稍微有点多,但主要思路就是解析@EnableFeignClients注解,根据解析信息获取FeignClients,然后包装成BeanDefinition注册进Spring容器中。具体分为以下几步:

    • [1] 首先解析@EnableFeignClients注解中的clients信息,如果存在,说明开发人员直接指定了FeignClient的全路径,因此只要加载这些全路径的class即可。如果未指定,则通过扫包的方式加载。
    • [2] 如果clients属性为null,则创建一个扫描器Scanner,并指定要扫描的类必须有FeignClient注解,然后通过getBasePackages()方法从@EnableFeignClients注解中获取basePackages信息,最后遍历所有待扫描的包,将扫描到的FeignClient类加入candidateComponents中,待后续加载进容器。
    • [3] 如果clients属性不为null,上面也说了,会直接注入
    • [4] 到了这里,所有的FeignClient都被扫描到并且封装成BeanDefinition,接下来会遍历这些BeanDefinition,然后在Assert.isTrue()方法中判断这些BeanDefinition是否为接口,因为@FeignClient注解只能使用在接口上。校验完后我们应该关注registerFeignClient()这个真正注册FeignClient的方法。
    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
        Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        Class clazz = ClassUtils.resolveClassName(className, null);
        ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
            ? (ConfigurableBeanFactory) registry : null;
        String contextId = getContextId(beanFactory, attributes);
        String name = getName(attributes);
        FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
        factoryBean.setBeanFactory(beanFactory);
        factoryBean.setName(name);
        factoryBean.setContextId(contextId);
        factoryBean.setType(clazz);
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
            factoryBean.setUrl(getUrl(beanFactory, attributes));
            factoryBean.setPath(getPath(beanFactory, attributes));
            factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
            Object fallback = attributes.get("fallback");
            if (fallback != null) {
              factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
                  : ClassUtils.resolveClassName(fallback.toString(), null));
            }
            Object fallbackFactory = attributes.get("fallbackFactory");
            if (fallbackFactory != null) {
              factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
                  : ClassUtils.resolveClassName(fallbackFactory.toString(), null));
            }
            return factoryBean.getObject();
        });
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        definition.setLazyInit(true);
        validate(attributes);
      
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
        beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
      
        // has a default, won't be null
        boolean primary = (Boolean) attributes.get("primary");
      
        beanDefinition.setPrimary(primary);
      
        String[] qualifiers = getQualifiers(attributes);
        if (ObjectUtils.isEmpty(qualifiers)) {
          qualifiers = new String[] { contextId + "FeignClient" };
        }
      
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }
    

    registerFeignClient()是对单个FeignClient注册的方法。这个方法乍一看很长,但其实主线非常清晰。

    就是将扫描到的BeanDefinition中的元信息提取出来,然后构造成一个Feign自定义的FactoryBean,即FeignClientFactoryBean,后面我们每次获取容器中的FeignClient时,就会通过该FactoryBean的getObject()方法中获取(这个涉及到了Spring容器中普通bean和FactoryBean的区别,大家可以自行去了解下)。

    方法中的其它部分都是为上面所说的逻辑服务,包括FactoryBean的构造,BeanDefinition注入到容器中等过程。我们不必太过关心,只要抓住重点即可。

    讲到这里,其实自动装载过程已经完成了,容器中已经包含了自定义的FeignClientFactoryBean。这里用一张流程图总结下自动装载的全过程。

    在这里插入图片描述
    但是我们暂时还不知道自动装载的FeignClientFactoryBean到底做了什么。下面我们就深入去了解。

    3.2、FeignClientFactoryBean#getObject

    FeignClientFactoryBean的源码如下:

    public class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
        @Override
        public Object getObject() {
            // [1] 真正的获取对象的方法委托给getTarget()方法了
            return getTarget();
        }
        <T> T getTarget() {
            // 获取Feign的上下文
            FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
                : applicationContext.getBean(FeignContext.class);
            Feign.Builder builder = feign(context);
            if (!StringUtils.hasText(url)) {
                // [2] 如果url没有定义,则进入到该判断中创建对象,该判断中创建的对象具有负载均衡功能
                if (!name.startsWith("http")) {
                    url = "http://" + name;
                }
                else {
                    url = name;
                }
                url += cleanPath();
                return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
            }
            if (StringUtils.hasText(url) && !url.startsWith("http")) {
                url = "http://" + url;
            }
            // [3] 如果url定义了,说明用户指定了某台机器,也就没有必要进行负载均衡了,则从下面的方法创建对象
            String url = this.url + cleanPath();
            // 可以发现下面的执行逻辑和loadBalance()非常像,只是多了两个if判断,这两个if判断就是移除负载均衡的关键
            Client client = getOptional(context, Client.class);
            if (client != null) {
                if (client instanceof FeignBlockingLoadBalancerClient) {
                    // not load balancing because we have a url,
                    // but Spring Cloud LoadBalancer is on the classpath, so unwrap
                    client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
                }
                if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
                    // not load balancing because we have a url,
                    // but Spring Cloud LoadBalancer is on the classpath, so unwrap
                    client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
                }
                builder.client(client);
            }
            Targeter targeter = get(context, Targeter.class);
            return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
        }
    }
    // 创建负载均衡的client
    protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
        Client client = getOptional(context, Client.class);
        if (client != null) {
            builder.client(client);
            Targeter targeter = get(context, Targeter.class);
            return targeter.target(this, builder, context, target);
        }
      
        throw new IllegalStateException(
            "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?");
    }
    
    

    getObject()会根据用户定义的FeignClient是否定义url属性决定是否返回具有负载均衡属性的对象。具体过程如下:

    • [1] 重写的方法委托给getTarget()执行了
    • [2] 如果没有定义url,则直接通过loadBalance()方法创建代理对象。此时会先从容器中拿到Client对象,然后使用targeter.target()方法创建Client的代理对象。注意,我们最终使用的FeignClient是Client的动态代理对象,而Client对象是真正执行http请求的对象。Client一般是httpClient或者是Feign自定义的具有LoadBalance功能的LoadBalancerClient。前者很好理解,可以直接认为是Apache HttpClient;后者是Feign在Apache HttpClient的基础上封装了Spring Cloud Loadbalancer一系列对象,而Apache HttpClient作为被封装的delegate,在delegate真正执行http请求时同时进行Loadbalancer的负载均衡逻辑。
    • [3] 如果url已经定义了,说明用户指定了具体某台机器,此时已经没有必要进行负载均衡了(当然,如果配置的是域名,可能会由下游ng或者网关层进行负载均衡,这里说的是Feign没有必要负载均衡)。为了移除负载均衡的功能,这里比loadBalance()方法多了client的判断,如果client是FeignBlockingLoadBalancerClient或者RetryableFeignBlockingLoadBalancerClient,会直接代理这两个对象中的delegate对象,即直接代理Apache HttpClient,这样就能移除其中的负载均衡功能了。

    从上面的源码中可知,无论是否需要负载均衡,都会通过targeter.target()方法创建动态代理对象。我们这里跳过中间不重要的环节,给出targeter.target()不太重要的调用栈,大家可以自行查看:Targeter.target()→DefaultTargeter.target()→Feign.Builder.target()→Feign.newInstance()→ReflectiveFeign.newInstance()

    接下来我们来到了ReflectiveFeign.newInstance()这个重要的方法:

    @Override
    public <T> T newInstance(Target<T> target) {
        // [1] nameToHandler 里面基本上是SynchronousMethodHandler,主要用于处理用户自定义的方法
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
        // [2] DefaultMethodHandler用于处理接口中default方法
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
      
        // 遍历接口中的所有方法
        for (Method method : target.type().getMethods()) {
            if (method.getDeclaringClass() == Object.class) {
                // [3] 如果是Object中的方法,直接跳过
                continue;
            } else if (Util.isDefault(method)) {
                // [4] 如果是default方法,则创建DefaultMethodHandler处理
                DefaultMethodHandler handler = new DefaultMethodHandler(method);
                defaultMethodHandlers.add(handler);
                methodToHandler.put(method, handler);
            } else {
                // [5] 其它的都是用户自定义的方法了,此时从nameToHandler中拿出SynchronousMethodHandler进行映射
                methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
            }
        }
        // [6] 这个就是动态代理的关键了,代理逻辑都在该handler中了
        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;  
    }
    

    从newInstance中我们看到了熟悉的proxy和InvocationHandler,这也就说明了Feign的底层还是依赖了JDK的动态代理:

    • [1] 这里会通过targetToHandlersByName.apply()方法创建configKey→SynchronousMethodHandler的映射
    • [2] defaultMethodHandlers用于存储处理用户定义的FeignClient接口中的default方法的handler
    • [3] 如果是Object方法,这里直接跳过了,因为会在后面的InvocationHandler中处理Object方法的代理逻辑
    • [4] 如果是接口中的default方法,则创建DefaultMethodHandler并添加进defaultMethodHandlers列表和methodToHandler 映射中
    • [5] 创建method→SynchronousMethodHandler的映射
    • [6] 创建InvocationHandler 核心代理对象,代理逻辑都封装在该对象中。注意,这里传递了methodToHandler(method→MethodHandler)这个映射,MethodHandler可能是DefaultMethodHandler(处理default方法)或者SynchronousMethodHandler(处理用户定义的远程调用方法)。代理过程中,会根据方法名称dispatch到这个映射中对应的MethodHandler进行处理。

    后面就是创建代理对象并返回了。

    下面我们来看核心代理逻辑究竟做了什么,InvocationHandler实现类为FeignInvocationHandler,是ReflectiveFeign的静态内部类。

    static class FeignInvocationHandler implements InvocationHandler {
        private final Target target;
        private final Map<Method, MethodHandler> dispatch;
        FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
            this.target = checkNotNull(target, "target");
            // dispatch就是我们上文提到的methodToHandler
            this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 这下面都是判断是否为Object中的方法
            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();
            }
            // 如果是用户自定义的远程调用方法,则执行MethodHandler中的invoke方法
            return dispatch.get(method).invoke(args);
        }
    
        @Override
        public boolean equals(Object obj) {
          if (obj instanceof FeignInvocationHandler) {
              FeignInvocationHandler other = (FeignInvocationHandler) obj;
              return target.equals(other.target);
          }
          return false;
        }
        @Override
        public int hashCode() {
            return target.hashCode();
        }
        @Override
        public String toString() {
            return target.toString();
        }
    }
    

    FeignInvocationHandler 还是非常简单的,dispatch就是我们上文提到的methodToHandler(method→MethodHandler)映射,在执行invoke方法时:

    • 如果是Object方法,则调用重写的方法处理
    • 如果是default方法,则从dispatch映射中获取对应的DefaultMethodHandler.invoke()处理
    • 如果是用户定义的远程调用方法,则从dispatch映射中获取对应的SynchronousMethodHandler.invoke()处理

    这里我们仅关心远程调用的实现机制,因此下面我们将进入到SynchronousMethodHandler中,观察invoke()方法的执行逻辑:

    @Override
    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;
            }
        }
    }
    

    invoke方法将具体的远程调用委托给executeAndDecode()执行,从方法名可知,该方法不仅执行http远程调用,同时还会对response进行节码操作,这也是Feign非常方便的一点,能够让开发者忽略http报文解析的过程。

    invoke还提供了失败重试机制,主要逻辑由Retryer 这个对象实现,感兴趣的小伙伴可以自行了解。

    Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        // 构造请求对象
        Request request = targetRequest(template);
    
        if (logLevel != Logger.Level.NONE) {
            logger.logRequest(metadata.configKey(), logLevel, request);
        }
    
        Response response;
        long start = System.nanoTime();
        try {
            // 执行http请求
            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);
        }
        // ...忽略解码过程
    }
    

    executeAndDecode()方法首先构造请求对象,然后使用client对象发起http请求。之前说了,client可以是Apache HttpClient或者Feign封装的具有负载均衡能力的FeignBlockingLoadBalancerClient或者RetryableFeignBlockingLoadBalancerClient,但这两个client的execute()方法底层最终会调用其中的delegate(即Apache HttpClient)执行http远程调用。

    讲到这里,FeignClientFactoryBean#getObject方法的执行逻辑我们也非常清楚了,就是通过JDK的动态代理,对Apache HttpClient进行多层封装,以实现远程调用的能力。这里也用一张图梳理下整个过程:

    在这里插入图片描述

    4、总结

    本文主要介绍了Feign的自动装载和动态代理机制,并梳理了这两个机制的主要脉络,而忽略其它次要信息。Feign能够被众多开发人员所使用绝不仅仅是具备以上介绍的两个功能,它还提供了诸如负载均衡、熔断等机制,这些也在文章中有少量提及,大家感兴趣的话,可以沿着本文所介绍的主脉络,一一梳理下这些功能。

    展开全文
  • Feign的工作原理

    千次阅读 多人点赞 2020-04-28 18:20:57
    文章目录Feign的简单介绍Feign的工作原理1.创建远程接口的本地代理实例2.封装Request对象并进行编码3.`feign.Client`发送请求并对获取结果进行解码总结 Feign的简单介绍 Feign组件主要用于微服务项目中,用来简化...
  • Feign实现原理

    千次阅读 2019-07-18 17:00:00
    什么是Feign? Feign是一个java的到http客户端绑定的开源项目。 Feign的主要目标是将Java Http 客户端变得简单。Feign的源码地址:...Feign的工作原理 feign是一个伪客户端,即它不做任何的...
  • Feign 原理解析

    2021-10-26 22:34:23
    Feign 原理解析 基本原理 现在已经了解了 Ribbon 的负载均衡原理,我们可以来猜想下,Feign原理,仅仅通过一个注解 @FeignClient + 一个接口,就可以服务之间的调用。 通过 @FeignClient 在注解中的name,确定...
  • Feign工作原理如下: 1、 启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理对象 2、 @FeignClient(value = "XC_SERVICE_MANAGE_CMS")即指定了cms的服务名称,
  • 实战系列-被面试官问到Feign原理

    千次阅读 2020-06-29 17:00:54
    registerFeignClients()方法 内部流转逻辑 FeignInvocationHandler 调用处理器 MethodHandler 方法处理器 SynchronousMethodHandler 同步处理 Feign 客户端组件 feign.Client Client.Default类 ApacheHttpClient类 ...
  • 一、Feign介绍 Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。 Spring Cloud Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix 实现负载均衡和断路...
  • 【第四章】详解Feign的实现原理

    千次阅读 2022-03-24 16:19:25
    这篇文章主要讲述如何通过Feign去消费服务,以及Feign的实现原理的解析。 Feign是Netflix开发的声明式、模板化的HTTP客户端,Feign可以帮助我们更快捷、优雅地调用HTTP API。 Feign是⼀个HTTP请求的轻量级客户端框架...
  • 三、Spring Cloud核心组件:Feign 四、Spring Cloud核心组件:Ribbon 五、Spring Cloud核心组件:Hystrix 六、Spring Cloud核心组件:Zuul 七、总结 概述 毫无疑问,Spring Cloud是目前微服务架构领域的翘楚,...
  • Spring Cloud OpenFeign 工作原理解析

    万次阅读 多人点赞 2020-05-07 18:05:45
    使用方式 在介绍 OpenFeign 的工作原理之前, 首先值得说明的是使用了 Open Feign 后, 开发人员的效率是如何得到提升的。 下面展示在使用了 OpenFeign 之后, 一个接口的提供方和消费方是如何快速高效地完成代码...
  • 《深入理解Spring Cloud与微服务构建》学习笔记(十二)~写一个Feign客户端,可以直接运行。
  • Feign实现服务之间调用的内部原理

    千次阅读 2020-10-14 21:31:30
    目录Feign 的调用过程服务调用传递复杂数据处理 Feign 的调用过程 /** * Feign 的调用过程 SynchronousMethodHandler对象 * 1、构造请求数据,将数据转化成为json * RequestTemplate template = ...
  • Feign的调用原理及其源码分析目录概述架构特性设计思路实现思路分析Feign是如何进行服务调用的拓展实现相关工具如下:实验效果:(解决思路)分析:小结:参考资料和推荐阅读 LD is tigger forever,CG are not ...
  • 超详细的SpringCloud底层原理

    万次阅读 多人点赞 2019-10-03 15:21:40
    3、业务集群,这一层我有些项目是分两层的,就是上面加了一个负载层,下面是从service开始的,底层只是单纯的接口,controller是单独一层由feign实现,然后内部不同业务服务接口互调,直接调用controller层,只能说...
  • SpringCloud底层原理

    2021-02-24 22:20:58
    本文先从最核心的几个组件,也就是Eureka、Ribbon、Feign、Hystrix、Zuul入手,来剖析其底层的工作原理。先来给大家说一个业务场景,假设咱们现在开发一个电商网站,要实现支付订单的功能。流程如下:创建一个订单后...
  • Feign的工作原理 主程序入口添加了@EnableFeignClients注解开启对FeignClient扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClientd注解。 当程序启动时,会进行包扫描,扫描所有@FeignClients的...
  • springclound 之feign原理源码解析及使用详解 目录 feign作用 feign是一款基于注解和动态代理实现的声明式restful http客户端,它提供了类似retrofit、 jaxrs的api和使用方式,但他更加简洁、扩展性也很强,能...
  • 今天在写业务的时候,需要通过feign调用远程接口,平常只是调用就行了,没有了解到他是如何代码实现的,今天就使用debug来解开feign为什么可以远程调用的面纱。 希望看本文章的同学朋友们,可以自己写一个简单的远程...
  • Spring Cloud源码解析 什么是Feign feign 是一种声明式的web 客户端,可以使用它的注解创建接口,它也支持 ... feign底层是使用了ribbon作为负载均衡的客户端,而ribbon的负载均衡也是依赖于eureka 获得各个服...
  • SpringCloud Alibaba Feign 核心原理以及优化方法

    多人点赞 热门讨论 2022-03-28 17:07:53
    1、Feign介绍 ...2、Feign工作原理 Feign远程调用流程图 整个流程步骤: 开始调用方法。 由动态代理Target接管方法运行。 Contract根据注解,取得MethodHandler列表。 执行Request相关的Metho...
  • Feign使用原理

    2021-02-05 17:18:58
    Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign全给你干了。 那么问题来了,Feign是如何做到这么神奇的呢?很简单,...
  • Feign原理 (图解)

    万次阅读 多人点赞 2020-07-28 21:17:51
    1.1简介:Feign远程调用的 Feign远程调用,核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的请求的响应结果,解码成JAVA Bean,放回给调用...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,936
精华内容 1,974
关键字:

feign底层原理

友情链接: LSB.zip