Spring 中 @Bean 注解流程分析
创始人
2024-03-29 07:57:43

代码案例

现在 SpringBoot、SpringCloud 基本上都是通过 @Bean 注解来将组件交给 Spring 管理,所以对 @Bean 的流程应该要有所了解。

这里先定义一个 Blue 的实体类,如下:

public class Blue {
}

然后定义一个入口类,通过 @Bean 注解将 Blue 交给 Spring 管理,如下:

@BeansScanner(basePackage = "com.gwm.scan.beans")
public class TestBean {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(TestBean.class);Blue blue = context.getBean(Blue.class);System.out.println("blue = " + blue);}@Beanpublic Blue blue() {System.out.println("======>invoke blue...");return new Blue();}
}

@Bean 扫描解析 BeanDefinition 阶段先搁一边后面补齐。。。。

@BeansScanner 注解不用太在意,只是一个自己模拟 @ComponentScan 注解写的。这主要分析 @Bean 执行流程。

首先看到 Spring 中的一段源码,如下所示:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

注意这里的判断条件 mbd.getFactoryMethodName() != nul 当使用了 @Bean 注解或者 xml 中配置了 才会成立。

现在我们已经在 TestBean 类中使用了 @Bean 注解,所以这里当加加载到 TestBean 类的时候,源码中的判断条件就成立。直接进入 instantiateUsingFactoryMethod() 方法内部,如下(直接进入核心代码部分):

在这里插入图片描述

这里 mbd.getFactoryBeanName() 获取到的是 @Bean 注解所在的类的名称,然后再通过 beanFactory.getBean() 拿到这个类的对象,因为最终要通过反射区调用这个 @Bean 修饰的方法,所以肯定是要先获取到这个类的对象,才能够调用 @Bean 修饰的方法。

继续追踪源码看下在哪里反射调用

在这里插入图片描述
在这里插入图片描述

可以看到通过 factoryMethod.invoke(factoryBean,args) 反射调用 @Bean 修饰的方法,然后把 result 结果设置到 BeanWrapper 中。至此 Blue 实例就通过 @Bean 注解交给了 Spring 管理。

加入现在 @Bean 修饰的方法中有参数呢?如下所示:

	@Beanpublic Blue blue(Apple apple3) {System.out.println("======>invoke blue..." + apple3);return new Blue();}

现在 blue() 方法中有一个入参 Apple,那么我们先定义好这个 Apple 类,如下所示:

public class Apple {
}

这里依旧通过 @Bean 将 Apple 交给 Spring 去管理,代码如下:

	@Beanpublic Apple apple2() {System.out.println("--apple2....");return new Apple();}

整体代码如下:

@BeansScanner(basePackage = "com.gwm.scan.beans")
public class TestBean {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(TestBean.class);Blue blue = context.getBean(Blue.class);System.out.println("blue = " + blue);}@Beanpublic Blue blue(Apple apple3) {System.out.println("======>invoke blue..." + apple3);return new Blue();}@Beanpublic Apple apple2() {System.out.println("--apple2....");return new Apple();}
}

Apple 的执行流程和不带参数 Blue 的流程解析一样,不过多赘述。这里看到 blue() 方法中有一个参数 Spring 是如何执行的。直接进入到源码如下:

在这里插入图片描述
在这里插入图片描述

这里获取到 factoryMethod 方法,也就是被 @Bean 修饰的方法。然后调用 createArgumentArray() 方法对参数进行赋值操作。可以看到这里获取到所有的了 paramTypes[]、paramNames[]。并且底层会调用到 getBean() 去实例化参数。就是会给参数赋上值。

在这里插入图片描述

继续跟踪源码,如下:

在这里插入图片描述

可以发现是通过 for 循环对获取到的所有 paramTypes 参数类型逐一赋值。所以如果你参数过多的话,也可能导致性能问题,这个得注意下。我们这里目前设置一个参数方便观察执行流程。

在这里插入图片描述

这里着重记忆下 resolveDependency() 方法,因为后面对于属性填充基本都是借助这个方法,这个方法会触发 getBean() 实例化操作。

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

findAutowireCandidates() 中有一个非常重要的逻辑,就是通过类型获取到所有的 beanName,beanNamesForTypeIncludingAncestors() 方法可以获取到父子类容器中满足条件的所有 beanName,这里我们在看下 @Bean 修饰的方法

 @BeansScanner(basePackage = "com.gwm.scan.beans")
public class TestBean {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestBean.class);Blue blue = context.getBean(Blue.class);System.out.println("blue = " + blue);}@Beanpublic Blue blue(Apple apple3) {System.out.println("======>invoke blue..." + apple3);return new Blue();}@Beanpublic Apple apple2() {System.out.println("--apple2....");return new Apple();}
}

可以清楚的看到我们在 blue() 方法中 Apple 类型的参数名称为 apple3,第一眼看过去很容易让人造成一种误解,就是会认为是根据 beanName = apple3 名称去容器中找到对应的 Apple 实例,但是这个理解有点小小的不足。下下说说这个具体的流程:

1、在 Spring 在扫描封装 BeanDefinition 阶段,会把带 @Bean 修饰的方法,封装成一个个的 BeanDefinition,并且以方法名称作为 beanName,就比如 blue() 和 apple2() 两个方法,beanName 分别是 blue、apple2。
 
