Spring Cloud - Feign 原理解析

3,410 阅读10分钟

1、简单大体机制

​ Feign通过在主启动类标记 @EnableFeignClients 注解,表示开启Feign的功能,会在调用的接口上标记 @FeignClient 设置相关的服务名等信息。

​ 大体机制就是会扫描 @FeignClient 标记的接口,通过Feign的核心机制,将其构建为一个个的 Rest Client 对象,解析Spring MVC的相关注解,联合 Ribbon Eureka 获取到请求地址等信息,通过 http 相关组件进行执行调用。

Feign 整体流程简略.png

2、EnableFeignClients 入口

​ 我们使用 Feign 的话,会在 Application 的主启动类上,标记 EnableFeignClient 注解,在要调用的接口上标记 FeignClient 注解。

​ 在 EnableFeignClients 注解内部,有一个 @Import(FeignClientsRegistrar.class) ,这个类实现了 ImportBeanDefinitionRegistrar 接口,这个的话是 Spring Context 项目下的,所以会在 Spring Boot 项目启动的时候,会调用 FeignClientsRegistrar.registerBeanDefinitions , 扫描 FeiginClient 注解,并设置相关信息

未命名文件.png

2.1 registerDefaultConfiguration 解析@EnableFeignClient

​ 这个方法比较简单,首先会获取到标记了 @EnableFeignClients 注解的启动类的全限定名,并且读取和解析了这个注解设置的属性值。

​ 然后会拿着获取好的这些信息,注册到一个 BeanDefinitionRegistry 里面去,不过在注册的时候,会将在上面拿到的 启动类的全限定名,进行一个 "." **+ FeignClientSpecification.class.getSimpleName() 的拼接

2.2 registerFeignClients 扫描@FeignClient 注解接口

​ 最开始的话,会先构造一个 ClassPathScanningCandidateComponentProvider 这样的一个扫描器和 AnnotationTypeFilter 这样的一个注解类型过滤器,设置为 FeignClient.class

​ 会根据 @EnableFeignCliens 注解获取到属性信息,通过 clients 属性,获取包路径,这个 client 属性,一般我们不会去进行配置,可以理解为这个就是获取不到的,然后会将上面初始化好的注解类型过滤器设置给 Scanner 扫描器,然后会通过读取 EnableFeignClients 的其他属性查看是否有配置包路径信息,如果都没有的话,默认设置 包路径(basePackages) 为当前启动类所在的包

​ 循环遍历包路径,通过上面构建的ClassPathScanningCandidateComponentProvider 扫描器和添加的FeignClient类型的过滤器,扫描包路径下****所有标记了 @FeignClient 注解的接口,并获取到 @FeignClient接口标记的服务名和相关的其他属性值。

​ 获取到相关信息之后,就和 2.1 一样,会将获取到的 服务名(不是类名) 拼接FeignClientSpecification.class.getSimpleName() 注册到 BeanDefinitionRegistry 里面去

​ 最后的话,就是将获取到的 FeignClient 的信息, 通过 BeanDefinitionBuilder 构造 FeignClientFactoryBean(这个类就是负责构建 Feign 的核心工厂类) ,将通过@FeginClient 注解获取到的信息,都设置到这个 definition 中,通过这个 definition.getBeanDefinition() 获取到相应的 AbstractBeanDefinition ,并将 AbstractBeanDefinition、className(扫描到的接口的全限定名)、还有拼接好的别名一起构建了 BeanDefinitionHolder ,最后还是会通过 BeanDefinitionRegistry 进行注册

registerFeignClients.png

2.3 构建接口的动态代理

​ 其实在 Spring 容器初始化的时候,一定是会根据扫描出来的 @FeignClient 的信息,去构造一个原生的 FeignClient 出来,然后基于这个 FeignClient 来构建一个 ServerAClient 接口的动态代理,后面将这个接口的动态代码注入给 Controller ,来进行调用。

​ 在 FeignClientFactoryBean 类中,发现 getObject 方法标有重写的注解,并且调用了大部分类中实现的代码,通过 debug 启动的时候,也发现会调用这个方法,所以这个方法应该就是动态代理实现的入口

