代码案例
现在 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 带参数的执行流程就先到这。