面试被问SpringBoot自动装配原理,怎么破?
共 20336字,需浏览 41分钟
·
2022-05-21 12:44
往期热门文章:
来源:blog.csdn.net/weixin_43826242?type=blog
前言
学习SpringBoot,绝对避不开自动装配这个概念,这也是SpringBoot的关键之一
本人也是SpringBoot的初学者,下面的一些总结都是结合个人理解和实践得出的,如果有错误或者疏漏,请一定一定一定(不是欢迎,是一定)帮我指出,在评论区回复即可,一起学习!
篇幅较长,希望你可以有耐心.
如果只关心SpringBoot装配过程,可以直接跳到第7部分
想要理解spring自动装配,需要明确两个含义:
装配,装配什么?
自动,怎么自动?
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文件,在里面加入:
"person" class="pojo.Person">
"name" value="dzzhyk"/>
"age" value="20"/>
"sex" value="true"/>
这里使用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
"person" class="pojo.Person">
"0" type="java.lang.String" value="dzzhyk" />
"1" type="java.lang.Integer" value="20"/>
"2" type="java.lang.Boolean" value="true"/>
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中开启注解支持和自动包扫描:
"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的自动装配原理是非常重要的。
首先我们在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注入
"person" class="pojo.Person">
"name" value="dzzhyk"/>
"age" value="20"/>
"sex" value="true"/>
"dog" ref="dog"/>
"car" ref="car"/>
"dog" class="pojo.Dog">
"name" value="旺财"/>
"age" value="5" />
"car" class="pojo.Car">
"brand" value="奥迪双钻"/>
"price" value="100000"/>
然后跟普通的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
这个容器存储了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的改动。
大致流程我画了个图:
![](https://filescdn.proginn.com/2f7d1a5dbdd0fa59ce4aaeaff55b2ead/5bbbfea85ec0713113d6f6d48abc6eeb.webp)
至此我们能总结出springIOC容器的本质:(我的理解)
由BeanDefinitionMap、BeanFactoryPostProcessor、BeanPostProcessor、BeanMap等等容器共同组成、共同完成、提供依赖注入和控制反转功能的一组集合,叫IOC容器。
4. BeanDefinition结构
既然讲到了BeanDefinition,我们来看一下BeanDefinition里面究竟定义了些什么
让我们点进AbstractBeanDefinition这个类,一探究竟:
![](https://filescdn.proginn.com/09ecf5e9f6654b5a41a5e7c2df8cc0d4/8c187f03197e4fa9bfda6a3026e6692d.webp)
哇!好多成员变量,整个人都要看晕了@_@
我们来重点关注以下三个成员:
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配置文件中开启包扫描
"pojo"/>
注意:这里不要使用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类对象
@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
而ValueHolder这个对象继承于BeanMetadataElement,就是构造器参数的一个包装类型
通过这个例子我们可以看到ConstructorArgumentValues就是用来管控构造器参数的,指定这个值会在进行bean注入的时候选择合适的构造器。
5. 装配对象
现在我们把目光放回到SpringBoot的自动装配上来,原来在真正进行bean实例化对象前,我们前面还有这些过程,尤其是存在使用后置处理器BeanFactoryPostProcessor来对bean定义进行各种自定义修改的操作。
经过上面我们漫长的研究过程,我们终于可以回答第一个问题了:
自动装配的对象:Bean定义 (BeanDefinition)
6. My自动装配
看到这里又自然会产生疑问:不会吧,上面可都是自动装配啊,我在配置文件或者使用注解都配置了变量的值,然后加个@Autowired注解就OK了,spring也是帮我自动去装配。
再高端一点话,我就把XML文件写成JavaConfig配置类,然后使用@Configuration注解,这样也能自动装配,这不是很nice了吗?
6.1 自动装配之再思考
我的理解,上面的自动装配,我们至少要写一个配置文件,无论是什么形式,我们都至少需要一个文件把它全部写下来,就算这个文件的内容是固定的,但是为了装配这个对象,我们不得不写。
我们甚至都可以做成模板了,比如我在学习spring框架整合时,把经常写的都搞成了模板:
![](https://filescdn.proginn.com/3b097f6a47396490c8ebdf2badcbc02f/301a00afc0a466eea9256076b216ac43.webp)
有了这些模板,我们只需要点点点,再进行修改,就能用了。
这样做确实很好,可是对于越来越成型的项目体系,我们每次都搞一些重复动作,是会厌烦的。而且面对这么多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定义
喂!你是不是搞我!绕了一大圈,怎么还是加载这个配置文件啊!这个配置文件明明就是我自己写的。
![](https://filescdn.proginn.com/28b982cb5a2f5aec59e83d73fcbb45ad/052e1c2a4bf3d14eef3135593d31290e.webp)
总结一下,我们这个例子大概绕了这些过程:
![](https://filescdn.proginn.com/93c7c7149722eeb44007f37687f8589b/a1efa1441b496537d90e3498fceae28e.webp)
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这个配置文件就行了!
至此,无论是添加或者删除组件,无非是在配置文件中加上或者删除一行的问题了。
让我们在更新之后运行程序,可以看到成功拿到了配置文件的全类名
![](https://filescdn.proginn.com/09de757356cdbd0a683d0d976790faf7/5389e58c66c5bbe486fac412193e59e8.webp)
程序的运行当然也是没问题的:
A(name=我是AAA, b=B(name=我是BBB))
到此,我仿佛又领悟了一些东西。。。
我的配置文件好像活了,在我需要的时候他会出现,在我不需要的时候只需要在配置文件里面给他”打个叉“,他自己就跑开了
7. 自动装配源码分析
终于来到了大家喜闻乐见的部分:源码分析
在我们前面6节学习了各种”招式“之后,让我们请出对手:SpringBoot
现在在你面前的是一个SpringBoot”空项目“,没有添加任何依赖包和starter包
![](https://filescdn.proginn.com/13d9c83eb46cb5069d99a696f487dfad/70455492eb6d766f149d5998d9e5315e.webp)
启动项目:
![](https://filescdn.proginn.com/228b4d88a5bd2448701b3f265ec93a26/9eb6a01ff5cb4d13d5e6786f4aa9fc92.webp)
正常启动,让我们从@SpringBootApplication开始研究
7.1 @SpringBootConfiguration
会看到@SpringBootApplication这个注解由好多注解组成
主要的有以下三个:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
先来看第一个:@SpringBootConfiguration
进入这个注解之后会发现
![](https://filescdn.proginn.com/54ce30d111926dc421796fa92bdcb504/5da67aaac219079e1680e23343845239.webp)
原来你就是一个@Configuration啊,一个JavaConfig配置类
那我们使用JavaConfig不就是用来配置bean吗,所以有了这个注解之后我们可以在SpringBoot运行的主类中使用@Bean标签配置类了,如下图所示:
![](https://filescdn.proginn.com/8fc35bf03acb2b55c292d46f452ad638/4cbd71aec2c678c0a7e7b62ec2fa1d39.webp)
7.2 @ComponentScan
这个注解相信大家都认识了,组件扫描
这个扫描的范围是:SpringBoot主启动类的同级路径及子路径
![](https://filescdn.proginn.com/415c50c25bcc82ee37533acd70e43b73/20791295ab76e1c42d3bff4efc5bf51c.webp)
7.3 @EnableAutoConfiguration
来看这个注解,也是最核心的内容
这个注解怎么这么眼熟啊,还记得刚才的@MyEnableAutoConfig注解吗?就是我们自己写的那个注解
进入@EnableAutoConfiguration:
![](https://filescdn.proginn.com/75d870a79d9a4d3203827233af5888c0/b755a1a4cd95960c6352c9cc613f120c.webp)
看图中红圈位置的注解:@Import(AutoConfigurationImportSelector.class)
是不是跟我们上面自己写的内容一样!
这里的作用便是导入了 AutoConfigurationImportSelector 这个类的bean定义
我们都知道,如果这个类实现了ImportSelector接口,那他肯定重写了一个方法,就是我们上面重写过的selectImports方法:
果然,在这个类里面确实有这个selectImports方法:
![](https://filescdn.proginn.com/66710e6d38cdca2d66561e18f0219550/1ecfdab92877758b4a55cbb4ce42728b.webp)
我的天,好长的一串代码,一行都放不下!
此时此刻,我又回想起了在家乡的母亲,夏天的蝉鸣,池塘的荷花…
等等等等,这个类我们当时返回的是什么?是一个字符串数组String[ ],那这个类无论多么长,返回的肯定就是一个字符串数组,不信你自己看:
![](https://filescdn.proginn.com/ef350cb7f61552620f7898b9e40fffa8/636b39e8b843ff98be1c65fdd4199e3b.webp)
这个字符串数组存放的内容我们是否清楚呢?当然清楚了!我们返回的是要加载的Config配置文件的全包名,通过返回这个全包名,我们就能自动装配上这些配置文件下定义的bean对象,从而达到了自动装配的目的!
根据刚才我们自己实现的selectImports方法,我们是通过注解类的名字来查找,并且最终得到需要加载的Config类的全类名,最后返回的。
因此,这里必然有一个根据注解类名字来查找相应的Config文件的操作
我们继续反推,看到返回时的定义如下:
![](https://filescdn.proginn.com/9d1f7df9640604a905038f3e9067e635/469f8afd3c226fc08b4f95e9ed99557f.webp)
我们发现autoConfigurationEntry中保存着我们需要的配置信息,它是通过getAutoConfigurationEntry方法获取的,于是我们继续深入,进入getAutoConfigurationEntry方法
![](https://filescdn.proginn.com/aa35e20d8b6a26a8408c22dba69ddcd1/13ba8c63d41349e3cf86b8e9a00ee92a.webp)
这一段代码真是把人难住了,好大一片,不知道在做什么
此时此刻,我又回想起了在家乡的母亲,夏天的蝉鸣,池塘的荷花…
回家!有了!我们先想这个方法应该返回什么,根据我们前面的经验,这里应该返回一个类似于Entry的保存了我们需要的配置信息的对象
这个方法返回的是新建的AutoConfigurationEntry对象,根据最后一行的构造函数来看,给他了两个参数:
configurations, exclusions
configurations显然使我们需要的配置文件,也是我们最关心的,而exclusions字面意思是排除,也就是不需要的,那我们接下来应该关注configurations到底是怎么来的
根据我们前面的经验,我们是根据注解类名来从一个配置文件中读取出我们需要的Config配置类,这里configurations就代表了Config配置类,那么我们应该找到一个入口,这个入口跟注解相关,并且返回了configurations这个参数。
正如我们所料,这个方法的参数确实传递过来了一个东西,跟注解有关:
看见那个大大的Annotation(注解)了吗!
![](https://filescdn.proginn.com/27f9558e8b42cf0d41057feeae6a9233/e7a4751736e5207d3f6d24c655704a8f.webp)
那么根据这条”线索“,我们按图索骥,找到了三行代码,范围进一步缩小了!
此时再加上返回了configurations,我们最终确定了一行代码:
![](https://filescdn.proginn.com/73eefd5a4c255d1e286d7f02c555f045/6e4da4ced0f881311aa08b7bc53a1da7.webp)
就是这个getCandidateConfigurations方法,符合我们的要求!
从字面意思上分析,获取候选的配置,确实是我们需要的方法
OK,让我们继续前进,进入这个方法:
![](https://filescdn.proginn.com/b301e7db346e2eaa3834a9dfa30102f7/c5cfbf0a22eeb2005f01f31c55b48429.webp)
这个方法是不是也似曾相识呢?我们之前写过一个专门用于读取配置文件的类MyPropertyReader,还记得吗?
如果你还记得的话,我们自己写的工具类里面也是一个静态方法readPropertyForMe来帮我读取配置文件
但是我们的配置文件路径一定是需要指定的,不能乱放。
从这个loadFactoryNames方法体来看,好像没有给他传递一个具体路径
但是从下面的Assert断言中,我们发现了玄机:
![](https://filescdn.proginn.com/7746411a658a5bacb45199627e164cf5/8d5fcd7e6c52a2c2e1ded1d2468c4a4c.webp)
在META-INF/spring.factories文件中没有找到自动配置类Config,你要检查balabala。。。。
根据我不太灵光的脑袋的判断,他的这个配置文件就叫spring.factories,存放的路径是META-INF/spring.factories
于是我们打开spring boot自动装配的依赖jar包:
![](https://filescdn.proginn.com/ce217f75e22348475cd83398b838962a/5fcddf8c6ca8b3bf9c9ce286507ae37f.webp)
那这个配置文件里面的内容,是不是跟我们想的一样呢?
![](https://filescdn.proginn.com/7a8e0428b908f32afa53ec003c5134db/88a47c832745d0384d9ad41d69cb5c1f.webp)
原来如此。
这里的EnableAutoConfiguration注解,正是我们此行的起点啊…
到这里,自动装配到底是什么,应该比较清楚了,原来他是帮我们加载了各种已经写好的Config类文件,实现了这些JavaConfig配置文件的重复利用和组件化
7.4 loadFactoryNames方法
行程不能到此结束,学习不能浅尝辄止。
我们还有最后一块(几块)面纱没有解开,现在还不能善罢甘休。
让我们进入loadFactoryNames方法:
![](https://filescdn.proginn.com/e3a5b872af9f9c1dd7403956442cbf18/9e2cac750021bc916f468a911bb524be.webp)
这个方法非常简短,因为他调用了真正实现的方法:loadSpringFactories
这一行return代码我复制在下面:
loadSpringFactories(classLoader)
.getOrDefault(factoryTypeName, Collections.emptyList());
可以分析得出:loadSpringFactories方法的返回值又调用了一个getOrDefault方法,这明显是一个容器类的方法,目的是从容器中拿点东西出来
就此推测:loadSpringFactories返回了一个包含我们需要的Config全类名(字符串)的集合容器,然后从这个集合容器中拿出来的东西就是我们的configurations
让我们看这个loadSpringFactories方法:
![](https://filescdn.proginn.com/53e19af653356ae269a973d0e89e7019/e4bb1b0be4940c529a7c615a3c0d65ba.webp)
它确实返回了一个容器:Map
这个数据结构就非常牛逼了,多值集合映射(我自己的翻译)简单来说,一个key可以对应多个value,根据他的返回值,我们可以看到在这个方法中一个String对应了一个List
那么不难想到MultiValueMap中存放的形式:是”注解的类名——多个Config配置类“ 让我们打个断点来验证一下:
![](https://filescdn.proginn.com/fa6efb416ebeccadf687e778c84dea61/251f8b50d18d4eeeb0fe2f49622877f5.webp)
果然是这样,并且@EnableAutoConfiguration注解竟然加载了多达124个配置类!
接下来我们继续思考:我们来的目的是获取configurations,所以无论你做什么,必须得读取配置文件,拿到configurations
于是我们在try方法体中果然发现了这个操作:
![](https://filescdn.proginn.com/7fc82535bc2e0016a2de1c16d9919b01/c376ba0bc15a96d111d78f857ffe094a.webp)
他获取了一个路径urls,那么这个路径是否就是我们前面验证的META-INF/spring.factories呢?
我们查看静态常量FACTORIES_RESOURCE_LOCATION的值:
![](https://filescdn.proginn.com/d19f94be97389cb1853d7442ba60b204/2f73c1f4dc832e186c9bc4032d2b8893.webp)
果真如此,bingo!继续往下看,果然他遍历了urls中的内容,从这个路径加载了配置文件:终于看到了我们熟悉的loadProperties方法!
![](https://filescdn.proginn.com/36f0587bffee054f6de15309a713996e/0b4bf477e4a463d900afba693f9cb168.webp)
那我们大概就知道了,他确实是通过找到路径,然后根据路径读取了配置文件,然后返回了读取的result
这就是loadFactoryNames方法的内部实现。
7.5 cache探秘
到这里有的人又要问了:是不是结束了?其实还远没有!
细心地朋友已经发现了玄机,隐藏在loadFactoryNames方法的开头和结尾:
![](https://filescdn.proginn.com/c22e0aa2e30c0ccde057a826017d4ffa/e9a9e31196245215a220d1dd7dffbfaa.webp)
喂喂,这个返回的result好像并不是直接new出来的哦
它是从cache缓存中取出来的,你发现了没有
根据下面的if判断,如果从缓存中读取出来了result,并且result的结果不为空,就直接返回,不需要再进行下面的读写操作了,这样减少了磁盘频繁的读写I/O
同理,在我更新完所有的配置文件资源之后,退出时也要更新缓存。
7.6 getAutoConfigurationEntry再探
关键部分已经过去,让我们反过头来重新审视一下遗漏的内容:
还记得getAutoConfigurationEntry方法吗?
![](https://filescdn.proginn.com/45f10a58b3bdd03f61c091aecc2f4618/61f2d7b87c9f8151afddfe4f84a1a842.webp)
我们最后来研究一下这个类除了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就实现了自动。
9. 总结
Spring Boot的自动装配特性可以说是Spring Boot最重要、最核心的一环,正是因为这个特性,使得我们的生产复杂性大大降低,极大地简化了开发流程,可以说是给我们带来了巨大的福音了~~
笔者本人对源码的理解仍然没有那么深刻,只是喜欢分享自己的一些学习经验,希望能和大家共同学习,毕竟掌握一门新技术的快感嘛… 大家都懂的!
写这篇文章耗费了巨大的精力,每一个字均是手码,真的希望喜欢的朋友可以点赞收藏关注支持一波,这就是对我这个未出世的学生的最大激励了!
最后,我画了一份Spring Boot自动装配详细流程图,分享给大家。
Spring Boot自动装配详细流程图:
![](https://filescdn.proginn.com/9804cab7942696256d2940b756eefa41/2634087d003bd4ee7cb22c7b974c9a00.webp)
版权申明:内容来源网络,版权归原创者所有。除非无法确认,都会标明作者及出处,如有侵权,烦请告知,我们会立即删除并致歉!
最近热文阅读:
1、五个刁钻的String面试问题及解答 2、IntelliJ平台将完全停止使用Log4j 3、神操作!我把 3000 行代码重构成 15 行! 4、我用Java几分钟处理完30亿个数据... 5、一款自动生成单元测试的 IDEA 插件 6、微软 10 大最受欢迎 GitHub 项目,最高 Star 数量 13 万 7、Spring Boot 中实现跨域的 5 种方式,你一定要知道! 8、Mybatis批处理踩坑,纠正一些错误写法 9、Java 8?Java之父都不用! 10、生产环境,清理大文件不生效?应该这样做! 关注公众号,你想要的Java都在这里