​ 首先会获取到 FeignContext ,其实就是和Ribbon的 SpringClientFactory 是同样的效果,都是继承父类 **NamedContextFactory,**为每个服务都初始化一套属于服务自己的组件,这也就是为什么 feign 可以实现对每个服务可以进行不同 Configuration 配置的原因,在 org.springframework.cloud.netflix.feign 包下,有clod 和 feign 整合的代码,由 FeignAutoConfiguration 负责初始化相关的一些 Bean 实例,FeignContext 也是在这里进行初始化的,会将服务名和配置信息,放入到 NamedContextFactory 中的 context Map 中。

​ 然后会通过调用 feign() 方法,构建 Feign.Builder 实例对象,内部的话,主要就是通过 applicationContext 服务上下文对象,获取到相应的服务实例,例如 FeignLoggerFactory、Logger、Feign.Builder、在获取到 Feign.Builder 之后,就是会设置它的相关组件:Encoder(默认 SpringEncoder)、Decoder(默认 ResponseDecoder)、Contract(默认 SpringMvcContract)。

给Feign.Builder 赋值完基本三大组件之后,就是去读取配置信息,赋值给 Feign.Builder,默认的话,会首先读取 @FeignClient 注解上面配置的参数,其次会读取 application.yml 中设置的 default 默认参数,最后会读取 application.yml 中为每个服务实例设置的参数,这三个的话有优先级关系,优先级最高的是 application.yml 中为服务实例设置的参数。设置的属性也就是链接超时时间(默认10S),读取超时时间(默认60S),失败重试(默认不重试)等等。。 基本到这的话, Feign.Builder 就已经构建完了。

Feign.Builder (2).png

​ 当构建完 Feign.Builder 之后,就会获取到配置的地址或者路径,进行 url 的拼接,这里如果我们没有设置 url 的话,默认的话 feign 会和 ribbon 进行整合,通过 ribbon 来实现负载均衡操作,会构建 loadBalance 实例。

​ 在构建 loadBalance 的时候,会先构建一个 HardCodedTarget ,将获取到的 服务名、接口、url 都进行一个赋值,然后拿到 HardCodedTarget 的实例、服务上下文实例、Feign.Builder 实例,进行构建。

在构建方法内部主要就是从 applicationContext 中获取到 FeignClient 的实例对象,并加到构造器中,然后会从 applicationContext 中获取到 HystrixTargeter 实例,调用HystrixTargeter.target() 方法,去创建动态代理的实例。

​ 首先会判断当前 Feign.Builder 是不是 feign.hystrix.HystrixFeign.Builder类型 ,默认的话是 Default 类型的,这个值只有在配置了 feign 和 hystrix 配合使用的时候才有,之后通过 Feign.Builder 的target的方法去生成动态代理的实例,在生成实例之前,会调用 build() 方法,初始化 SynchronousMethodHandler(处理方法) 和 ParseHandlersByName(为每个方法绑定一个处理方法) 实例,构建出来 ReflectiveFeign 实例,由这个实例去生成对应的动态代理的实例。

​ 生成实例的逻辑比较繁琐,首先是会通过 ParseHandlersByName 获取到接口中包含的方法名对应的 SynchronousMethodHandler 处理方法,然后会通过反射,将每个方法的 Method 对象和 SynchronousMethodHandler 处理方法,进行关联(放到了一个 Map 中),通过 FeignInvocationHandler 创建出来对应的 InvocationHandler 实例,最后就是通过 jdk 的动态代理,构建出来这个服务实例接口对应的 T proxy 对象,放入到 Spring 容器中,注入给需要的 Controller 。

​ 当 Controller 进行执行的时候,其实就是执行的 InvocationHandler 中的 invoke() 方法。

Feign Client 动态代理实现机制 (2).png

2.4 feign 请求的基本机制

​ 当用户发送请求的时候,实际的调用的是 proxy 代理对象,会交由 InvacationHandler 去执行,在创建动态代理对象的时候,为每个方法都初始化了一个 SynchronousMethodHandler 负责处理方法的请求。

​ feign 的话,是会和 ribbon 进行整合使用,在创建 Feign.Builder 的时候,初始化了一个 LoadBalancerClient 对象,负责和 ribbon 组件进行交互, ribbon 经由 eureka 和 loadBalancer 之后,获得到真正的请求地址,由http 组件真正的负责调用。

Feign 基于动态代理处理请求的机制.png

