1、简单大体机制

  1. Feign通过在主启动类标记 @EnableFeignClients 注解,表示开启Feign的功能,会在调用的接口上标记 @FeignClient 设置相关的服务名等信息。<br /> 大体机制就是会扫描 @FeignClient 标记的接口,通过Feign的核心机制,将其构建为一个个的 Rest Client 对象,解析Spring MVC的相关注解,联合 Ribbon Eureka 获取到请求地址等信息,通过 http 相关组件进行执行调用。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22256886/1629258804357-8d54fa6c-b92e-48ae-b889-f38be9e2770a.png#clientId=uddf4bdec-68e3-4&from=paste&id=u5a6492c5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1328&originWidth=2000&originalType=url&ratio=1&size=406769&status=done&style=none&taskId=uf635d453-2165-4044-8f8a-6e6253ba96e)

2、EnableFeignClients 入口

  1. 我们使用 Feign 的话,会在 Application 的主启动类上,标记 EnableFeignClient 注解,在要调用的接口上标记 FeignClient 注解。<br /> EnableFeignClients 注解内部,有一个 @Import(FeignClientsRegistrar.class) ,这个类实现了 ImportBeanDefinitionRegistrar 接口,这个的话是 Spring Context 项目下的,所以会**在 Spring Boot 项目启动的时候,会调用 FeignClientsRegistrar.registerBeanDefinitions , 扫描 FeiginClient 注解,并设置相关**信息<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22256886/1629258804051-fb530736-eea8-45ce-bfe2-c5bfbc0bad6b.png#clientId=uddf4bdec-68e3-4&from=paste&id=u34575d05&margin=%5Bobject%20Object%5D&name=image.png&originHeight=932&originWidth=1462&originalType=url&ratio=1&size=186589&status=done&style=none&taskId=u36d15c8e-4d8f-4494-a5c5-109032e6993)

2.1 registerDefaultConfiguration 解析@EnableFeignClient

  1. 这个方法比较简单,首先会获取到标记了 **@EnableFeignClients** 注解的启动类的全限定名,并且读取和解析了这个注解设置的属性值。<br /> 然后会拿着获取好的这些信息,注册到一个 **BeanDefinitionRegistry** 里面去,不过在注册的时候,会将在上面拿到的 **启动类的全限定名,进行一个"."** **+ FeignClientSpecification.**class**.getSimpleName() 的拼接

