Feign源码解析二

Feign源码解析二,第1张

本文会基于Feign源码,看看Feign到底是怎么实现远程调用

上文中,我们的 user-service 服务需要调用远程的 order-service 服务完成一定的业务逻辑,而基本实现是order-service提供一个spi的jar包给user-service依赖,并且在user-service的启动类上添加了一个注解

这个注解就是@EnableFeignClients,接下来我们就从这个注解入手,一步一步解开Feign的神秘面纱

该注解类上的注释大概的意思就是:

扫描那些被声明为 Feign Clients (只要有 orgspringframeworkcloudopenfeignFeignClient 注解修饰的接口都是Feign Clients接口)的接口

下面我们继续追踪源码,看看到底什么地方用到了这个注解

利用IDEA的查找调用链快捷键,可以发现在class类型的文件中只有一个文件用到了这个注解

OK,下面主要就是看这个类做了什么

通过UML图我们发现该类分别实现了 ImportBeanDefinitionRegistrar , ResourceLoaderAware 以及 EnvironmentAware 接口

这三个接口均是spring-framework框架的spring-context模块下的接口,都是和spring上下文相关,具体作用下文会分析

总结下来就是利用这两个重要属性,一个获取应用配置属性,一个可以加载classpath下的文件,那么FeignClientsRegistrar持有这两个东西之后要做什么呢?

上面将bean配置类包装成 FeignClientSpecification ,注入到容器。该对象非常重要,包含FeignClient需要的 重试策略 , 超时策略 , 日志 等配置,如果某个FeignClient服务没有设置独立的配置类,则读取默认的配置,可以将这里注册的bean理解为整个应用中所有feign的默认配置

由于 FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,这里简单提下这个接口的作用

我们知道在spring框架中,我们如果想注册一个bean的话主要由两种方式:自动注册/手动注册

知道了 ImportBeanDefinitionRegistrar 接口的作用,下面就来看下 FeignClientsRegistrar 类是何时被加载实例化的

通过IDEA工具搜索引用链,发现该类是在注解@EnableFeignClients上被import进来的,文章开始的中有

这里提下@Import注解的作用

该注解仅有一个属性value,使用该注解表明导入一个或者多个@Configuration类,其作用和xml文件中的<import>等效,其允许导入@Configuration类,ImportSelector接口/ImportBeanDefinitionRegistrar接口的实现,也同样可以导入一个普通的组件类

注意,如果是XML或非@Configuration的bean定义资源需要被导入的话,需要使用@ImportResource注解代替

这里我们导入的FeignClientsRegistrar类正是一个ImportBeanDefinitionRegistrar接口的实现

FeignClientsRegistrar重写了该接口的 registerBeanDefinitions 方法,该方法有两个参数注解元数据 metadata 和bean定义注册表 registry

该方法会由spring负责调用,继而注册所有标注为@FeignClient注解的bean定义

下面看registerBeanDefinitions方法中的第二个方法,在该方法中完成了所有@FeignClient注解接口的扫描工作,以及注册到spring中,注意这里注册bean的类型为 FeignClientFactoryBean ,下面细说

总结一下该方法,就是扫描@EnableFeignClients注解上指定的basePackage或clients值,获取所有@FeignClient注解标识的接口,然后将这些接口一一调用以下 两个重要方法 完成 注册configuration配置bean 和注册 FeignClient bean

断点位置相当重要

BeanDefinitionBuilder definition = BeanDefinitionBuildergenericBeanDefinition(FeignClientFactoryBeanclass);

这里是利用了spring的代理工厂来生成代理类,即这里将所有的 feignClient的描述信息 BeanDefinition 设定为 FeignClientFactoryBean 类型,该类继承自FactoryBean,因此这是一个代理类,FactoryBean是一个工厂bean,用作创建代理bean,所以得出结论,feign将所有的 feignClient bean定义的类型包装成 FeignClientFactoryBean

最终其实就是存入了BeanFactory的beanDefinitionMap中

那么代理类什么时候会触发生成呢? 在spring 刷新容器时 ,会根据beanDefinition去实例化bean,如果beanDefinition的beanClass类型为代理bean,则会调用其 T getObject() throws Exception; 方法生成代理bean,而我们实际利用注入进来的FeignClient接口就是这些一个个代理类

这里有一个需要注意的点,也是开发中会遇到的一个 启动报错点