2、beanName 确定好,在 Spring 实例化完成之后,容器中就可以根据 beanName 去找到对应的实例 bean,在这里可以根据 beanName = blue 找到 Blue 的实例 bean,根据 beanName = apple2 找到 Apple 的实例化 bean,但是你想根据 beanName = apple3 去找到 Apple 的实例,绝对不可能的,因为容器中就没有 beanName = apple3 的实例 bean
 
3、apple3 被 Spring 被封装成在 DependencyDescriptor 对象中,后面会讲到有什么作用。
 
4、既然根据 beanName = apple3 找不到对应的实例 bean,那么怎么给 blue() 方法中的参数赋值呢?所以 Spring 在这里就利用 beanNamesForTypeIncludingAncestors() 方法, 根据 Apple 类型去 BeanDefinitionMap 中对应的 beanName,在 Spring 在扫描封装 BeanDefinition 阶段,已经将 beanName = apple2 封装到了 BeanDefinitionMap 容器,自然而然可以拿到 apple2 对应的实例。
 
5、所以得出结论,如果只配置了一个类型 bean 的话,@Bean 修饰的方法中属性是通过类型进行注入值的。

那么这里扩展下,如果我们配置了两个或者更多相同类型的 bean?那么在 blue() 方法中 Spring 知道该注入哪个实例 bean ? 如下代码:

@BeansScanner(basePackage = "com.gwm.scan.beans")
public class TestBean {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestBean.class);Blue blue = context.getBean(Blue.class);System.out.println("blue = " + blue);}@Beanpublic Blue blue(Apple apple3) {System.out.println("======>invoke blue..." + apple3);return new Blue();}@Beanpublic Apple apple1() {System.out.println("--apple1....");return new Apple();}@Beanpublic Apple apple2() {System.out.println("--apple2....");return new Apple();}
}

首先这里是绝对会报错的,因为在 blue() 方法中 Spring 根本不知道该如何选择注入哪个 Apple 实例 bean。追踪报错的源码如下:

首先 beanNamesForTypeIncludingAncestors() 方法会获取到 beanName=apple1、beanName=apple2 名称

在这里插入图片描述

然后再进入 determineAutowireCandidate() 方法,这个方法要决策出来一个能够注入的候选 beanName,如果这里面没有选择出来结果,就会报错。那么这段逻辑就显得格外重要了,现在进入内部逻辑。

在这里插入图片描述

进入 determineAutowireCandidate() 逻辑,如下所示:

在这里插入图片描述

determineAutowireCandidate() 方法的内部逻辑简述如下:

1、candidates 就是我们上面通过类型从 BeanDefinitionMap 中找到的 beanNames(apple1、apple2),descriptor 就是我们上面说的 DependencyDescriptor 对象,将 blue() 方法中的入参 apple3 进行包装了。
 
2、determinePrimaryCandidate() 方法会在 apple1、apple2 中查找是否标注了 @Primary 注解,如果找到了,Spring 立即就返回,当做是 blue() 方法中入参的值,其他的就都不管了。
 
3、determineHighestPriorityCandidate() 方法会在 apple1、apple2 中查找是否实现了 Comparator 排序接口,如果找到了,Spring 立即就返回,当做是 blue() 方法中入参的值,其他的就都不管了。
 
4、如果上述两个都没有找到,最后兜底的方法,就是从 DependencyDescriptor 对象中取出之前封装的 beanName=apple3 去和 beanNames(apple1、apple2)匹配,如果匹配到了一个就会立即返回,当做是 blue() 方法中入参的值,其他的就都不管了。

所以从 determineAutowireCandidate() 方法我们可以得出几个结论:

在多个相同类型的 @Bean 同时存在时,Spring 优先找 @Primary 注解,找不到在找是否实现了 Comparator 排序接口,还找不到在用 DependencyDescriptor 入参和 BeanDefinitionMap 中的 beanName 匹配,如果匹配也不成,那么就会抛出 NoUniqueBeanDefinitionException 异常,异常如下:

在这里插入图片描述
在这里插入图片描述

所以在相同类型 @Bean 同时存在时,可以加 @Primary 注解,或指定其中一个具体的 beanName,如下所示:

加 @Primary 注解:

@BeansScanner(basePackage = "com.gwm.scan.beans")
public class TestBean {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestBean.class);Blue blue = context.getBean(Blue.class);System.out.println("blue = " + blue);}@Beanpublic Blue blue(Apple apple3) {System.out.println("======>invoke blue..." + apple3);return new Blue();}@Bean@Primarypublic Apple apple1() {System.out.println("--apple1....");return new Apple();}@Beanpublic Apple apple2() {System.out.println("--apple2....");return new Apple();}
}

指定具体名称 apple1、或者 apple2:

@BeansScanner(basePackage = "com.gwm.scan.beans")
public class TestBean {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestBean.class);Blue blue = context.getBean(Blue.class);System.out.println("blue = " + blue);}@Beanpublic Blue blue(Apple apple1) {System.out.println("======>invoke blue..." + apple3);return new Blue();}@Beanpublic Apple apple1() {System.out.println("--apple1....");return new Apple();}@Beanpublic Apple apple2() {System.out.println("--apple2....");return new Apple();}
}

所以最终 @Bean 执行流程主要关注 beanNamesForTypeIncludingAncestors() 和 determineAutowireCandidate() 方法即可。

继续回到主线,如下所示:最终发现会调用到 getBean() 去实例化 Apple

在这里插入图片描述
在这里插入图片描述

到此 @Bean 执行流程和 @Bean 带参数的执行流程就先到这。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...