Spring 中 @Bean 注解流程分析

代码案例

现在 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);

	}

	@Bean
	public Blue blue() {
		System.out.println("======>invoke blue...");
		return new Blue();
	}
}

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

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

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

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

注意这里的判断条件 mbd.getFactoryMethodName() != nul 当使用了 @Bean 注解或者 xml 中配置了 <bean factory-method=‘…’> 才会成立。

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

在这里插入图片描述

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

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

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

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

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

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

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

public class Apple {
}

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

	@Bean
	public 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);

	}
	
	@Bean
	public Blue blue(Apple apple3) {
		System.out.println("======>invoke blue..." + apple3);
		return new Blue();
	}
	
	@Bean
	public 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);
	}

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

	@Bean
	public 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);
	}

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

	@Bean
	public Apple apple1() {
		System.out.println("--apple1....");
		return new Apple();
	}

	@Bean
	public 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);
	}

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

	@Bean
	@Primary
	public Apple apple1() {
		System.out.println("--apple1....");
		return new Apple();
	}

	@Bean
	public 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);
	}

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

	@Bean
	public Apple apple1() {
		System.out.println("--apple1....");
		return new Apple();
	}

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

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

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

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

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

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值