如果我们同时定义了两个不同名称的接口 (同一个包下/或依赖方指定全部扫描我们提供的 @FeignClient ),且这两个 @FeignClient 接口注解的 value/name/serviceId 值一样的话,依赖方拿到我们的提供的spi依赖,启动类上 @EnableFeignClients 注解扫描能同时扫描到这两个接口,就会 启动报错

原因就是Feign会为每个@FeignClient注解标识的接口都注册一个以serviceId/name/value为key,FeignClientSpecification类型的bean定义为value去spring注册bean定义,又默认不允许覆盖bean定义,所以报错

官方提示给出的解决方法要么改个@FeignClient注解的serviceId,name,value属性值,要么就开启spring允许bean定义覆写

至此我们知道利用在springboot的启动类上添加的@EnableFeignClients注解,该注解中import进来了一个手动注册bean的 FeignClientsRegistrar注册器 ,该注册器会由spring加载其 registerBeanDefinitions方法 ,由此来扫描所有@EnableFeignClients注解定义的basePackages包路径下的所有标注为@FeignClient注解的接口,并将其注册到spring的bean定义Map中,并实例化bean

下一篇博文中,我会分析为什么我们在调用(@Resource)这些由@FeignClient注解的bean的方法时会发起 远程调用

注册就是添加一个新的用户,当你没账号密码的时候,点击注册,进入另外一个页面,填写用户名,密码等相关信息,点击保存,在用户表中新添加一条刚才保存的用户,这样你就可以用刚注册的用户登陆系统了。

我们一般在项目开发中都是使用这种方式。

一般导入第三方组件的时候使用,如注册一个 RedisTemplate :

一般快速导入一批组件时使用,如同时注册好几个动物类:

容器中的Bean:

只有动物园里面有 猫和狗的时候我么才将猪注入进去。ImportBeanDefinitionRegistrar注册器,在注册bean的过程中会在最后执行。

输出结果:

以下是他们在实现依赖注入时执行顺序的概括:

@Autowired and @Inject

@Resource

>

@Target({ElementTypeMETHOD, ElementTypeANNOTATION_TYPE}) :注解作用在方法上或者注解上

@Retention(RetentionPolicyRUNTIME) :保留策略——>运行时保留

@Documented :表明这个注解应该被 javadoc工具记录 默认情况下,javadoc是不包括注解的

value() , name() :互为别名,为bean起一个名字,可以通过名字从spring容器中拿到bean对象。如果不设置,默认为@Bean修饰的方法名。

autowire()

指定 bean 的装配方式, 根据名称和类型装配,一般不设置,采用默认即可。

initMethod() :指定对象初始化时调用的方法

destroyMethod() :指定对象销毁时调用的方法

实体类

配置类

测试类

测试结果

通过测试结果可以看出,initMethod在对象构造方法执行时候就会调用,destroyMethod 在容器摧毁的时候会调用。

现在我们再看看当前容器里bean对应的名称是什么

配置类

测试类

测试结果

通过结果可以看出bean的名称就是我们手动的name属性

这时候小伙伴们肯定在想,如果我不指定bean的名称呢?

配置类

测试类

测试结果

通过测试结果可以看到,如果我们不手动指定name,就按照方法名作为bean的名称。

目录

Spring 作为 Ioc 框架,实现了依赖注入,由一个中心化的 Bean 工厂来负责各个 Bean 的实例化和依赖管理。各个 Bean 可以不需要关心各自的复杂的创建过程,达到了很好的解耦效果。

我们对 Spring 的工作流进行一个粗略的概括,主要为两大环节:

我们假设所有的配置和扩展类都已经装载到了 ApplicationContext 中,然后具体的分析一下 Bean 的加载流程。

思考一个问题,抛开 Spring 框架的实现,假设我们手头上已经有一套完整的 Bean Definition Map,然后指定一个 beanName 要进行实例化,需要关心什么?即使我们没有 Spring 框架,也需要了解这两方面的知识:

Spring 进行了抽象和封装,使得作用域和依赖关系的配置对开发者透明,我们只需要知道当初在配置里已经明确指定了它的生命周期和依赖了谁,至于是怎么实现的,依赖如何注入,托付给了 Spring 工厂来管理。

Spring 只暴露了很简单的接口给调用者,比如 getBean :

那我们就从 getBean 方法作为入口,去理解 Spring 加载的流程是怎样的,以及内部对创建信息、作用域、依赖关系等等的处理细节。

上面是跟踪了 getBean 的调用链创建的流程图,为了能够很好地理解 Bean 加载流程,省略一些异常、日志和分支处理和一些特殊条件的判断。

从上面的流程图中,可以看到一个 Bean 加载会经历这么几个阶段(用绿色标记):

