1、简单大体机制
Feign通过在主启动类标记 @EnableFeignClients 注解,表示开启Feign的功能,会在调用的接口上标记 @FeignClient 设置相关的服务名等信息。
大体机制就是会扫描 @FeignClient 标记的接口,通过Feign的核心机制,将其构建为一个个的 Rest Client 对象,解析Spring MVC的相关注解,联合 Ribbon Eureka 获取到请求地址等信息,通过 http 相关组件进行执行调用。
2、EnableFeignClients 入口
我们使用 Feign 的话,会在 Application 的主启动类上,标记 EnableFeignClient 注解,在要调用的接口上标记 FeignClient 注解。
在 EnableFeignClients 注解内部,有一个 @Import(FeignClientsRegistrar.class) ,这个类实现了 ImportBeanDefinitionRegistrar 接口,这个的话是 Spring Context 项目下的,所以会在 Spring Boot 项目启动的时候,会调用 FeignClientsRegistrar.registerBeanDefinitions , 扫描 FeiginClient 注解,并设置相关信息
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 进行注册
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 之后,就会获取到配置的地址或者路径,进行 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() 方法。
2.4 feign 请求的基本机制
当用户发送请求的时候,实际的调用的是 proxy 代理对象,会交由 InvacationHandler 去执行,在创建动态代理对象的时候,为每个方法都初始化了一个 SynchronousMethodHandler 负责处理方法的请求。
feign 的话,是会和 ribbon 进行整合使用,在创建 Feign.Builder 的时候,初始化了一个 LoadBalancerClient 对象,负责和 ribbon 组件进行交互, ribbon 经由 eureka 和 loadBalancer 之后,获得到真正的请求地址,由http 组件真正的负责调用。
也就是说,每次当我们发送一个请求之后,会首先找到服务接口的动态代理对象,调用动态代理的 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 实例对结果进行反序列化
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的重试机制