写点什么

java 培训 SpringBoot 自动装配原理

作者:@零度
  • 2022 年 4 月 12 日
  • 本文字数:13290 字

    阅读完需:约 44 分钟

以下文章来源于 Java 团长

1. Warm up

在开始之前,让我们先来看点简单的开胃菜:spring 中 bean 注入的三种形式

首先我们先来一个 Person 类,这里为了篇幅长度考虑使用了 lombok

如果你不知道 lombok 是什么,那就最好不要知道,加了几个注解之后我的 pojo 类 Person 就完成了

/**

* @author dzzhyk

*/

@Data

@NoArgsConstructor

@AllArgsConstructor

public class Person {

private String name;

private Integer age;

private Boolean sex;

}

在 Spring 中(不是 Spring Boot),要实现 bean 的注入,我们有 3 种注入方式:

1.1 setter 注入

这是最基本的注入方式

首先我们创建 applicationContext.xml 文件,在里面加入:

<!-- 手动配置 bean 对象 -->

<bean id="person" class="pojo.Person">

<property name="name" value="dzzhyk"/>

<property name="age" value="20"/>

<property name="sex" value="true"/>

</bean>

这里使用 property 为 bean 对象赋值

紧接着我们会在 test 包下写一个 version1.TestVersion1 类

/**

* 第一种 bean 注入实现方式 - 在 xml 文件中直接配置属性

*/

public class TestVersion1 {

@Test

public void test(){

ApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");

Person person = ca.getBean("person", Person.class);

System.out.println(person);

}

}

这里我使用了 ClassPathXmlApplicationContext 来加载 spring 配置文件并且读取其中定义的 bean,然后使用 getBean 方法使用 id 和类来获取这个 Person 的 Bean 对象,结果成功输出:

Person(name=dzzhyk, age=20, sex=true)

1.2 构造器注入

接下来是使用构造器注入,我们需要更改 applicationContext.xml 文件中的 property 为 construct-arg

<!-- 使用构造器 -->

<bean id="person" class="pojo.Person">

<constructor-arg index="0" type="java.lang.String" value="dzzhyk" />

<constructor-arg index="1" type="java.lang.Integer" value="20"/>

<constructor-arg index="2" type="java.lang.Boolean" value="true"/>

</bean>

version2.TestVersion2 内容不变:

public class TestVersion2 {

@Test

public void test(){

ApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");

Person person = ca.getBean("person", Person.class);

System.out.println(person);

}

}

依然正常输出结果:

Person(name=dzzhyk, age=20, sex=true)

1.3 属性注入

使用注解方式的属性注入 Bean 是比较优雅的做法

首先我们需要在 applicationContext.xml 中开启注解支持和自动包扫描:

<context:annotation-config />

<context:component-scan base-package="pojo"/>

在 pojo 类中对 Person 类加上 @Component 注解,将其标记为组件,并且使用 @Value 注解为各属性赋初值

@Component

public class Person {


@Value("dzzhyk")

private String name;

@Value("20")

private Integer age;

@Value("true")

private Boolean sex;

}

然后添加新的测试类 version3.TestVersion3

public class TestVersion3 {

@Test

public void test(){

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

Person person = ac.getBean("person", Person.class);

System.out.println(person);

}

}

运行也可以得到如下结果:

Person(name=dzzhyk, age=20, sex=true)

2. Warm up again

什么?还有什么?接下来我们来聊聊 Spring 的两种配置方式:基于 XML 的配置和基于 JavaConfig 类的配置方式,这对于理解 SpringBoot 的自动装配原理是非常重要的_java培训

首先我们在 Person 的基础上再创建几个 pojo 类:这个 Person 有 Car、有 Dog

public class Car {

private String brand;

private Integer price;

}

public class Dog {

private String name;

private Integer age;

}

public class Person {

private String name;

private Integer age;

private Boolean sex;

private Dog dog;

private Car car;

}

2.1 基于 XML 的配置

接下来让我们尝试使用 XML 的配置方式来为一个 Person 注入

<bean id="person" class="pojo.Person">

<property name="name" value="dzzhyk"/>

<property name="age" value="20"/>

<property name="sex" value="true"/>

<property name="dog" ref="dog"/>

<property name="car" ref="car"/>

</bean>

<bean id="dog" class="pojo.Dog">

<property name="name" value="旺财"/>

<property name="age" value="5" />