2.2 registerFeignClients 扫描@FeignClient 注解接口

  1. 最开始的话,会先构造一个 **ClassPathScanningCandidateComponentProvider** 这样的一个扫描器和 **AnnotationTypeFilter** 这样的一个注解类型过滤器,设置为 FeignClient.class<br /> 会根据 **@EnableFeignCliens** 注解获取到属性信息,通过 **clients** 属性,获取包路径,这个 client 属性,一般我们不会去进行配置,可以理解为这个就是获取不到的,**然后会将上面初始化好的注解类型过滤器设置给 Scanner 扫描器**,然后会通过读取 **EnableFeignClients** 的其他属性查看是否有配置包路径信息,如果都没有的话,默认设置 **包路径(basePackages 为当前启动类所在的包** 。<br /> 循环遍历包路径,**通过上面构建的ClassPathScanningCandidateComponentProvider扫描器和添加的FeignClient类型的过滤器,扫描包路径下****所有标记了@FeignClient 注解的接口,并获取到 @FeignClient接口标记的服务名和相关的其他属性值。**<br /> 获取到相关信息之后,就和 **2.1** 一样,会将获取到的 **服务名(不是类名)** 拼接**FeignClientSpecification.class.getSimpleName()** 注册到 **BeanDefinitionRegistry** 里面去<br /> 最后的话,就是将获取到的 FeignClient 的信息, 通过 BeanDefinitionBuilder 构造 **FeignClientFactoryBean(这个类就是负责构建 Feign 的核心工厂类)** ,将通过@FeginClient 注解获取到的信息,都设置到这个 definition 中,通过这个 definition.getBeanDefinition() 获取到相应的 **AbstractBeanDefinition** ,并将 AbstractBeanDefinitionclassName(扫描到的接口的全限定名)、还有拼接好的别名一起构建了 **BeanDefinitionHolder** ,最后还是会通过 **BeanDefinitionRegistry** 进行注册<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22256886/1629258804368-86bac617-0213-4439-bdf9-c640c9e135b2.png#clientId=uddf4bdec-68e3-4&from=paste&id=u81430d86&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1668&originWidth=1716&originalType=url&ratio=1&size=442901&status=done&style=none&taskId=u7e1236d2-7c18-497b-b657-38b9314a432)

2.3 构建接口的动态代理

  1. 其实在 Spring 容器初始化的时候,一定是会根据扫描出来的 **@FeignClient** 的信息,去构造一个原生的 **FeignClient** 出来,然后基于这个 **FeignClient** 来构建一个 **ServerAClient** 接口的动态代理,后面将这个接口的动态代码注入给 Controller ,来进行调用。<br /> **FeignClientFactoryBean** 类中,发现 **getObject** 方法标有重写的注解,并且调用了大部分类中实现的代码,通过 debug 启动的时候,也发现会调用这个方法,所以这个方法应该就是动态代理实现的入口<br /> 首先会获取到 FeignContext ,其实就是和Ribbon SpringClientFactory 是同样的效果,都是继承父类 **NamedContextFactory,**为每个服务都初始化一套属于服务自己的组件,这也就是为什么 feign 可以实现对每个服务可以进行不同 Configuration 配置的原因,在 **org.springframework.cloud.netflix.feign** 包下,有clod feign 整合的代码,由 **FeignAutoConfiguration** 负责初始化相关的一些 Bean 实例,FeignContext 也是在这里进行初始化的,会将服务名和配置信息,放入到 **NamedContextFactory** 中的 context Map 中。<br /> 然后会通过调用 feign() 方法,构建 Feign.Builder 实例对象,内部的话,主要就是通过 applicationContext 服务上下文对象,获取到相应的服务实例,例如 FeignLoggerFactoryLoggerFeign.Builder、在获取到 Feign.Builder 之后,就是会设置它的相关组件:Encoder(默认 SpringEncoder)、Decoder(默认 ResponseDecoder)、Contract(默认 SpringMvcContract)。<br />给Feign.Builder 赋值完基本三大组件之后,就是去读取配置信息,赋值给 Feign.Builder,默认的话,会首先读取 **@FeignClient** 注解上面配置的参数,其次会读取 **application.yml** 中设置的 default 默认参数,最后会读取 **application.yml** 中为每个服务实例设置的参数,这三个的话有优先级关系,优先级最高的是 application.yml 中为服务实例设置的参数。设置的属性也就是链接超时时间(默认10S),读取超时时间(默认60S),失败重试(默认不重试)等等。。 基本到这的话, Feign.Builder 就已经构建完了。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22256886/1629258804282-8b0bd657-867f-4ae4-84b4-a8b23beabc5b.png#clientId=uddf4bdec-68e3-4&from=paste&id=u01ccba6c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1372&originWidth=2000&originalType=url&ratio=1&size=365036&status=done&style=none&taskId=uffabb676-ec2a-45f6-b9ea-a8ce869de36)<br /> 当构建完 Feign.Builder 之后,就会获取到配置的地址或者路径,进行 url 的拼接,这里如果我们没有设置 url 的话,默认的话 feign 会和 ribbon 进行整合,通过 ribbon 来实现负载均衡操作,会构建 loadBalance 实例。<br /> 在构建 loadBalance 的时候,会先构建一个 HardCodedTarget ,将获取到的 服务名、接口、url 都进行一个赋值,然后拿到 HardCodedTarget 的实例、服务上下文实例、Feign.Builder 实例,进行构建。<br />在构建方法内部主要就是从 applicationContext 中获取到 FeignClient 的实例对象,并加到构造器中,然后会从 applicationContext 中获取到 HystrixTargeter 实例,调用HystrixTargeter.target() 方法,去创建动态代理的实例。<br /> 首先会判断当前 Feign.Builder 是不是 **feign.hystrix.HystrixFeign.Builder类型** ,默认的话是 Default 类型的,这个值只有在配置了 feign 和 hystrix 配合使用的时候才有,之后通过 **Feign.Builder** 的target的方法去生成动态代理的实例,在生成实例之前,会调用 build() 方法,**初始化 SynchronousMethodHandler(处理方法) 和 ParseHandlersByName(为每个方法绑定一个处理方法) 实例**,构建出来 **ReflectiveFeign** 实例,由这个实例去生成对应的动态代理的实例。<br /> 生成实例的逻辑比较繁琐,首先是会通过 ParseHandlersByName 获取到接口中包含的方法名对应的 SynchronousMethodHandler 处理方法,然后会通过反射,将每个方法的 **Method** 对象和 SynchronousMethodHandler 处理方法,进行关联(放到了一个 Map 中),通过 FeignInvocationHandler 创建出来对应的 InvocationHandler 实例,最后就是通过 jdk 的动态代理,构建出来这个服务实例接口对应的 T proxy 对象,放入到 Spring 容器中,注入给需要的 Controller 。<br /> 当 Controller 进行执行的时候,其实就是执行的 InvocationHandler 中的 invoke() 方法。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22256886/1629258804488-c3c6366c-43bf-4c01-8345-bd2c62096a66.png#clientId=uddf4bdec-68e3-4&from=paste&id=ueea84196&margin=%5Bobject%20Object%5D&name=image.png&originHeight=2179&originWidth=2000&originalType=url&ratio=1&size=756207&status=done&style=none&taskId=u123bc18d-a6a1-4be4-91c5-13c5cdf9098)

2.4 feign 请求的基本机制

  1. 当用户发送请求的时候,实际的调用的是 proxy 代理对象,会交由 InvacationHandler 去执行,在创建动态代理对象的时候,为每个方法都初始化了一个 **SynchronousMethodHandler** 负责处理方法的请求。<br /> feign 的话,是会和 ribbon 进行整合使用,在创建 **Feign.Builder** 的时候,初始化了一个 **LoadBalancerClient** 对象,负责和 ribbon 组件进行交互, ribbon 经由 eureka loadBalancer 之后,获得到真正的请求地址,由http 组件真正的负责调用。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22256886/1629258805172-b2e0e02f-5520-4b99-a3b6-a90fa9d40753.png#clientId=uddf4bdec-68e3-4&from=paste&id=u49e8b066&margin=%5Bobject%20Object%5D&name=image.png&originHeight=857&originWidth=2000&originalType=url&ratio=1&size=246891&status=done&style=none&taskId=ua6b9b651-e412-4446-9bd5-04bf31b3313)<br /> 也就是说,每次当我们发送一个请求之后,会首先找到服务接口的动态代理对象,调用动态代理的 invoke 方法,在它的方法内部会首先判断是不是 **equalshashCodetoString** ,如果都不是的话,就会根据我们创建代理对象时,构造的 Map<Method, MethodHandler> 根据方法,找到对象的处理方法的 Handler 对象。<br />**SynchronousMethodHandler** 首先会先对请求进行一定的处理,拼接上方法参数等,然后调用 **executeAndDecode** 方法。<br /> 这个方法首先也是去处理请求,看看需不需要执行定义的拦截器,若需要这直接执行,之后会根据我们之前创建的 **HardCodeTarger** 处理请求,比如拼接服务名之类的,通过 **LoadBalancerFeignClient** 中的 execute 方法进行执行,主要就是对 url 的处理和 RibbonRequest/IClientConfig 的构建,最后会通过工厂去获取这个服务实例对应的 **FeignLoadBalancer** 实例,由它负责和 Ribbon 进行交互,完成真正的执行。<br /> 其实我们获取到的 FeignBalancer 实例就是 Ribbon 默认的 **ZoneAwareLoadBalancer** 实例,这个实例内部有一个 **ServerList** 对象,它通过 **pollingServerUpdateList** 方法,每隔30秒从 eureka client 上拉取注册表,缓存在自己本地。<br /> 最后的话,就是通过构建一个 **LoadBalancerCommand** 实例,调用它的 submit 方法,在判断 Server 对象为空的时候,会通过 ZoneAwareLoadBalancer 的 chooseServer 方法进行负载均衡,选择出来一个服务的地址,然后调用 **ServerOperation.call()** 方法,进行请求的发送。最终在发送的时候,连接超时时间会被设置为默认1秒钟,然后将响应结果封装为 RibbonResponse,最后通过 ResponseEntityDecode 实例对结果进行反序列化<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22256886/1629258806232-115b10fa-b2ab-4c04-9aea-0b9c898d188d.png#clientId=uddf4bdec-68e3-4&from=paste&id=u73de9985&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1594&originWidth=2000&originalType=url&ratio=1&size=382251&status=done&style=none&taskId=ue8db709d-d7dd-4c0b-9a66-043324a7dca)

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的重试机制

https://juejin.cn/post/6904886151136411655