Spring-Cloud之Feign原理剖析


Feign 主要是帮助我们方便进行rest api服务间的调用,其大体实现思路就我们通过标记注解在一个接口类上(注解上将包含要调用的接口信息),之后在调用时根据注解信息组装好请求信息,接下来基于ribbon这些负载均衡器来生成真实的服务地址,最后将请求发送出去;之后将接收到的结果反序列化为相关的Java对象供我们直接使用。 下面我们走进Spring Cloud对feign封装的源码中去了解其主要实现机制。

相关文档:

feign的基本使用
参考文档
feign-core源码
spring-cloud-openfeign源码

Feign的大体机制

通过在启动类上标记 @EnableFeignClients 注解来开启feign的功能,服务启动后会扫描 @FeignClient 注解标记的接口,然后根据扫描的注解信息为每个接口类生成feign客户端请求,同时解析接口方法中的Spring MVC的相关注解,通过专门的注解解析器识别这些注解信息,以便后面可以正确的组装请求参数,使用 Ribbon 和 Eureka 获取到请求服务的真实地址等信息,最后使用 http 相关组件进行执行调用。其大致流程图如下:

@EnableFeignClients 和 @FeignClient 注解

在EnableFeignClients 注解类中有一个 @Import(FeignClientsRegistrar.class)的配置

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 引入FeignClientsRegistrar 来扫描@FeignClient注解下的类
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    ...
}

我们追踪代码进入到FeignClientsRegistrar类中,会发现FeignClientsRegistrar 类实现了ImportBeanDefinitionRegistrar(在spring context 项目中)接口,因此spring boot启动时会调用它的registerBeanDefinitions()方法,该方法中会扫描 EnableFeignClients 和 FeignClient 注解信息并设置相关信息。

/**
 * spring boot 启动时会自动调用 ImportBeanDefinitionRegistrar 入口方法
 */
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata
        , BeanDefinitionRegistry registry) {
    // 读取 @EnableFeignClients 注解中信息
    registerDefaultConfiguration(metadata, registry);
    // 扫描所有@FeignClient注解的类
    registerFeignClients(metadata, registry);
}

registerDefaultConfiguration方法

在registerDefaultConfiguration()方法中会读取@EnableFeignClients注解信息,然后将这些信息注册到 bean注册表(BeanDefinitionRegistry) 里面去;之后feign的一些默认配置将通过这里注册的信息中去获取。

注册的bean名称为:name + . + FeignClientSpecification.class.getSimpleName() ;其中name是由 default + 类的全限定名组成

if (metadata.hasEnclosingClass()) {
    name = "default." + metadata.getEnclosingClassName();
}
else {
    // 这里metadata.getClassName()其实就是EnableFeignClients注解当前标记类,通常就是启动类的路径,如:top.vchar.FeignDemoApplication
    name = "default." + metadata.getClassName();
}

registerFeignClients方法

  • registerFeignClients()方法首先会构建一个类扫描器ClassPathScanningCandidateComponentProvider ,然后创建一个FeignClient注解的过滤器AnnotationTypeFilter;

  • 获取要扫描的类路径 basePackages;共有2个分支:固定的类和配置的扫描包路径

    • 如果EnableFeignClients的clients没有配置(通常都不会配置);那么会依次从EnableFeignClients的 value、basePackage、basePackageClasses 这3个配置中取值合并作为basePackages的值;如果3个里面都没有那么将默认使用当前标注了EnableFeignClients注解类所在的路径(因为我们通常都是在启动类上用这个注解,因此大家常常听到的是:默认使用启动类所在的包路径)
    • 如果EnableFeignClients的clients配置了值,即表示只有这些配置了的类才会被feign注册为feign客户端;里面会将这些clients中配置的类的包路径加入 basePackages;然后在类扫描器上绑定2个过滤器,其中一个就是上面创建的根据 FeignClient 注解作为过滤条件,另外一个是判断是否是clients中配置的类为条件的过滤器。
  • 开始遍历包路径,扫描其中符合条件的类,同时里面会对扫描结果的类再次判断其是否为接口类型(不是的话将会抛出异常)且是否有注解,最后将其注册到bean注册表中;

  • 将服务名称作为bean的name向bean注册表中注册(里面会包含其默认配置);注意这里设置bean名称是使用服务名+FeignClientSpecification.class.getSimpleName(),因此如果对于一个服务写多个接口类会发生bean名称重复导致注册失败。所以需要增加一个 allow-bean-definition-overriding: true 的配置。

  • 注册feign接口客户端的bean到注册表中去;后面在使用的时候,比如注入时使用的就是这里注册的。