</bean>

<bean id="car" class="pojo.Car">

<property name="brand" value="奥迪双钻"/>

<property name="price" value="100000"/>

</bean>

然后跟普通的 Bean 注入一样,使用 ClassPathXmlApplicationContext 来加载配置文件,然后获取 Bean

/**

* 使用 XML 配置

*/

public class TestVersion1 {

@Test

public void test(){

ClassPathXmlApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");

Person person = ca.getBean("person", Person.class);

System.out.println(person);

}

}

输出结果如下:

Person(name=dzzhyk, age=20, sex=true, dog=Dog(name=旺财, age=5), car=Car(brand=奥迪双钻, price=100000))

2.2 基于 JavaConfig 类的配置

想要成为 JavaConfig 类,需要使用 @Configuration 注解

我们新建一个包命名为 config,在 config 中新增一个 PersonConfig 类

@Configuration

@ComponentScan

public class PersonConfig {

@Bean

public Person person(Dog dog, Car car){

return new Person("dzzhyk", 20, true, dog, car);

}

@Bean

public Dog dog(){

return new Dog("旺财", 5);

}

@Bean

public Car car(){

return new Car("奥迪双钻", 100000);

}

}

此时我们的 XML 配置文件可以完全为空了,此时应该使用 AnnotationConfigApplicationContext 来获取注解配置

/**

* 使用 JavaConfig 配置

*/

public class TestVersion2 {

@Test

public void test(){

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PersonConfig.class);

Person person = ac.getBean("person", Person.class);

System.out.println(person);

}

}

仍然正常输出了结果:

Person(name=dzzhyk, age=20, sex=true, dog=Dog(name=旺财, age=5), car=Car(brand=奥迪双钻, price=100000))

3. BeanDefinition

AbstractBeanDefinition

是 spring 中所有 bean 的抽象定义对象,我把他叫做 bean 定义

当 bean.class 被 JVM 类加载到内存中时,会被 spring 扫描到一个 map 容器中:

BeanDefinitionMap<beanName, BeanDefinition>

这个容器存储了 bean 定义,但是 bean 此时还没有进行实例化,在进行实例化之前,还有一个

BeanFactoryPostProcessor

可以对 bean 对象进行一些自定义处理

我们打开 BeanFactoryProcessor 这个接口的源码可以发现如下内容:

/*

* Modify the application context's internal bean factory after its standard

* initialization. All bean definitions will have been loaded, but no beans

* will have been instantiated yet. This allows for overriding or adding

* properties even to eager-initializing beans.

*/

在 spring 完成标准的初始化过程后,实现 BeanFactoryPostProcessor 接口的对象可以用于定制 bean factory,所有的 bean definition 都会被加载,但是此时还没有被实例化。这个接口允许对一些 bean 定义做出属性上的改动。

简言之就是实现了 BeanFactoryPostProcessor 这个接口的类,可以在 bean 实例化之前完成一些对 bean 的改动。

大致流程我画了个图:



至此我们能总结出 springIOC 容器的本质:(我的理解)

由 BeanDefinitionMap、BeanFactoryPostProcessor、BeanPostProcessor、BeanMap 等等容器共同组成、共同完成、提供依赖注入和控制反转功能的一组集合,叫 IOC 容器。

4. BeanDefinition 结构

既然讲到了 BeanDefinition,我们来看一下 BeanDefinition 里面究竟定义了些什么

让我们点进 AbstractBeanDefinition 这个类,一探究竟:



哇!好多成员变量,整个人都要看晕了 @_@

我们来重点关注以下三个成员:

private volatile Object beanClass;

private int autowireMode = AUTOWIRE_NO;

private ConstructorArgumentValues constructorArgumentValues;

4.1 beanClass

这个属性决定了该 Bean 定义的真正 class 到底是谁,接下来我们来做点实验

我们定义两个 Bean 类,A 和 B

@Component

public class A {

@Value("我是 AAA")

private String name;

}

@Component

public class B {

@Value("我是 BBB")

private String name;

}

接下来我们实现上面的 BeanFactoryPostProcessor 接口,来创建一个自定义的 bean 后置处理器

/**

* 自定义的 bean 后置处理器

* 通过这个 MyBeanPostProcessor 来修改 bean 定义的属性

* @author dzzhyk

*/