整个流程最为复杂的是对循环依赖的解决方案,后续会进行重点分析。

而在我们解析完配置后创建的 Map,使用的是 beanName 作为 key。见 DefaultListableBeanFactory:

BeanFactorygetBean 中传入的 name,有可能是这几种情况:

为了能够获取到正确的 BeanDefinition,需要先对 name 做一个转换,得到 beanName。

见 AbstractBeanFactorydoGetBean :

如果是 alias name ,在解析阶段,alias name 和 bean name 的映射关系被注册到 SimpleAliasRegistry 中。从该注册器中取到 beanName。见 SimpleAliasRegistrycanonicalName :

如果是 factorybean name ,表示这是个工厂 bean,有携带前缀修饰符 & 的,直接把前缀去掉。见 BeanFactoryUtilstransformedBeanName :

我们从配置文件读取到的 BeanDefinition 是 GenericBeanDefinition 。它的记录了一些当前类声明的属性或构造参数,但是对于父类只用了一个 parentName 来记录。

接下来会发现一个问题,在后续实例化 Bean 的时候,使用的 BeanDefinition 是 RootBeanDefinition 类型而非 GenericBeanDefinition 。这是为什么?

答案很明显,GenericBeanDefinition 在有继承关系的情况下,定义的信息不足:

为了能够正确初始化对象,需要完整的信息才行 。需要递归 合并父类的定义

见 AbstractBeanFactorydoGetBean :

在判断 parentName 存在的情况下,说明存在父类定义,启动合并。如果父类还有父类怎么办?递归调用,继续合并。

见 AbstractBeanFactorygetMergedBeanDefinition 方法:

每次合并完父类定义后,都会调用 RootBeanDefinitionoverrideFrom 对父类的定义进行覆盖,获取到当前类能够正确实例化的 全量信息

什么是循环依赖?

举个例子,这里有三个类 A、B、C,然后 A 关联 B,B 关联 C,C 又关联 A,这就形成了一个循环依赖。如果是方法调用是不算循环依赖的,循环依赖必须要持有引用。

循环依赖根据注入的时机分成两种类型:

如果是构造器循环依赖,本质上是无法解决的 。比如我们准调用 A 的构造器,发现依赖 B,于是去调用 B 的构造器进行实例化,发现又依赖 C,于是调用 C 的构造器去初始化,结果依赖 A,整个形成一个死结,导致 A 无法创建。

如果是设值循环依赖,Spring 框架只支持单例下的设值循环依赖 。Spring 通过对还在创建过程中的单例,缓存并提前暴露该单例,使得其他实例可以引用该依赖。

Spring 不支持原型模式的任何循环依赖 。检测到循环依赖会直接抛出 BeanCurrentlyInCreationException 异常。

使用了一个 ThreadLocal 变量 prototypesCurrentlyInCreation 来记录当前线程正在创建中的 Bean 对象,见 AbtractBeanFactory#prototypesCurrentlyInCreation :

在 Bean 创建前进行记录,在 Bean 创建后删除记录。见 AbstractBeanFactorydoGetBean :

见 AbtractBeanFactorybeforePrototypeCreation 的记录 *** 作:

见 AbtractBeanFactorybeforePrototypeCreation 的删除 *** 作:

为了节省内存空间,在单个元素时 prototypesCurrentlyInCreation 只记录 String 对象,在多个依赖元素后改用 Set 集合。这里是 Spring 使用的一个节约内存的小技巧。

了解了记录的写入和删除过程好了,再来看看读取以及判断循环的方式。这里要分两种情况讨论。

这两个地方的实现略有不同。

如果是构造函数依赖的,比如 A 的构造函数依赖了 B,会有这样的情况。实例化 A 的阶段中,匹配到要使用的构造函数,发现构造函数有参数 B,会使用 BeanDefinitionValueResolver 来检索 B 的实例。见 BeanDefinitionValueResolverresolveReference :

我们发现这里继续调用 beanFactorygetBean 去加载 B。

如果是设值循环依赖的的,比如我们这里不提供构造函数,并且使用了 @Autowire 的方式注解依赖(还有其他方式不举例了):

加载过程中,找到无参数构造函数,不需要检索构造参数的引用,实例化成功。接着执行下去,进入到属性填充阶段 AbtractBeanFactorypopulateBean ,在这里会进行 B 的依赖注入。

为了能够获取到 B 的实例化后的引用,最终会通过检索类 DependencyDescriptor 中去把依赖读取出来,见 DependencyDescriptorresolveCandidate :