其流程图如下:

feign客户端的动态代理

上面registerFeignClient()方法中在构建bean的时候,实际构建的是FeignClientFactoryBean(这里面保存有FeignClient注解的所有属性)。

BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

FeignClientFactoryBean 类实现了FactoryBean接口的getObject()方法,后面动态代理时使用的就是它来获取feign client的。在这里会根据上面注解配置,同时会读取application.yml配置信息,根据配置来设置feign的相关信息,比如编解码器、注解解析器、请求超时时间等;之后如果没有设置url那么就会和负载均衡器(ribbon)整合。最后会通过反射将接口中相关方法进行解析保存供后面进行jdk代理使用。

@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();
  }
  // 需要代理,执行代理方法
  return dispatch.get(method).invoke(args);
}

feign的请求机制

当通过feign的服务接口进行服务调用时,实际调用的使其代理对象;我们在创建代理对象时绑定了处理器器 InvocationHandler,这里主要就是将不需要代理的方法(比如:equals、toString…)排除掉,然后执行该方法的invoke方法;而在创建动态代理对象的时候,为每个方法都初始化了一个同步方法处理器(SynchronousMethodHandler) 负责处理方法的请求。

@Override
public Object invoke(Object[] argv) throws Throwable {
  // 创建请求实例,组装请求参数信息: /user/findById?id=1
  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;
    }
  }
}

SynchronousMethodHandler会先将参数、请求的URL信息组装好,然后通过feign的负载均衡器 LoadBalancerFeignClient 中组装ribbon所需的相关信息,再使用FeignLoadBalancer执行请求,同时会通过ribbon来获取服务的真实地址(FeignLoadBalancer继承了ribbon的AbstractLoadBalancerAwareClient类,以此来和ribbon集成)。最后会将请求结果反序列化处理。

通过ribbon的 LoadBalancerCommand 中通过观察者模式来进行重试相关的判断,同时获取服务的真实地址;里面在会将选择的服务真实地址回传给FeignLoadBalancer。

服务重试和时间超时

这里需要注意的是feign的默认配置中是不会重试的,使用的是NEVER_RETRY。我们需要创建Retryer的bean才会生效。如下:

@Bean
public Retryer retryer(){
    // 这个feign提供的默认实现;可以自定义,实现 Retryer 接口即可
    return new Retryer.Default();
}

当ribbon中发生异常抛出时SynchronousMethodHandler中会补获到这个异常,然后SynchronousMethodHandler会调用Retryer的continueOrPropagate方法,在那里面来决定重试的策略。

因此在实际使用的时候需要注意,如果我们在ribbon中配置了重试相关的信息,比如下面的:

spring:
  cloud:
    loadbalancer:
      retry:
        # 负载均衡的重试,默认是开启的,因此可以不配置这个
        enabled: true
ribbon:
  # 连接和读取超时时间,单位毫秒
  ConnectTimeout: 1000
  ReadTimeout: 1000
  # 是否认为所有异常都要处理
  OkToRetryOnAllOperations: true
  # 每台机器重试次数
  MaxAutoRetries: 1
  # 重试的机器数量
  MaxAutoRetriesNextServer: 2

我们配置了ribbon的重试次数为每台机器重试1次,共计切换2次(即:每台机器要执行2次,总共为6次);在LoadBalancerCommand中就会按照这个配置来进行处理,当其仍然失败就会将异常抛出;此时在SynchronousMethodHandler中会补获到这个异常,如果配置了feign重试的Retryer,那么会按照Retryer的实现来进行重试处理,因此实际重试的次数是乘以了ribbon中重试次数了的。


特别提醒:扫码关注微信订阅号'起岸星辰',实时掌握IT业界技术资讯! 转载请保留原文中的链接!
 上一篇
基于雪花算法生成分布式ID(Java版) 基于雪花算法生成分布式ID(Java版)
雪花算法(SnowFlake算法),是 Twitter 开源的分布式 id 生成算法。其核心思想就是 使用一个 64 bit 的 long 型的数字作为全局唯一 id。
2021-06-01
下一篇 
Spring-Cloud之Feign Spring-Cloud之Feign
Feign 是Netflix实现的一套轻量级的REST API调用工具,Spring-Cloud-Feign 是在 Netflix 的 Feign 上再次封装的一层,下面我们通过一些Feign的示例带你快速了解如何使用它。
2021-05-16
  目录