public class MyBeanPostProcessor implements BeanFactoryPostProcessor {

@Override

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

GenericBeanDefinition defA = (GenericBeanDefinition) beanFactory.getBeanDefinition("a");

System.out.println("这里是 MyBeanPostProcessor,我拿到了:" + defA.getBeanClassName());

}

}

最后在 XML 配置文件中开启包扫描

<context:component-scan base-package="pojo"/>

<context:annotation-config />

注意:这里不要使用 JavaConfig 类来配置 bean,不然会报如下错误

ConfigurationClassBeanDefinition cannot be cast to org.springframework.beans.factory.support.GenericBeanDefinition

这个错误出自这一句:

GenericBeanDefinition defA = (GenericBeanDefinition) beanFactory.getBeanDefinition("a");

最后,我们创建一个测试类:

public class Test {

@org.junit.Test

public void test(){

ClassPathXmlApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");

A aaa = ca.getBean("a", A.class);

System.out.println("最终拿到了==> " + aaa);

}

}

测试运行!

这里是 MyBeanPostProcessor,我拿到了:pojo.A

最终拿到了==> A(name=我是 AAA, b=B(name=我是 BBB))

可以看到 MyBeanPostProcessor 成功拿到了 A 的 Bean 定义,并且输出了提示信息

接下来让我们做点坏事

我们在 MyBeanPostProcessor 中修改 A 的 Bean 对象,将 A 的 beanClass 修改为 B.class

System.out.println("这里是 MyBeanPostProcessor,我修改了:"+ defA.getBeanClassName() + " 的 class 为 B.class");

// 把 A 的 class 改成 B

defA.setBeanClass(B.class);

重新运行 Test 类,输出了一些信息后:报错了!

这里是 MyBeanPostProcessor,我拿到了:pojo.A

这里是 MyBeanPostProcessor,我修改了:pojo.A 的 class 为 B.class

BeanNotOfRequiredTypeException:

Bean named 'a' is expected to be of type 'pojo.A' but was actually of type 'pojo.B'

我要拿到一个 A 类对象,你怎么给我一个 B 类对象呢?这明显不对

综上所述,我们可以得出 beanClass 属性控制 bean 定义的类

4.2 autowireMode

我们继续看第二个属性:autowireMode,自动装配模式

我们在 AbstractBeanDefinition 源码中可以看到:

private int autowireMode = AUTOWIRE_NO;

自动装配模式默认是 AUTOWIRE_NO,就是不开启自动装配

可选的常量值有以下四种:不自动装配,通过名称装配,通过类型装配,通过构造器装配

  • AUTOWIRE_NO

  • AUTOWIRE_BY_NAME

  • AUTOWIRE_BY_TYPE

  • AUTOWIRE_CONSTRUCTOR

接下来我们来模拟一个自动装配场景,仍然是 A 和 B 两个类,现在在 A 类中添加 B 类对象_java培训班

@Component

public class A {

@Value("我是 AAA")

private String name;

@Autowired

private B b;

}

我们希望 b 对象能够自动装配,于是我们给他加上了 @Autowired 注解,其他的完全不变,我们自定义的 MyBeanPostProcessor 中也不做任何操作,让我们运行测试类:

这里是 MyBeanPostProcessor,我拿到了:pojo.A

最终拿到了==> A(name=我是 AAA, b=B(name=我是 BBB))

自动装配成功了!我们拿到的 A 类对象里面成功注入了 B 类对象 b

现在问题来了,如果我把 @Autowired 注解去掉,自动装配会成功吗?

这里是 MyBeanPostProcessor,我拿到了:pojo.A

最终拿到了==> A(name=我是 AAA, b=null)

必然是不成功的

但是我就是想要不加 @Autowired 注解,仍然可以实现自动装配,需要怎么做?

这时就要在我们的 MyBeanPostProcessor 中做文章了,加入如下内容:

defA.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);

再输出结果:

这里是 MyBeanPostProcessor,我拿到了:pojo.A

最终拿到了==> A(name=我是 AAA, b=B(name=我是 BBB))

自动装配成功了!这次我们可没加 @Autowired,在我们的自定义的 bean 后置处理器中设置了 autowireMode 属性,也实现了自动装配

综上,autowireMode 属性是用来控制自动装配模式的,默认值是 AUTOWIRE_NO,即不自动装配

4.3 constructorArgumentValues

constructorArgumentValues 的字面含义是构造器参数值

改变这个参数值,我们可以做到在实例化对象时指定特定的构造器

