当前位置: 代码迷 >> 综合 >> 【Feign源码解析】feign 源码解析
  详细解决方案

【Feign源码解析】feign 源码解析

热度:62   发布时间:2023-11-25 14:40:25.0

一. 前言

看 feign 源码有一个有趣的感受,feign 利用 spring 或者说与 spring 结合的方式,和 mybatis 是一样的,真是天下代码一大抄。

先附上 feign 官方提供的示例,非常的全,藏在 github 仓库中,不看源码都没发现。。。

feign官方示例

二. feignclient bean 被代理的方式

首先从 springboot 的启动项看起,springboot 项目使用 feign 需要引入包,和在启动类上使用 @EnableFeignClients 注解,这个注解上有另一个注解 @Import(FeignClientsRegistrar.class)
,@Import 注解意思是引入一个或多个 class 文件。FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistry。

@Import 和 ImportBeanDefinitionRegister 配合使用,与 xml 中配置实现了 BeanDefinitionRegistryPostProcessor 功能类似。

前者是 springboot 注解的方式,后者是 xml 配置 bean 的方式。
mybatis-spring,也有这种实现,在 @MapperScan(MapperScanRegistry.class) 注解中,引入了 MapperScanRegistry 实现了 ImportBeanDefinitionRegister,在 registryBeanDefinitions 中,使用 BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class) ,等于把注解的内容解析了,然后传给 MapperScanerConfigurer,后续流程和使用 xml 方式一样,只是先解析了一下注解中的内容,如 basePackage 等。

回到 Fegin 中,FeignClientsRegistrar 重写的方法 registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 中,有两个子方法:

1.registerDefaultConfiguration(),这个方法,把注解中的 Class<?>][] defaultConfigurations 这一项取出来,放在类中 FeignClientConfiguration,并注册。这里的是全局配置,但每个 feignClient 都可以有自己单独的配置。

2.registerFeignClient(),这里先根据规则,调用 Scan 扫描器扫描 basePackages 中的 bean。
然后使用 BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class),实例化出 BeanDefinitionBuilder,BeanDefinitionBuilder 有许多参数,如:url、path、name、contextId、type、fallback、fallbackFactory等。
还向 registry 中注册了另一个 beanDefinition,即注解中配置的 configure,configure 的 bean 是 FeignClientSpecification。

name 参数和 contextId 参数是结合起来使用的,可以配置 name 相同,但是 contextId 不同的 feignClient,contextId 会被注册成 spring 中配置的 bean 的别名

三.从工厂 FeignClientFactoryBean 中取 bean

mybatis 的 factory bean 实现了 FactoryBean 和 InitializingBean ,在 InitializingBean 的 afterPropertiesSet() 方法中把代理的 mapper 的全名称放到 configuration 即环境中,getObject() 取对象时,也从 configuration 中取,等于全局缓存了。

而 feign 不是这么做的,feign 的 afterPropertiesSet() 中只是校验了一下 contextId 和 name 是否有值。和 mybatis 不同的一点是,mybatis 中所有 mapper 可能有一个全局的 FactoryBean,而 Feign 每个 FeignClient 都有自己单独的 FeignClientBeanFactory,这可能和配置有关系,因为 mybatis 有全局拦截器,很少单独的配置,而 feign 在调用时可能每个 FeignClient 都需要有单独的配置。

介绍一下 feignClient 的几个组件:

ReflectiveFeign 的属性就是 feign 的各种配置项,如 LoadBalancerClient 、logLevel、retryer、options、encoder、decoder、queryMapEncoder、contract 等,并且这个类中,有一个静态子类FeignInvocationHandler 实现了 InvocationHandler,最终 feignclient 执行时,是使用这里的 invoke() 方法。

FeignContext 继承 NameContextFactory,在获取属性时,使用父类的 NameContextFactory.getInstance(String name,Class type),这个方法有两步,先获取 Context,再获取 bean。context 就是 spring 的环境,在上文说的 registerFeignClient() 方法中,增加个别 client 的配置到 spring 中。

Targeter 很有意思,封装了获取目标类的过程,最后还是找 ReflectiveFeign 来实例化对象。

SynchronousMethodHandler 是代理 feignclient 文件中具体方法的,每个方法有单独的。这个和 mybatis 也很像,mybatis 中每个 mapper 文件也有单独的 MethodHandler 。MethodHandler
有两个实现类,一个是 java8 中增加的 DefaultMethodHandler,还有一个就是 SynchronousMethodHandler。

回到获取 bean 的流程:

在工厂 bean FeignClientFactoryBean 的 getObject() 方法中,构建代理时,使用到了 ReflectiveFeign,ReflectiveFeign 使用了建造者模式,在设置属性时,通过 FeignContext 获取上下文。下面给出 getObject() 的示例代码:

	<T> T getTarget() {
    FeignContext context = this.applicationContext.getBean(FeignContext.class);// 这里把配置封装到了 feign 中Feign.Builder builder = feign(context);// 这里如果指定了 url ,则不使用 eureka 进行服务名称查询,不适用 ribbonClientif (!StringUtils.hasText(this.url)) {
    ......省略参数组装//在这里找 ReflectiveFeign 创建 feignClient 的实例return (T) loadBalance(builder, context,new HardCodedTarget<>(this.type, this.name, this.url));}.... 省略参数组装// url 硬编码的代理实例化Targeter targeter = get(context, Targeter.class);return (T) targeter.target(this, builder, context,new HardCodedTarget<>(this.type, this.name, url));}

ReflectiveFeign.newInstance() 方法,就是遍历每一个 feignClient 中的方法,然后生成对应的 MethodHandler 注入 FeignInvocationHandler 的 Map<Method, MethodHandler> dispatch 中。

SynchronousMethodHandler.invoke() 是运行时,执行的方法,如果需要调试,可以在这个方法上打断点。

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 {
    //这里调用了 Thread.sleep 进行休眠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;}}}

具体执行的 client 是从环境中获取的,放在环境中的 client 的实现类是什么,获取的就是什么。

四. spring.factories

在 spring.factories 中,有默认的 client 、LoadBanlancer、Gzip、等等的默认配置,如果有需要,都可以更改。

这里默认的 FeignClient 是 LoadBalancerFeignClient ,在执行 execute() 方法时,会调用 ribbon 包中的 AbstractLoadBalancerAwareClient.executeLoadBalancer(request, requestConfig)。

还有一些细节,像 name 如何从 eureka 上获取真实的服务器地址的,还有请求的拦截器何时织入何时执行的,下次再补充。

  相关解决方案