发现 beanFactorygetBean 方法又被调用到了。

在这里,两种循环依赖达成了同一 。无论是构造函数的循环依赖还是设置循环依赖,在需要注入依赖的对象时,会继续调用 beanFactorygetBean 去加载对象,形成一个递归 *** 作。

而每次调用 beanFactorygetBean 进行实例化前后,都使用了 prototypesCurrentlyInCreation 这个变量做记录。按照这里的思路走,整体效果等同于 建立依赖对象的构造链

prototypesCurrentlyInCreation 中的值的变化如下:

调用判定的地方在 AbstractBeanFactorydoGetBean 中,所有对象的实例化均会从这里启动。

判定的实现方法为 AbstractBeanFactoryisPrototypeCurrentlyInCreation :

所以在原型模式下,构造函数循环依赖和设值循环依赖,本质上使用同一种方式检测出来。Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。

Spring 也不支持单例模式的构造循环依赖 。检测到构造循环依赖也会抛出 BeanCurrentlyInCreationException 异常。

和原型模式相似,单例模式也用了一个数据结构来记录正在创建中的 beanName。见 DefaultSingletonBeanRegistry :

会在创建前进行记录,创建化后删除记录。

见 DefaultSingletonBeanRegistrygetSingleton

记录和判定的方式见 DefaultSingletonBeanRegistrybeforeSingletonCreation :

这里会尝试往 singletonsCurrentlyInCreation 记录当前实例化的 bean。我们知道 singletonsCurrentlyInCreation 的数据结构是 Set,是不允许重复元素的, 所以一旦前面记录了,这里的 add *** 作将会返回失败

比如加载 A 的单例,和原型模式类似,单例模式也会调用匹配到要使用的构造函数,发现构造函数有参数 B,然后使用 BeanDefinitionValueResolver 来检索 B 的实例,根据上面的分析,继续调用 beanFactorygetBean 方法。

所以拿 A,B,C 的例子来举例 singletonsCurrentlyInCreation 的变化,这里可以看到和原型模式的循环依赖判断方式的算法是一样:

单例模式下,构造函数的循环依赖无法解决,但设值循环依赖是可以解决的

这里有一个重要的设计: 提前暴露创建中的单例

我们理解一下为什么要这么做。

还是拿上面的 A、B、C 的的设值依赖做分析,

=> 1 A 创建 -> A 构造完成,开始注入属性,发现依赖 B,启动 B 的实例化

=> 2 B 创建 -> B 构造完成,开始注入属性,发现依赖 C,启动 C 的实例化

=> 3 C 创建 -> C 构造完成,开始注入属性,发现依赖 A

重点来了,在我们的阶段 1中, A 已经构造完成,Bean 对象在堆中也分配好内存了,即使后续往 A 中填充属性(比如填充依赖的 B 对象),也不会修改到 A 的引用地址。

所以,这个时候是否可以 提前拿 A 实例的引用来先注入到 C ,去完成 C 的实例化,于是流程变成这样。

=> 3 C 创建 -> C 构造完成,开始注入依赖,发现依赖 A,发现 A 已经构造完成,直接引用,完成 C 的实例化。

=> 4 C 完成实例化后,B 注入 C 也完成实例化,A 注入 B 也完成实例化。

这就是 Spring 解决单例模式设值循环依赖应用的技巧。流程图为:

为了能够实现单例的提前暴露。Spring 使用了三级缓存,见 DefaultSingletonBeanRegistry :

这三个缓存的区别如下:

从 getBean("a") 开始,添加的 SingletonFactory 具体实现如下:

可以看到如果使用该 SingletonFactory 获取实例,使用的是 getEarlyBeanReference 方法,返回一个未初始化的引用。

读取缓存的地方见 DefaultSingletonBeanRegistry :

先尝试从 singletonObjects 和 singletonFactory 读取,没有数据,然后尝试 singletonFactories 读取 singletonFactory,执行 getEarlyBeanReference 获取到引用后,存储到 earlySingletonObjects 中。

这个 earlySingletonObjects 的好处是,如果此时又有其他地方尝试获取未初始化的单例,可以从 earlySingletonObjects 直接取出而不需要再调用 getEarlyBeanReference 。

从流程图上看,实际上注入 C 的 A 实例,还在填充属性阶段,并没有完全地初始化。等递归回溯回去,A 顺利拿到依赖 B,才会真实地完成 A 的加载。

获取到完整的 RootBeanDefintion 后,就可以拿这份定义信息来实例具体的 Bean。