话不多说,show me your code:

因为要研究构造器,只能先”忍痛“关掉 lombok 插件,手写一个 pojo.Student 类

/**

* Student 类

* @author dzzhyk

*/

@Component

public class Student {

private String name;

private Integer age;

public Student() {

System.out.println("==>使用空参构造器 Student()");

}

public Student(String name, Integer age) {

this.name = name;

this.age = age;

System.out.println("==>使用双参数构造器 Student(String name, Integer age)");

}

}

我们都知道,spring 在实例化对象时使用的是对象的默认空参构造器:

我们新建一个测试方法 test

@Test

public void test(){

ApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");

Student student = ca.getBean("stu", Student.class);

System.out.println("==>" + student);

}

运行可以得到下面结果:

这里是 MyBeanPostProcessor,我拿到了:pojo.Student

==>使用空参构造器 Student()

==>pojo.Student@402e37bc

可以看到,确实使用了空参构造器

但是如何指定(自定义)使用哪个构造器呢?我根本看不见摸不着,Spring 全帮我做了,实在是太贴心了。

接下来就聊聊 constructorArgumentValues 的使用:

我们在 MyBeanPostProcessor 中加入如下内容,对获取到的 pojo.Student 的 bean 定义进行操作:

ConstructorArgumentValues args = new ConstructorArgumentValues();

args.addIndexedArgumentValue(0, "我指定的姓名");

args.addIndexedArgumentValue(1, 20);

defStu.setConstructorArgumentValues(args);

再次运行 test:

这里是 MyBeanPostProcessor,我拿到了:pojo.Student

==>使用双参数构造器 Student(String name, Integer age)

==>pojo.Student@2f177a4b

可以看到这次使用了双参数构造器

有人会好奇 ConstructorArgumentValues 到底是个什么东西,我点进源码研究一番,结果发现这个类就是一个普通的包装类,包装的对象是 ValueHolder,里面一个 List 一个 Map<Integer, ValueHolder>

而 ValueHolder 这个对象继承于 BeanMetadataElement,就是构造器参数的一个包装类型

通过这个例子我们可以看到 ConstructorArgumentValues 就是用来管控构造器参数的,指定这个值会在进行 bean 注入的时候选择合适的构造器。

5. 装配对象

现在我们把目光放回到 SpringBoot 的自动装配上来,原来在真正进行 bean 实例化对象前,我们前面还有这些过程,尤其是存在使用后置处理器 BeanFactoryPostProcessor 来对 bean 定义进行各种自定义修改的操作。

经过上面我们漫长的研究过程,我们终于可以回答第一个问题了:

自动装配的对象:Bean 定义 (BeanDefinition)

6. My 自动装配

看到这里又自然会产生疑问:不会吧,上面可都是自动装配啊,我在配置文件或者使用注解都配置了变量的值,然后加个 @Autowired 注解就 OK 了,spring 也是帮我自动去装配。

再高端一点话,我就把 XML 文件写成 JavaConfig 配置类,然后使用 @Configuration 注解,这样也能自动装配,这不是很 nice 了吗?

6.1 自动装配之再思考

我的理解,上面的自动装配,我们至少要写一个配置文件,无论是什么形式,我们都至少需要一个文件把它全部写下来,就算这个文件的内容是固定的,但是为了装配这个对象,我们不得不写。

我们甚至都可以做成模板了,比如我在学习 spring 框架整合时,把经常写的都搞成了模板:



有了这些模板,我们只需要点点点,再进行修改,就能用了。

这样做确实很好,可是对于越来越成型的项目体系,我们每次都搞一些重复动作,是会厌烦的。而且面对这么多 xml 配置文件,我太难了。

于是我有了一个想说但不敢说的问题:

我一个配置文件都不想写,程序还能照样跑,我只关心有我需要的组件就可以了,我只需要关注我的目标就可以了,我想打开一个工程之后可以 1 秒进入开发状态,而不是花 3 小时写完配置文件(2.5 小时找 bug)希望有个东西帮我把开始之前的准备工作全做了,即那些套路化的配置,这样在我接完水之后回来就可以直接进行开发。

说到这里,想必大家都懂了:SpringBoot

6.2 一个例子

让我们在偷懒的道路上继续前进。

来看下面这个例子:

仍然是 A 类和 B 类,其中 A 类仍然引用了 B 类,我们给 A 类组件起 id=“a”,B 类组件起 id=“b”