​ 也就是说,每次当我们发送一个请求之后,会首先找到服务接口的动态代理对象,调用动态代理的 invoke 方法,在它的方法内部会首先判断是不是 equals hashCode toString ,如果都不是的话,就会根据我们创建代理对象时,构造的 Map<Method, MethodHandler> 根据方法,找到对象的处理方法的 Handler 对象。

SynchronousMethodHandler 首先会先对请求进行一定的处理,拼接上方法参数等,然后调用 executeAndDecode 方法。

​ 这个方法首先也是去处理请求,看看需不需要执行定义的拦截器,若需要这直接执行,之后会根据我们之前创建的 HardCodeTarger 处理请求,比如拼接服务名之类的,通过 LoadBalancerFeignClient 中的 execute 方法进行执行,主要就是对 url 的处理和 RibbonRequest/IClientConfig 的构建,最后会通过工厂去获取这个服务实例对应的 FeignLoadBalancer 实例,由它负责和 Ribbon 进行交互,完成真正的执行。

​ 其实我们获取到的 FeignBalancer 实例就是 Ribbon 默认的 ZoneAwareLoadBalancer 实例,这个实例内部有一个 ServerList 对象,它通过 pollingServerUpdateList 方法,每隔30秒从 eureka client 上拉取注册表,缓存在自己本地。

​ 最后的话,就是通过构建一个 LoadBalancerCommand 实例,调用它的 submit 方法,在判断 Server 对象为空的时候,会通过 ZoneAwareLoadBalancer 的 chooseServer 方法进行负载均衡,选择出来一个服务的地址,然后调用 ServerOperation.call() 方法,进行请求的发送。最终在发送的时候,连接超时时间会被设置为默认1秒钟,然后将响应结果封装为 RibbonResponse,最后通过 ResponseEntityDecode 实例对结果进行反序列化

Feign 详细请求机制 (2).png

3、服务重试,超时

ribbon:
  ConnectTimeout: 1000
  ReadTimeout: 1000
  OkToRetryOnAllOperations: true
  MaxAutoRetries: 1
  MaxAutoRetriesNextServer: 3

​ 首先在 FeignLoadBalancer.getRequestSpecificRetryHandler() 方法中,就会读取配置的几个参数:OkToRetryOnAllOperations、MaxAutoRetries、MaxAutoRetriesNextServer。

​ LoadBalancerCommand.submit()方法中,也就是在执行请求逻辑的时候,读取RetryHandler中配置的参数,会根据请求的情况,是否报错,是否报异常,进行重试的控制

  • MaxAutoRetries: 1

  • MaxAutoRetriesNextServer: 3

    比如你请求8080机器,第一次超时或者报错了,重试1次,再次请求1次,如果第二次请求,还是超时或者报错的话,那么就会尝试其他的机器,比如说8088,但是第一次尝试其他机器的时候,其实还是访问的是8080,而且对8080是访问1次,重试1次,如果还是不行,尝试下一台机器

  • OkToRetryOnAllOperations: true

这个参数的意思,就是无论是请求的时候有什么异常,超时、报错,都会触发重试

spring:
  cloud:
loadbalancer:
  retry:
    enabled: true

​ 如果一旦说,整个哪怕上面的所有的重试都没生效,请求都失败了,就会报错,就会进入SynchronousMethodHandler的try catch中,去处理这个异常,此时就会调用一个retryer的方法,这个retryer默认情况下是不重试的,但是如果你开启了这坨东西:

​ 那么此时retryer就会工作,就默认的feign的重试机制,这个的话,会去读取我们自己设置的 Retry 类

@Bean
    public Retryer feignRetryer() {
        return new Retryer.Default();
    }

​ 启用自定义的一个Retryer,feign的Retryer才可以,默认情况下是没有重试,NO_RETRY,直接是报错,Retryer.DEFAULT,默认是自动重试5次,每次重试的时候,会停留一段时间,这里是150ms,就会重试一次,每次重试,都会依次访问ServiceA的每台机器,每台机器都会发现是超时了,再次休眠225ms,再次重试,每次重试的时间都是不一样的,都会不断的增加

​ 如果去掉了下面那坨配置,就会发现说,ServiceA的每个服务实例,都反复请求了6次,然后才会报错

spring:
  cloud:
loadbalancer:
  retry:
    enabled: true

​ 如果加上了上面的那段配置之后,就会发现说,是ServiceA的每个服务实例,都会请求1次,然后就会报错,然后就会走feign的Retryer的重试机制