具体实例创建见 AbstractAutowireCapableBeanFactorycreateBeanInstance ,返回 Bean 的包装类 BeanWrapper,一共有三种策略:

使用工厂方法创建,会先使用 getBean 获取工厂类,然后通过参数找到匹配的工厂方法,调用实例化方法实现实例化,具体见 ConstructorResolverinstantiateUsingFactoryMethod :

使用有参构造函数创建,整个过程比较复杂,涉及到参数和构造器的匹配。为了找到匹配的构造器,Spring 花了大量的工作,见 ConstructorResolverautowireConstructor :

使用无参构造函数创建是最简单的方式,见 AbstractAutowireCapableBeanFactoryinstantiateBean :

我们发现这三个实例化方式,最后都会走 getInstantiationStrategy()instantiate() ,见实现类 SimpleInstantiationStrategyinstantiate :

虽然拿到了构造函数,并没有立即实例化。因为用户使用了 replace 和 lookup 的配置方法,用到了动态代理加入对应的逻辑。如果没有的话,直接使用反射来创建实例。

创建实例后,就可以开始注入属性和初始化等 *** 作。

但这里的 Bean 还不是最终的 Bean。返回给调用方使用时,如果是 FactoryBean 的话需要使用 getObject 方法来创建实例。见 AbstractBeanFactorygetObjectFromBeanInstance ,会执行到 doGetObjectFromFactoryBean :

实例创建完后开始进行属性的注入,如果涉及到外部依赖的实例,会自动检索并关联到该当前实例。

Ioc 思想体现出来了。正是有了这一步 *** 作,Spring 降低了各个类之间的耦合。

属性填充的入口方法在 AbstractAutowireCapableBeanFactorypopulateBean 。

可以看到主要的处理环节有:

如果我们的 Bean 需要容器的一些资源该怎么办?比如需要获取到 BeanFactory、ApplicationContext 等等。

Spring 提供了 Aware 系列接口来解决这个问题。比如有这样的 Aware:

Spring 在初始化阶段,如果判断 Bean 实现了这几个接口之一,就会往 Bean 中注入它关心的资源。

见 AbstractAutowireCapableBeanFactoryinvokeAwareMethos :

在 Bean 的初始化前或者初始化后,我们如果需要进行一些增强 *** 作怎么办?

这些增强 *** 作比如打日志、做校验、属性修改、耗时检测等等。Spring 框架提供了 BeanPostProcessor 来达成这个目标。比如我们使用注解 @Autowire 来声明依赖,就是使用 AutowiredAnnotationBeanPostProcessor 来实现依赖的查询和注入的。接口定义如下:

实现该接口的 Bean 都会被 Spring 注册到 beanPostProcessors 中, 见 AbstractBeanFactory :

只要 Bean 实现了 BeanPostProcessor 接口,加载的时候会被 Spring 自动识别这些 Bean,自动注册,非常方便。

然后在 Bean 实例化前后,Spring 会去调用我们已经注册的 beanPostProcessors 把处理器都执行一遍。

这里使用了责任链模式,Bean 会在处理器链中进行传递和处理。当我们调用 BeanFactorygetBean 的后,执行到 Bean 的初始化方法 AbstractAutowireCapableBeanFactoryinitializeBean 会启动这些处理器。

自定义初始化有两种方式可以选择:

见 AbstractAutowireCapableBeanFactoryinvokeInitMethods :

Bean 已经加载完毕,属性也填充好了,初始化也完成了。

在返回给调用者之前,还留有一个机会对 Bean 实例进行类型的转换。见 AbstractBeanFactorydoGetBean :

抛开一些细节处理和扩展功能,一个 Bean 的创建过程无非是:

获取完整定义 -> 实例化 -> 依赖注入 -> 初始化 -> 类型转换。

作为一个完善的框架,Spring 需要考虑到各种可能性,还需要考虑到接入的扩展性。

所以有了复杂的循环依赖的解决,复杂的有参数构造器的匹配过程,有了 BeanPostProcessor 来对实例化或初始化的 Bean 进行扩展修改。

先有个整体设计的思维,再逐步击破针对这些特殊场景的设计,整个 Bean 加载流程迎刃而解。

以上就是关于Feign源码解析二全部的内容,包括:Feign源码解析二、JSP+Servlet+JavaBean中(注册)问题、Spring Bean 的注册和注入的几种常用方式和区别等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/web/9376596.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-27
下一篇2023-04-27

发表评论

登录后才能评论

评论列表(0条)

    保存