@Component("a")

public class A {

@Value("我是 AAA")

private String name;

@Autowired

private B b;

}

@Component("b")

public class B {

@Value("我是 BBB")

private String name;

}

可以看到我们使用了 @Autowired 注解来自动注入 b,测试类如下:

@Test

public void test(){

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyAutoConfig.class);

A aaa = ac.getBean("a", A.class);

System.out.println(aaa);

}

细心的同学已经发现了:我们这里使用了 AnnotationConfigApplicationContext 这个 JavaConfig 配置类会使用到的加载类,于是我们顺利成章地点开它所加载的 MyAutoConfig 类文件

文件内容如下:

@Configuration

@MyEnableAutoConfig

public class MyAutoConfig {

// bean 都去哪了 ???

}

what? 我要声明的 Bean 对象都去哪了(注意:这里的 applicationContext.xml 是空的)?

让我们运行 test:

A(name=我是 AAA, b=B(name=我是 BBB))

竟然运行成功了,这究竟是为什么?(元芳,你怎么看?)

细心的同学已经发现了:@MyEnableAutoConfig 是什么注解?我怎么没有这个注解

让我们点进 @MyEnableAutoConfig 一探究竟:

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Import(MyImportSelector.class) // 导入 bean 定义

public @interface MyEnableAutoConfig {

}

原来如此!你是用了 @Import 注解导入了 Bean 定义对吧,注释都写着呢!

可是客官,@Import 导入 bean 定义是没错,但是它导入的是 MyImportSelector 这个 bean,不是 A 也不是 B 啊…

6.3 @Import 注解

@Import 的功能就是获取某个类的 bean 对象,他的使用形式大致如下:

@Import(A.class)

@Import(MyImportBeanDefinitionRegister.class)

@Import(MyImportSelector.class)

6.3.1 @Import(A.class)

第一种形式 @Import(A.class),是最简单易懂的形式

我们需要哪个 Bean 定义,直接 Import 他的 class 即可

6.3.2 @Import(MyImportBeanDefinitionRegister.class)

第二种形式 @Import(MyImportBeanDefinitionRegister.class)

传递了一个 bean 定义注册器,这个注册器的具体内容如下:

public class MyImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {

@Override

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {


RootBeanDefinition aDef = new RootBeanDefinition(A.class);

registry.registerBeanDefinition("a", aDef);


}

}

这个注册器实现了 ImportBeanDefinitionRegistrar 接口,并且重写了里面的 registerBeanDefinitions 方法

看他做了什么事:创建了一个新的 bean 定义,他的类型就是 A,然后把这个 bean 定义注册到 BeanDefinitionMap(还记得吧!)里面,key 值我们可以人为设置,这里就设置成"a"

这样在传递一个注册器的时候,我们就可以把注册器中新增的 bean 定义注册进来使用

6.3.3 @Import(MyImportSelector.class)

可以看到,这种使用方式就是我们刚才的注解中使用的方式

他传递了一个叫 MyImportSelector 的类,这个类依然是我们自己定义的,具体内容如下:

public class MyImportSelector implements ImportSelector {

@Override

public String[] selectImports(AnnotationMetadata importingClassMetadata) {

// 导入配置类

return new String[]{"config.MyConfig"};

}

}

这个类实现了 ImportSelector 接口,并且重写了 selectImports 方法,返回一个字符串数组

我们可以看到,返回的字符串数组中是我们要导入类的全类名

这个 Importer 返回的类如果是组件 bean 对象,就会被加载进来使用;如果是一个配置类,就会加载这个配置类

第三种和第二种的区别是第三种可以一次性写很多类,而且比较简洁,只需要清楚类的全包名即可。而第二种方式需要自己清楚包类名,手动创建 bean 定义,然后手动加入 BeanDefinitionMap。

6.4 例子的研究

我们打开 MyImportSelector,发现里面赫然写着几个大字:

return new String[]{"config.MyConfig"};

然后我们找到 config.MyConfig 类,发现这个类竟然就是我们刚才写的 JavaConfig 版本的配置文件:

@Configuration

public class MyConfig {

@Bean

public A a(){

return new A();

}

@Bean

public B b(){

return new B();

}

}

加载这个 MyConfig 配置类,就相当于加载了 A 和 B 两个 Bean 定义

喂!你是不是搞我!绕了一大圈,怎么还是加载这个配置文件啊!这个配置文件明明就是我自己写的。

总结一下,我们这个例子大概绕了这些过程:



6.5 将偷懒进行到底

"没有会偷懒的人解决不掉的问题“ —— 鲁迅

上面的例子也没有多大优化啊,我怎么觉得更加麻烦了?不但绕了一大圈,定义了许多新东西,到最后还是加载了我写好的 JavaConfig 类,说到底我不是还在写 javaConfig 类吗…

但是你注意到没有:有了上面的机制,我只需要把 JavaConfig 类写一次,然后放在某个地方,在 MyImportSelector 中加入这个地方的全包名路径,下次用的时候直接导入最顶层的 MyAutoConfig 类,所有有关这个部件我需要的东西,就全部自动整理好了,甚至比鼠标点点点添加代码模板还要快!

我突然有了个很棒的想法,不知道你有了没有 。

如果你开始有点感觉了,就会自然提出另一个问题:我这样做确实可以提高效率,但是一段代码里写入我自己定制的内容,每次更改起来不是太费劲了吗?

想到这里,我就不禁回想起使用 JDBC 的时候,在代码里改 SQL 语句的痛苦了,那真是生不如死…这种情况就构成了硬编码的行为,是不好的。

我们自然会想到:要是我创建一个配置文件 properties 来专门保存我这个需求所使用的 bean 对象,然后使用的时候在 MyImportSelector 中读取配置文件并且返回全包名,不就更加 nice 了吗?

于是 MyImportSelector 中的代码又改成了下面这样:

public class MyImportSelector implements ImportSelector {

@Override

public String[] selectImports(AnnotationMetadata importingClassMetadata) {


Properties properties = MyPropertyReader.readPropertyForMe("/MyProperty.properties");

String strings = (String) properties.get(MyEnableAutoConfig.class.getName());


return new String[]{strings};

}

}

其中 MyPropertyReader 是我们自己新创建的用于读取 properties 文件的工具类

之所以要自己再定义这样一个工具类,是为了以后在其中可以做一些其他操作(比如:去重、预检查)

public class MyPropertyReader {

public static Properties readPropertyForMe(String path){

Properties properties = new Properties();

try(InputStream sin = MyPropertyReader.class.getResourceAsStream(path)){

properties.load(sin);

}catch (IOException e){

e.printStackTrace();

System.out.println("读取异常...");

}

return properties;

}

}

我们的配置文件里面这么写:

anno.MyEnableAutoConfig=config.MyConfig

可以看到,key 是注解 @MyEnableAutoConfig 的类名,也就是根据这个注解,就会导入后面的 MyConfig 这个 Bean,这个 Bean 就是我们的配置文件

如此一来我们读取这个配置文件,然后加载跟这个注解名称相符的 value(即 JavaConfig 配置文件),就相当于我们在代码里手写的"config.MyConfig",只不过现在的形式已经发生了巨大的变化:我们添加或者删除一个配件,完全只需要修改 MyProperty.properties 这个配置文件就行了!

至此,无论是添加或者删除组件,无非是在配置文件中加上或者删除一行的问题了。

让我们在更新之后运行程序,可以看到成功拿到了配置文件的全类名


程序的运行当然也是没问题的:

A(name=我是 AAA, b=B(name=我是 BBB))

到此,我仿佛又领悟了一些东西。。。

我的配置文件好像活了,在我需要的时候他会出现,在我不需要的时候只需要在配置文件里面给他”打个叉“,他自己就跑开了

7. 自动装配源码分析

终于来到了大家喜闻乐见的部分:源码分析

在我们前面 6 节学习了各种”招式“之后,让我们请出对手:SpringBoot

现在在你面前的是一个 SpringBoot”空项目“,没有添加任何依赖包和 starter 包



启动项目:



正常启动,让我们从 @SpringBootApplication 开始研究

7.1 @SpringBootConfiguration

会看到 @SpringBootApplication 这个注解由好多注解组成

主要的有以下三个:

@SpringBootConfiguration

@EnableAutoConfiguration

@ComponentScan

先来看第一个:@SpringBootConfiguration

进入这个注解之后会发现



原来你就是一个 @Configuration 啊,一个 JavaConfig 配置类

那我们使用 JavaConfig 不就是用来配置 bean 吗,所以有了这个注解之后我们可以在 SpringBoot 运行的主类中使用 @Bean 标签配置类了,如下图所示:



7.2 @ComponentScan

这个注解相信大家都认识了,组件扫描

这个扫描的范围是:SpringBoot 主启动类的同级路径及子路径



7.3 @EnableAutoConfiguration

来看这个注解,也是最核心的内容

这个注解怎么这么眼熟啊,还记得刚才的 @MyEnableAutoConfig 注解吗?就是我们自己写的那个注解

进入 @EnableAutoConfiguration:



看图中红圈位置的注解:@Import(AutoConfigurationImportSelector.class)

是不是跟我们上面自己写的内容一样!

这里的作用便是导入了 AutoConfigurationImportSelector 这个类的 bean 定义

我们都知道,如果这个类实现了 ImportSelector 接口,那他肯定重写了一个方法,就是我们上面重写过的 selectImports 方法:

果然,在这个类里面确实有这个 selectImports 方法:



我的天,好长的一串代码,一行都放不下!

此时此刻,我又回想起了在家乡的母亲,夏天的蝉鸣,池塘的荷花…

等等等等,这个类我们当时返回的是什么?是一个字符串数组 String[ ],那这个类无论多么长,返回的肯定就是一个字符串数组,不信你自己看:



这个字符串数组存放的内容我们是否清楚呢?当然清楚了!我们返回的是要加载的 Config 配置文件的全包名,通过返回这个全包名,我们就能自动装配上这些配置文件下定义的 bean 对象,从而达到了自动装配的目的!

根据刚才我们自己实现的 selectImports 方法,我们是通过注解类的名字来查找,并且最终得到需要加载的 Config 类的全类名,最后返回的。

因此,这里必然有一个根据注解类名字来查找相应的 Config 文件的操作

我们继续反推,看到返回时的定义如下:


我们发现 autoConfigurationEntry 中保存着我们需要的配置信息,它是通过 getAutoConfigurationEntry 方法获取的,于是我们继续深入,进入 getAutoConfigurationEntry 方法


这一段代码真是把人难住了,好大一片,不知道在做什么

此时此刻,我又回想起了在家乡的母亲,夏天的蝉鸣,池塘的荷花…

回家!有了!我们先想这个方法应该返回什么,根据我们前面的经验,这里应该返回一个类似于 Entry 的保存了我们需要的配置信息的对象

这个方法返回的是新建的 AutoConfigurationEntry 对象,根据最后一行的构造函数来看,给他了两个参数:

configurations, exclusions

configurations 显然使我们需要的配置文件,也是我们最关心的,而 exclusions 字面意思是排除,也就是不需要的,那我们接下来应该关注 configurations 到底是怎么来的

根据我们前面的经验,我们是根据注解类名来从一个配置文件中读取出我们需要的 Config 配置类,这里 configurations 就代表了 Config 配置类,那么我们应该找到一个入口,这个入口跟注解相关,并且返回了 configurations 这个参数。

正如我们所料,这个方法的参数确实传递过来了一个东西,跟注解有关:

看见那个大大的 Annotation(注解)了吗!


那么根据这条”线索“,我们按图索骥,找到了三行代码,范围进一步缩小了!

此时再加上返回了 configurations,我们最终确定了一行代码:



就是这个 getCandidateConfigurations 方法,符合我们的要求!

从字面意思上分析,获取候选的配置,确实是我们需要的方法

OK,让我们继续前进,进入这个方法:



这个方法是不是也似曾相识呢?我们之前写过一个专门用于读取配置文件的类 MyPropertyReader,还记得吗?

如果你还记得的话,我们自己写的工具类里面也是一个静态方法 readPropertyForMe 来帮我读取配置文件

但是我们的配置文件路径一定是需要指定的,不能乱放。

从这个 loadFactoryNames 方法体来看,好像没有给他传递一个具体路径

但是从下面的 Assert 断言中,我们发现了玄机:


在 META-INF/spring.factories 文件中没有找到自动配置类 Config,你要检查 balabala。。。。

根据我不太灵光的脑袋的判断,他的这个配置文件就叫 spring.factories,存放的路径是 META-INF/spring.factories

于是我们打开 spring boot 自动装配的依赖 jar 包:



那这个配置文件里面的内容,是不是跟我们想的一样呢?


原来如此。

这里的 EnableAutoConfiguration 注解,正是我们此行的起点啊…

到这里,自动装配到底是什么,应该比较清楚了,www.atguigu.com 原来他是帮我们加载了各种已经写好的 Config 类文件,实现了这些 JavaConfig 配置文件的重复利用和组件化

7.4 loadFactoryNames 方法

行程不能到此结束,学习不能浅尝辄止。

我们还有最后一块(几块)面纱没有解开,现在还不能善罢甘休。

让我们进入 loadFactoryNames 方法:


这个方法非常简短,因为他调用了真正实现的方法:loadSpringFactories

这一行 return 代码我复制在下面:

loadSpringFactories(classLoader)

.getOrDefault(factoryTypeName, Collections.emptyList());

可以分析得出:loadSpringFactories 方法的返回值又调用了一个 getOrDefault 方法,这明显是一个容器类的方法,目的是从容器中拿点东西出来

就此推测:loadSpringFactories 返回了一个包含我们需要的 Config 全类名(字符串)的集合容器,然后从这个集合容器中拿出来的东西就是我们的 configurations

让我们看这个 loadSpringFactories 方法:


它确实返回了一个容器:Map<String, List> 这个容器的类型是:MultiValueMap<String, String>

这个数据结构就非常牛逼了,多值集合映射(我自己的翻译)简单来说,一个 key 可以对应多个 value,根据他的返回值,我们可以看到在这个方法中一个 String 对应了一个 List

那么不难想到 MultiValueMap 中存放的形式:是”注解的类名——多个 Config 配置类“ 让我们打个断点来验证一下:



果然是这样,并且 @EnableAutoConfiguration 注解竟然加载了多达 124 个配置类!

接下来我们继续思考:我们来的目的是获取 configurations,所以无论你做什么,必须得读取配置文件,拿到 configurations

于是我们在 try 方法体中果然发现了这个操作:



他获取了一个路径 urls,那么这个路径是否就是我们前面验证的 META-INF/spring.factories 呢?

我们查看静态常量 FACTORIES_RESOURCE_LOCATION 的值:



果真如此,bingo!继续往下看,果然他遍历了 urls 中的内容,从这个路径加载了配置文件:终于看到了我们熟悉的 loadProperties 方法!



那我们大概就知道了,他确实是通过找到路径,然后根据路径读取了配置文件,然后返回了读取的 result

这就是 loadFactoryNames 方法的内部实现。

7.5 cache 探秘

到这里有的人又要问了:是不是结束了?其实还远没有!

细心地朋友已经发现了玄机,隐藏在 loadFactoryNames 方法的开头和结尾:



喂喂,这个返回的 result 好像并不是直接 new 出来的哦

它是从 cache 缓存中取出来的,你发现了没有

根据下面的 if 判断,如果从缓存中读取出来了 result,并且 result 的结果不为空,就直接返回,不需要再进行下面的读写操作了,这样减少了磁盘频繁的读写 I/O

同理,在我更新完所有的配置文件资源之后,退出时也要更新缓存。

7.6 getAutoConfigurationEntry 再探

关键部分已经过去,让我们反过头来重新审视一下遗漏的内容:

还记得 getAutoConfigurationEntry 方法吗?



我们最后来研究一下这个类除了 getCandidateConfigurations 还干了哪些事情:

  • removeDuplicates

  • configurations.removeAll(exclusions)

可以看到,这里对加载进来的配置进行了去重、排除的操作,这是为了使得用户自定义的排除包生效,同时避免包冲突异常,在 SpringBoot 的入口函数中我们可以通过注解指定需要排除哪些不用的包:

例如我不使用 RabbitMQ 的配置包,就把它的配置类的 class 传给 exclude

@SpringBootApplication(exclude = {RabbitAutoConfiguration.class})

8. 自动装配本质

我的理解:

  • SpringBoot 自动装配的本质就是通过 Spring 去读取 META-INF/spring.factories 中保存的配置类文件然后加载 bean 定义的过程。

  • 如果是标了 @Configuration 注解,就是批量加载了里面的 bean 定义

  • 如何实现”自动“:通过配置文件获取对应的批量配置类,然后通过配置类批量加载 bean 定义,只要有写好的配置文件 spring.factories 就实现了自动。

用户头像

@零度

关注

关注尚硅谷,轻松学IT 2021.11.23 加入

IT培训 www.atguigu.com

评论

发布
暂无评论
java培训SpringBoot自动装配原理_JAVA开发_@零度_InfoQ写作平台