學(xué)習(xí)SpringBoot,絕對(duì)避不開(kāi)自動(dòng)裝配這個(gè)概念,這也是SpringBoot的關(guān)鍵之一
本人也是SpringBoot的初學(xué)者,下面的一些總結(jié)都是結(jié)合個(gè)人理解和實(shí)踐得出的,如果有錯(cuò)誤或者疏漏,請(qǐng)一定一定一定(不是歡迎,是一定)幫我指出,在評(píng)論區(qū)回復(fù)即可,一起學(xué)習(xí)!
篇幅較長(zhǎng),希望你可以有耐心.
如果只關(guān)心SpringBoot裝配過(guò)程,可以直接跳到第7部分
想要理解spring自動(dòng)裝配,需要明確兩個(gè)含義:
裝配,裝配什么?
自動(dòng),怎么自動(dòng)?
在開(kāi)始之前,讓我們先來(lái)看點(diǎn)簡(jiǎn)單的開(kāi)胃菜:spring中bean注入的三種形式
首先我們先來(lái)一個(gè)Person類(lèi),這里為了篇幅長(zhǎng)度考慮使用了lombok
如果你不知道lombok是什么,那就最好不要知道,加了幾個(gè)注解之后我的pojo類(lèi)Person就完成了
/**
* @author dzzhyk
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String name;
private Integer age;
private Boolean sex;
}
在Spring中(不是Spring Boot),要實(shí)現(xiàn)bean的注入,我們有3種注入方式:
這是最基本的注入方式
首先我們創(chuàng)建applicationContext.xml文件,在里面加入:
<!-- 手動(dòng)配置bean對(duì)象 -->
<bean id="person" class="pojo.Person">
<property name="name" value="dzzhyk"/>
<property name="age" value="20"/>
<property name="sex" value="true"/>
</bean>
這里使用property為bean對(duì)象賦值
緊接著我們會(huì)在test包下寫(xiě)一個(gè)version1.TestVersion1類(lèi)
/**
* 第一種bean注入實(shí)現(xiàn)方式 - 在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來(lái)加載spring配置文件并且讀取其中定義的bean,然后使用getBean方法使用id和類(lèi)來(lái)獲取這個(gè)Person的Bean對(duì)象,結(jié)果成功輸出:
Person(name=dzzhyk, age=20, sex=true)
接下來(lái)是使用構(gòu)造器注入,我們需要更改applicationContext.xml文件中的property為construct-arg
<!-- 使用構(gòu)造器 -->
<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內(nèi)容不變:
public class TestVersion2 {
@Test
public void test(){
ApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");
Person person = ca.getBean("person", Person.class);
System.out.println(person);
}
}
依然正常輸出結(jié)果:
Person(name=dzzhyk, age=20, sex=true)
使用注解方式的屬性注入Bean是比較優(yōu)雅的做法
首先我們需要在applicationContext.xml中開(kāi)啟注解支持和自動(dòng)包掃描:
<context:annotation-config />
<context:component-scan base-package="pojo"/>
在pojo類(lèi)中對(duì)Person類(lèi)加上@Component注解,將其標(biāo)記為組件,并且使用@Value注解為各屬性賦初值
@Component
public class Person {
@Value("dzzhyk")
private String name;
@Value("20")
private Integer age;
@Value("true")
private Boolean sex;
}
然后添加新的測(cè)試類(lèi)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);
}
}
運(yùn)行也可以得到如下結(jié)果:
Person(name=dzzhyk, age=20, sex=true)
什么?還有什么?接下來(lái)我們來(lái)聊聊Spring的兩種配置方式:基于XML的配置和基于JavaConfig類(lèi)的配置方式,這對(duì)于理解SpringBoot的自動(dòng)裝配原理是非常重要的。
首先我們?cè)赑erson的基礎(chǔ)上再創(chuàng)建幾個(gè)pojo類(lèi):這個(gè)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;
}
接下來(lái)讓我們嘗試使用XML的配置方式來(lái)為一個(gè)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="旺財(cái)"/>
<property name="age" value="5" />
</bean>
<bean id="car" class="pojo.Car">
<property name="brand" value="奧迪雙鉆"/>
<property name="price" value="100000"/>
</bean>
然后跟普通的Bean注入一樣,使用ClassPathXmlApplicationContext來(lái)加載配置文件,然后獲取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);
}
}
輸出結(jié)果如下:
Person(name=dzzhyk, age=20, sex=true, dog=Dog(name=旺財(cái), age=5), car=Car(brand=奧迪雙鉆, price=100000))
想要成為JavaConfig類(lèi),需要使用@Configuration注解
我們新建一個(gè)包命名為config,在config中新增一個(gè)PersonConfig類(lèi)
@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("旺財(cái)", 5);
}
@Bean
public Car car(){
return new Car("奧迪雙鉆", 100000);
}
}
此時(shí)我們的XML配置文件可以完全為空了,此時(shí)應(yīng)該使用AnnotationConfigApplicationContext來(lái)獲取注解配置
/**
* 使用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);
}
}
仍然正常輸出了結(jié)果:
Person(name=dzzhyk, age=20, sex=true, dog=Dog(name=旺財(cái), age=5), car=Car(brand=奧迪雙鉆, price=100000))
AbstractBeanDefinition
是spring中所有bean的抽象定義對(duì)象,我把他叫做bean定義
當(dāng)bean.class被JVM類(lèi)加載到內(nèi)存中時(shí),會(huì)被spring掃描到一個(gè)map容器中:
BeanDefinitionMap<beanName, BeanDefinition>
這個(gè)容器存儲(chǔ)了bean定義,但是bean此時(shí)還沒(méi)有進(jìn)行實(shí)例化,在進(jìn)行實(shí)例化之前,還有一個(gè)
BeanFactoryPostProcessor
可以對(duì)bean對(duì)象進(jìn)行一些自定義處理
我們打開(kāi)BeanFactoryProcessor這個(gè)接口的源碼可以發(fā)現(xiàn)如下內(nèi)容:
/*
* 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完成標(biāo)準(zhǔn)的初始化過(guò)程后,實(shí)現(xiàn)BeanFactoryPostProcessor接口的對(duì)象可以用于定制bean factory,所有的bean definition都會(huì)被加載,但是此時(shí)還沒(méi)有被實(shí)例化。這個(gè)接口允許對(duì)一些bean定義做出屬性上的改動(dòng)。
簡(jiǎn)言之就是實(shí)現(xiàn)了BeanFactoryPostProcessor這個(gè)接口的類(lèi),可以在bean實(shí)例化之前完成一些對(duì)bean的改動(dòng)。
大致流程我畫(huà)了個(gè)圖:
至此我們能總結(jié)出springIOC容器的本質(zhì):(我的理解)
由BeanDefinitionMap、BeanFactoryPostProcessor、BeanPostProcessor、BeanMap等等容器共同組成、共同完成、提供依賴(lài)注入和控制反轉(zhuǎn)功能的一組集合,叫IOC容器。
既然講到了BeanDefinition,我們來(lái)看一下BeanDefinition里面究竟定義了些什么
讓我們點(diǎn)進(jìn)AbstractBeanDefinition這個(gè)類(lèi),一探究竟:
哇!好多成員變量,整個(gè)人都要看暈了@_@
我們來(lái)重點(diǎn)關(guān)注以下三個(gè)成員:
private volatile Object beanClass;
private int autowireMode = AUTOWIRE_NO;
private ConstructorArgumentValues constructorArgumentValues;
這個(gè)屬性決定了該Bean定義的真正class到底是誰(shuí),接下來(lái)我們來(lái)做點(diǎn)實(shí)驗(yàn)
我們定義兩個(gè)Bean類(lèi),A和B
@Component
public class A {
@Value("我是AAA")
private String name;
}
@Component
public class B {
@Value("我是BBB")
private String name;
}
接下來(lái)我們實(shí)現(xiàn)上面的BeanFactoryPostProcessor接口,來(lái)創(chuàng)建一個(gè)自定義的bean后置處理器
/**
* 自定義的bean后置處理器
* 通過(guò)這個(gè)MyBeanPostProcessor來(lái)修改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配置文件中開(kāi)啟包掃描
<context:component-scan base-package="pojo"/>
<context:annotation-config />
注意:這里不要使用JavaConfig類(lèi)來(lái)配置bean,不然會(huì)報(bào)如下錯(cuò)誤
ConfigurationClassBeanDefinition cannot be cast to org.springframework.beans.factory.support.GenericBeanDefinition
這個(gè)錯(cuò)誤出自這一句:
GenericBeanDefinition defA = (GenericBeanDefinition) beanFactory.getBeanDefinition("a");
最后,我們創(chuàng)建一個(gè)測(cè)試類(lèi):
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);
}
}
測(cè)試運(yùn)行!
這里是MyBeanPostProcessor,我拿到了:pojo.A
最終拿到了==> A(name=我是AAA, b=B(name=我是BBB))
可以看到MyBeanPostProcessor成功拿到了A的Bean定義,并且輸出了提示信息
接下來(lái)讓我們做點(diǎn)壞事
我們?cè)贛yBeanPostProcessor中修改A的Bean對(duì)象,將A的beanClass修改為B.class
System.out.println("這里是MyBeanPostProcessor,我修改了:"+ defA.getBeanClassName() + " 的class為 B.class");
// 把A的class改成B
defA.setBeanClass(B.class);
重新運(yùn)行Test類(lèi),輸出了一些信息后:報(bào)錯(cuò)了!
這里是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'
我要拿到一個(gè)A類(lèi)對(duì)象,你怎么給我一個(gè)B類(lèi)對(duì)象呢?這明顯不對(duì)
綜上所述,我們可以得出beanClass屬性控制bean定義的類(lèi)
我們繼續(xù)看第二個(gè)屬性:autowireMode,自動(dòng)裝配模式
我們?cè)贏bstractBeanDefinition源碼中可以看到:
private int autowireMode = AUTOWIRE_NO;
自動(dòng)裝配模式默認(rèn)是AUTOWIRE_NO,就是不開(kāi)啟自動(dòng)裝配
可選的常量值有以下四種:不自動(dòng)裝配,通過(guò)名稱(chēng)裝配,通過(guò)類(lèi)型裝配,通過(guò)構(gòu)造器裝配
AUTOWIRE_NO
AUTOWIRE_BY_NAME
AUTOWIRE_BY_TYPE
AUTOWIRE_CONSTRUCTOR
接下來(lái)我們來(lái)模擬一個(gè)自動(dòng)裝配場(chǎng)景,仍然是A和B兩個(gè)類(lèi),現(xiàn)在在A類(lèi)中添加B類(lèi)對(duì)象
@Component
public class A {
@Value("我是AAA")
private String name;
@Autowired
private B b;
}
我們希望b對(duì)象能夠自動(dòng)裝配,于是我們給他加上了@Autowired注解,其他的完全不變,我們自定義的MyBeanPostProcessor中也不做任何操作,讓我們運(yùn)行測(cè)試類(lèi):
這里是MyBeanPostProcessor,我拿到了:pojo.A
最終拿到了==> A(name=我是AAA, b=B(name=我是BBB))
自動(dòng)裝配成功了!我們拿到的A類(lèi)對(duì)象里面成功注入了B類(lèi)對(duì)象b
現(xiàn)在問(wèn)題來(lái)了,如果我把@Autowired注解去掉,自動(dòng)裝配會(huì)成功嗎?
這里是MyBeanPostProcessor,我拿到了:pojo.A
最終拿到了==> A(name=我是AAA, b=null)
必然是不成功的
但是我就是想要不加@Autowired注解,仍然可以實(shí)現(xiàn)自動(dòng)裝配,需要怎么做?
這時(shí)就要在我們的MyBeanPostProcessor中做文章了,加入如下內(nèi)容:
defA.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
再輸出結(jié)果:
這里是MyBeanPostProcessor,我拿到了:pojo.A
最終拿到了==> A(name=我是AAA, b=B(name=我是BBB))
自動(dòng)裝配成功了!這次我們可沒(méi)加@Autowired,在我們的自定義的bean后置處理器中設(shè)置了autowireMode屬性,也實(shí)現(xiàn)了自動(dòng)裝配
綜上,autowireMode屬性是用來(lái)控制自動(dòng)裝配模式的,默認(rèn)值是AUTOWIRE_NO,即不自動(dòng)裝配
constructorArgumentValues的字面含義是構(gòu)造器參數(shù)值
改變這個(gè)參數(shù)值,我們可以做到在實(shí)例化對(duì)象時(shí)指定特定的構(gòu)造器
話(huà)不多說(shuō),show me your code:
因?yàn)橐芯繕?gòu)造器,只能先”忍痛“關(guān)掉lombok插件,手寫(xiě)一個(gè)pojo.Student類(lèi)
/**
* Student類(lèi)
* @author dzzhyk
*/
@Component
public class Student {
private String name;
private Integer age;
public Student() {
System.out.println("==>使用空參構(gòu)造器 Student()");
}
public Student(String name, Integer age) {
this.name = name;
this.age = age;
System.out.println("==>使用雙參數(shù)構(gòu)造器 Student(String name, Integer age)");
}
}
我們都知道,spring在實(shí)例化對(duì)象時(shí)使用的是對(duì)象的默認(rèn)空參構(gòu)造器:
我們新建一個(gè)測(cè)試方法test
@Test
public void test(){
ApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");
Student student = ca.getBean("stu", Student.class);
System.out.println("==>" + student);
}
運(yùn)行可以得到下面結(jié)果:
這里是MyBeanPostProcessor,我拿到了:pojo.Student
==>使用空參構(gòu)造器 Student()
==>pojo.Student@402e37bc
可以看到,確實(shí)使用了空參構(gòu)造器
但是如何指定(自定義)使用哪個(gè)構(gòu)造器呢?我根本看不見(jiàn)摸不著,Spring全幫我做了,實(shí)在是太貼心了。
接下來(lái)就聊聊constructorArgumentValues的使用:
我們?cè)贛yBeanPostProcessor中加入如下內(nèi)容,對(duì)獲取到的pojo.Student的bean定義進(jìn)行操作:
ConstructorArgumentValues args = new ConstructorArgumentValues();
args.addIndexedArgumentValue(0, "我指定的姓名");
args.addIndexedArgumentValue(1, 20);
defStu.setConstructorArgumentValues(args);
再次運(yùn)行test:
這里是MyBeanPostProcessor,我拿到了:pojo.Student
==>使用雙參數(shù)構(gòu)造器 Student(String name, Integer age)
==>pojo.Student@2f177a4b
可以看到這次使用了雙參數(shù)構(gòu)造器
有人會(huì)好奇ConstructorArgumentValues到底是個(gè)什么東西,我點(diǎn)進(jìn)源碼研究一番,結(jié)果發(fā)現(xiàn)這個(gè)類(lèi)就是一個(gè)普通的包裝類(lèi),包裝的對(duì)象是ValueHolder,里面一個(gè)List
而ValueHolder這個(gè)對(duì)象繼承于BeanMetadataElement,就是構(gòu)造器參數(shù)的一個(gè)包裝類(lèi)型
通過(guò)這個(gè)例子我們可以看到ConstructorArgumentValues就是用來(lái)管控構(gòu)造器參數(shù)的,指定這個(gè)值會(huì)在進(jìn)行bean注入的時(shí)候選擇合適的構(gòu)造器。
現(xiàn)在我們把目光放回到SpringBoot的自動(dòng)裝配上來(lái),原來(lái)在真正進(jìn)行bean實(shí)例化對(duì)象前,我們前面還有這些過(guò)程,尤其是存在使用后置處理器BeanFactoryPostProcessor來(lái)對(duì)bean定義進(jìn)行各種自定義修改的操作。
經(jīng)過(guò)上面我們漫長(zhǎng)的研究過(guò)程,我們終于可以回答第一個(gè)問(wèn)題了:
自動(dòng)裝配的對(duì)象:Bean定義 (BeanDefinition)
看到這里又自然會(huì)產(chǎn)生疑問(wèn):不會(huì)吧,上面可都是自動(dòng)裝配啊,我在配置文件或者使用注解都配置了變量的值,然后加個(gè)@Autowired注解就OK了,spring也是幫我自動(dòng)去裝配。
再高端一點(diǎn)話(huà),我就把XML文件寫(xiě)成JavaConfig配置類(lèi),然后使用@Configuration注解,這樣也能自動(dòng)裝配,這不是很nice了嗎?
我的理解,上面的自動(dòng)裝配,我們至少要寫(xiě)一個(gè)配置文件,無(wú)論是什么形式,我們都至少需要一個(gè)文件把它全部寫(xiě)下來(lái),就算這個(gè)文件的內(nèi)容是固定的,但是為了裝配這個(gè)對(duì)象,我們不得不寫(xiě)。
我們甚至都可以做成模板了,比如我在學(xué)習(xí)spring框架整合時(shí),把經(jīng)常寫(xiě)的都搞成了模板:
有了這些模板,我們只需要點(diǎn)點(diǎn)點(diǎn),再進(jìn)行修改,就能用了。
這樣做確實(shí)很好,可是對(duì)于越來(lái)越成型的項(xiàng)目體系,我們每次都搞一些重復(fù)動(dòng)作,是會(huì)厭煩的。而且面對(duì)這么多xml配置文件,我太難了。
于是我有了一個(gè)想說(shuō)但不敢說(shuō)的問(wèn)題:
我一個(gè)配置文件都不想寫(xiě),程序還能照樣跑,我只關(guān)心有我需要的組件就可以了,我只需要關(guān)注我的目標(biāo)就可以了,我想打開(kāi)一個(gè)工程之后可以1秒進(jìn)入開(kāi)發(fā)狀態(tài),而不是花3小時(shí)寫(xiě)完配置文件(2.5小時(shí)找bug)希望有個(gè)東西幫我把開(kāi)始之前的準(zhǔn)備工作全做了,即那些套路化的配置,這樣在我接完水之后回來(lái)就可以直接進(jìn)行開(kāi)發(fā)。
說(shuō)到這里,想必大家都懂了:SpringBoot
讓我們?cè)谕祽械牡缆飞侠^續(xù)前進(jìn)。
來(lái)看下面這個(gè)例子:
仍然是A類(lèi)和B類(lèi),其中A類(lèi)仍然引用了B類(lèi),我們給A類(lèi)組件起id=“a”,B類(lèi)組件起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注解來(lái)自動(dòng)注入b,測(cè)試類(lèi)如下:
@Test
public void test(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyAutoConfig.class);
A aaa = ac.getBean("a", A.class);
System.out.println(aaa);
}
細(xì)心的同學(xué)已經(jīng)發(fā)現(xiàn)了:我們這里使用了AnnotationConfigApplicationContext這個(gè)JavaConfig配置類(lèi)會(huì)使用到的加載類(lèi),于是我們順利成章地點(diǎn)開(kāi)它所加載的MyAutoConfig類(lèi)文件
文件內(nèi)容如下:
@Configuration
@MyEnableAutoConfig
public class MyAutoConfig {
// bean 都去哪了 ???
}
what? 我要聲明的Bean對(duì)象都去哪了(注意:這里的applicationContext.xml是空的)?
讓我們運(yùn)行test:
A(name=我是AAA, b=B(name=我是BBB))
竟然運(yùn)行成功了,這究竟是為什么?(元芳,你怎么看?)
細(xì)心的同學(xué)已經(jīng)發(fā)現(xiàn)了:@MyEnableAutoConfig是什么注解?我怎么沒(méi)有這個(gè)注解
讓我們點(diǎn)進(jìn)@MyEnableAutoConfig一探究竟:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyImportSelector.class) // 導(dǎo)入bean定義
public @interface MyEnableAutoConfig {
}
原來(lái)如此!你是用了@Import注解導(dǎo)入了Bean定義對(duì)吧,注釋都寫(xiě)著呢!
可是客官,@Import導(dǎo)入bean定義是沒(méi)錯(cuò),但是它導(dǎo)入的是MyImportSelector這個(gè)bean,不是A也不是B啊…
@Import的功能就是獲取某個(gè)類(lèi)的bean對(duì)象,他的使用形式大致如下:
@Import(A.class)
@Import(MyImportBeanDefinitionRegister.class)
@Import(MyImportSelector.class)
第一種形式@Import(A.class),是最簡(jiǎn)單易懂的形式
我們需要哪個(gè)Bean定義,直接Import他的class即可
第二種形式@Import(MyImportBeanDefinitionRegister.class)
傳遞了一個(gè)bean定義注冊(cè)器,這個(gè)注冊(cè)器的具體內(nèi)容如下:
public class MyImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
RootBeanDefinition aDef = new RootBeanDefinition(A.class);
registry.registerBeanDefinition("a", aDef);
}
}
這個(gè)注冊(cè)器實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口,并且重寫(xiě)了里面的registerBeanDefinitions方法
看他做了什么事:創(chuàng)建了一個(gè)新的bean定義,他的類(lèi)型就是A,然后把這個(gè)bean定義注冊(cè)到BeanDefinitionMap(還記得吧?。├锩?,key值我們可以人為設(shè)置,這里就設(shè)置成"a"
這樣在傳遞一個(gè)注冊(cè)器的時(shí)候,我們就可以把注冊(cè)器中新增的bean定義注冊(cè)進(jìn)來(lái)使用
可以看到,這種使用方式就是我們剛才的注解中使用的方式
他傳遞了一個(gè)叫MyImportSelector的類(lèi),這個(gè)類(lèi)依然是我們自己定義的,具體內(nèi)容如下:
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 導(dǎo)入配置類(lèi)
return new String[]{"config.MyConfig"};
}
}
這個(gè)類(lèi)實(shí)現(xiàn)了ImportSelector接口,并且重寫(xiě)了selectImports方法,返回一個(gè)字符串?dāng)?shù)組
我們可以看到,返回的字符串?dāng)?shù)組中是我們要導(dǎo)入類(lèi)的全類(lèi)名
這個(gè)Importer返回的類(lèi)如果是組件bean對(duì)象,就會(huì)被加載進(jìn)來(lái)使用;如果是一個(gè)配置類(lèi),就會(huì)加載這個(gè)配置類(lèi)
第三種和第二種的區(qū)別是第三種可以一次性寫(xiě)很多類(lèi),而且比較簡(jiǎn)潔,只需要清楚類(lèi)的全包名即可。而第二種方式需要自己清楚包類(lèi)名,手動(dòng)創(chuàng)建bean定義,然后手動(dòng)加入BeanDefinitionMap。
我們打開(kāi)MyImportSelector,發(fā)現(xiàn)里面赫然寫(xiě)著幾個(gè)大字:
return new String[]{"config.MyConfig"};
然后我們找到config.MyConfig類(lèi),發(fā)現(xiàn)這個(gè)類(lèi)竟然就是我們剛才寫(xiě)的JavaConfig版本的配置文件:
@Configuration
public class MyConfig {
@Bean
public A a(){
return new A();
}
@Bean
public B b(){
return new B();
}
}
加載這個(gè)MyConfig配置類(lèi),就相當(dāng)于加載了A和B兩個(gè)Bean定義
喂!你是不是搞我!繞了一大圈,怎么還是加載這個(gè)配置文件??!這個(gè)配置文件明明就是我自己寫(xiě)的。
總結(jié)一下,我們這個(gè)例子大概繞了這些過(guò)程:
"沒(méi)有會(huì)偷懶的人解決不掉的問(wèn)題“ —— 魯迅
上面的例子也沒(méi)有多大優(yōu)化啊,我怎么覺(jué)得更加麻煩了?不但繞了一大圈,定義了許多新東西,到最后還是加載了我寫(xiě)好的JavaConfig類(lèi),說(shuō)到底我不是還在寫(xiě)javaConfig類(lèi)嗎…
但是你注意到?jīng)]有:有了上面的機(jī)制,我只需要把JavaConfig類(lèi)寫(xiě)一次,然后放在某個(gè)地方,在MyImportSelector中加入這個(gè)地方的全包名路徑,下次用的時(shí)候直接導(dǎo)入最頂層的MyAutoConfig類(lèi),所有有關(guān)這個(gè)部件我需要的東西,就全部自動(dòng)整理好了,甚至比鼠標(biāo)點(diǎn)點(diǎn)點(diǎn)添加代碼模板還要快!
我突然有了個(gè)很棒的想法,不知道你有了沒(méi)有 。
如果你開(kāi)始有點(diǎn)感覺(jué)了,就會(huì)自然提出另一個(gè)問(wèn)題:我這樣做確實(shí)可以提高效率,但是一段代碼里寫(xiě)入我自己定制的內(nèi)容,每次更改起來(lái)不是太費(fèi)勁了嗎?
想到這里,我就不禁回想起使用JDBC的時(shí)候,在代碼里改SQL語(yǔ)句的痛苦了,那真是生不如死…這種情況就構(gòu)成了硬編碼的行為,是不好的。
我們自然會(huì)想到:要是我創(chuàng)建一個(gè)配置文件properties來(lái)專(zhuān)門(mén)保存我這個(gè)需求所使用的bean對(duì)象,然后使用的時(shí)候在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是我們自己新創(chuàng)建的用于讀取properties文件的工具類(lèi)
之所以要自己再定義這樣一個(gè)工具類(lèi),是為了以后在其中可以做一些其他操作(比如:去重、預(yù)檢查)
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;
}
}
我們的配置文件里面這么寫(xiě):
anno.MyEnableAutoConfig=config.MyConfig
可以看到,key是注解@MyEnableAutoConfig的類(lèi)名,也就是根據(jù)這個(gè)注解,就會(huì)導(dǎo)入后面的MyConfig這個(gè)Bean,這個(gè)Bean就是我們的配置文件
如此一來(lái)我們讀取這個(gè)配置文件,然后加載跟這個(gè)注解名稱(chēng)相符的value(即JavaConfig配置文件),就相當(dāng)于我們?cè)诖a里手寫(xiě)的"config.MyConfig",只不過(guò)現(xiàn)在的形式已經(jīng)發(fā)生了巨大的變化:我們添加或者刪除一個(gè)配件,完全只需要修改MyProperty.properties這個(gè)配置文件就行了!
至此,無(wú)論是添加或者刪除組件,無(wú)非是在配置文件中加上或者刪除一行的問(wèn)題了。
讓我們?cè)诟轮筮\(yùn)行程序,可以看到成功拿到了配置文件的全類(lèi)名
程序的運(yùn)行當(dāng)然也是沒(méi)問(wèn)題的:
A(name=我是AAA, b=B(name=我是BBB))
到此,我仿佛又領(lǐng)悟了一些東西。。。
我的配置文件好像活了,在我需要的時(shí)候他會(huì)出現(xiàn),在我不需要的時(shí)候只需要在配置文件里面給他”打個(gè)叉“,他自己就跑開(kāi)了
終于來(lái)到了大家喜聞樂(lè)見(jiàn)的部分:源碼分析
在我們前面6節(jié)學(xué)習(xí)了各種”招式“之后,讓我們請(qǐng)出對(duì)手:SpringBoot
現(xiàn)在在你面前的是一個(gè)SpringBoot”空項(xiàng)目“,沒(méi)有添加任何依賴(lài)包和starter包
啟動(dòng)項(xiàng)目:
正常啟動(dòng),讓我們從@SpringBootApplication開(kāi)始研究
會(huì)看到@SpringBootApplication這個(gè)注解由好多注解組成
主要的有以下三個(gè):
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
先來(lái)看第一個(gè):@SpringBootConfiguration
進(jìn)入這個(gè)注解之后會(huì)發(fā)現(xiàn)
原來(lái)你就是一個(gè)@Configuration啊,一個(gè)JavaConfig配置類(lèi)
那我們使用JavaConfig不就是用來(lái)配置bean嗎,所以有了這個(gè)注解之后我們可以在SpringBoot運(yùn)行的主類(lèi)中使用@Bean標(biāo)簽配置類(lèi)了,如下圖所示:
這個(gè)注解相信大家都認(rèn)識(shí)了,組件掃描
這個(gè)掃描的范圍是:SpringBoot主啟動(dòng)類(lèi)的同級(jí)路徑及子路徑
來(lái)看這個(gè)注解,也是最核心的內(nèi)容
這個(gè)注解怎么這么眼熟啊,還記得剛才的@MyEnableAutoConfig注解嗎?就是我們自己寫(xiě)的那個(gè)注解
進(jìn)入@EnableAutoConfiguration:
看圖中紅圈位置的注解:@Import(AutoConfigurationImportSelector.class)
是不是跟我們上面自己寫(xiě)的內(nèi)容一樣!
這里的作用便是導(dǎo)入了 AutoConfigurationImportSelector 這個(gè)類(lèi)的bean定義
我們都知道,如果這個(gè)類(lèi)實(shí)現(xiàn)了ImportSelector接口,那他肯定重寫(xiě)了一個(gè)方法,就是我們上面重寫(xiě)過(guò)的selectImports方法:
果然,在這個(gè)類(lèi)里面確實(shí)有這個(gè)selectImports方法:
我的天,好長(zhǎng)的一串代碼,一行都放不下!
此時(shí)此刻,我又回想起了在家鄉(xiāng)的母親,夏天的蟬鳴,池塘的荷花…
等等等等,這個(gè)類(lèi)我們當(dāng)時(shí)返回的是什么?是一個(gè)字符串?dāng)?shù)組String[ ],那這個(gè)類(lèi)無(wú)論多么長(zhǎng),返回的肯定就是一個(gè)字符串?dāng)?shù)組,不信你自己看:
這個(gè)字符串?dāng)?shù)組存放的內(nèi)容我們是否清楚呢?當(dāng)然清楚了!我們返回的是要加載的Config配置文件的全包名,通過(guò)返回這個(gè)全包名,我們就能自動(dòng)裝配上這些配置文件下定義的bean對(duì)象,從而達(dá)到了自動(dòng)裝配的目的!
根據(jù)剛才我們自己實(shí)現(xiàn)的selectImports方法,我們是通過(guò)注解類(lèi)的名字來(lái)查找,并且最終得到需要加載的Config類(lèi)的全類(lèi)名,最后返回的。
因此,這里必然有一個(gè)根據(jù)注解類(lèi)名字來(lái)查找相應(yīng)的Config文件的操作
我們繼續(xù)反推,看到返回時(shí)的定義如下:
我們發(fā)現(xiàn)autoConfigurationEntry中保存著我們需要的配置信息,它是通過(guò)getAutoConfigurationEntry方法獲取的,于是我們繼續(xù)深入,進(jìn)入getAutoConfigurationEntry方法
這一段代碼真是把人難住了,好大一片,不知道在做什么
此時(shí)此刻,我又回想起了在家鄉(xiāng)的母親,夏天的蟬鳴,池塘的荷花…
回家!有了!我們先想這個(gè)方法應(yīng)該返回什么,根據(jù)我們前面的經(jīng)驗(yàn),這里應(yīng)該返回一個(gè)類(lèi)似于Entry的保存了我們需要的配置信息的對(duì)象
這個(gè)方法返回的是新建的AutoConfigurationEntry對(duì)象,根據(jù)最后一行的構(gòu)造函數(shù)來(lái)看,給他了兩個(gè)參數(shù):
configurations, exclusions
configurations顯然使我們需要的配置文件,也是我們最關(guān)心的,而exclusions字面意思是排除,也就是不需要的,那我們接下來(lái)應(yīng)該關(guān)注configurations到底是怎么來(lái)的
根據(jù)我們前面的經(jīng)驗(yàn),我們是根據(jù)注解類(lèi)名來(lái)從一個(gè)配置文件中讀取出我們需要的Config配置類(lèi),這里configurations就代表了Config配置類(lèi),那么我們應(yīng)該找到一個(gè)入口,這個(gè)入口跟注解相關(guān),并且返回了configurations這個(gè)參數(shù)。
正如我們所料,這個(gè)方法的參數(shù)確實(shí)傳遞過(guò)來(lái)了一個(gè)東西,跟注解有關(guān):
看見(jiàn)那個(gè)大大的Annotation(注解)了嗎!
那么根據(jù)這條”線(xiàn)索“,我們按圖索驥,找到了三行代碼,范圍進(jìn)一步縮小了!
此時(shí)再加上返回了configurations,我們最終確定了一行代碼:
就是這個(gè)getCandidateConfigurations方法,符合我們的要求!
從字面意思上分析,獲取候選的配置,確實(shí)是我們需要的方法
OK,讓我們繼續(xù)前進(jìn),進(jìn)入這個(gè)方法:
這個(gè)方法是不是也似曾相識(shí)呢?我們之前寫(xiě)過(guò)一個(gè)專(zhuān)門(mén)用于讀取配置文件的類(lèi)MyPropertyReader,還記得嗎?
如果你還記得的話(huà),我們自己寫(xiě)的工具類(lèi)里面也是一個(gè)靜態(tài)方法readPropertyForMe來(lái)幫我讀取配置文件
但是我們的配置文件路徑一定是需要指定的,不能亂放。
從這個(gè)loadFactoryNames方法體來(lái)看,好像沒(méi)有給他傳遞一個(gè)具體路徑
但是從下面的Assert斷言中,我們發(fā)現(xiàn)了玄機(jī):
在META-INF/spring.factories文件中沒(méi)有找到自動(dòng)配置類(lèi)Config,你要檢查balabala。。。。
根據(jù)我不太靈光的腦袋的判斷,他的這個(gè)配置文件就叫spring.factories,存放的路徑是META-INF/spring.factories
于是我們打開(kāi)spring boot自動(dòng)裝配的依賴(lài)jar包:
那這個(gè)配置文件里面的內(nèi)容,是不是跟我們想的一樣呢?
原來(lái)如此。
這里的EnableAutoConfiguration注解,正是我們此行的起點(diǎn)啊…
到這里,自動(dòng)裝配到底是什么,應(yīng)該比較清楚了,原來(lái)他是幫我們加載了各種已經(jīng)寫(xiě)好的Config類(lèi)文件,實(shí)現(xiàn)了這些JavaConfig配置文件的重復(fù)利用和組件化
行程不能到此結(jié)束,學(xué)習(xí)不能淺嘗輒止。
我們還有最后一塊(幾塊)面紗沒(méi)有解開(kāi),現(xiàn)在還不能善罷甘休。
讓我們進(jìn)入loadFactoryNames方法:
這個(gè)方法非常簡(jiǎn)短,因?yàn)樗{(diào)用了真正實(shí)現(xiàn)的方法:loadSpringFactories
這一行return代碼我復(fù)制在下面:
loadSpringFactories(classLoader)
.getOrDefault(factoryTypeName, Collections.emptyList());
可以分析得出:loadSpringFactories方法的返回值又調(diào)用了一個(gè)getOrDefault方法,這明顯是一個(gè)容器類(lèi)的方法,目的是從容器中拿點(diǎn)東西出來(lái)
就此推測(cè):loadSpringFactories返回了一個(gè)包含我們需要的Config全類(lèi)名(字符串)的集合容器,然后從這個(gè)集合容器中拿出來(lái)的東西就是我們的configurations
讓我們看這個(gè)loadSpringFactories方法:
它確實(shí)返回了一個(gè)容器:Map<String, List
這個(gè)數(shù)據(jù)結(jié)構(gòu)就非常牛逼了,多值集合映射(我自己的翻譯)簡(jiǎn)單來(lái)說(shuō),一個(gè)key可以對(duì)應(yīng)多個(gè)value,根據(jù)他的返回值,我們可以看到在這個(gè)方法中一個(gè)String對(duì)應(yīng)了一個(gè)List
那么不難想到MultiValueMap中存放的形式:是”注解的類(lèi)名——多個(gè)Config配置類(lèi)“ 讓我們打個(gè)斷點(diǎn)來(lái)驗(yàn)證一下:
果然是這樣,并且@EnableAutoConfiguration注解竟然加載了多達(dá)124個(gè)配置類(lèi)!
接下來(lái)我們繼續(xù)思考:我們來(lái)的目的是獲取configurations,所以無(wú)論你做什么,必須得讀取配置文件,拿到configurations
于是我們?cè)趖ry方法體中果然發(fā)現(xiàn)了這個(gè)操作:
他獲取了一個(gè)路徑urls,那么這個(gè)路徑是否就是我們前面驗(yàn)證的META-INF/spring.factories呢?
我們查看靜態(tài)常量FACTORIES_RESOURCE_LOCATION的值:
果真如此,bingo!繼續(xù)往下看,果然他遍歷了urls中的內(nèi)容,從這個(gè)路徑加載了配置文件:終于看到了我們熟悉的loadProperties方法!
那我們大概就知道了,他確實(shí)是通過(guò)找到路徑,然后根據(jù)路徑讀取了配置文件,然后返回了讀取的result
這就是loadFactoryNames方法的內(nèi)部實(shí)現(xiàn)。
到這里有的人又要問(wèn)了:是不是結(jié)束了?其實(shí)還遠(yuǎn)沒(méi)有!
細(xì)心地朋友已經(jīng)發(fā)現(xiàn)了玄機(jī),隱藏在loadFactoryNames方法的開(kāi)頭和結(jié)尾:
喂喂,這個(gè)返回的result好像并不是直接new出來(lái)的哦
它是從cache緩存中取出來(lái)的,你發(fā)現(xiàn)了沒(méi)有
根據(jù)下面的if判斷,如果從緩存中讀取出來(lái)了result,并且result的結(jié)果不為空,就直接返回,不需要再進(jìn)行下面的讀寫(xiě)操作了,這樣減少了磁盤(pán)頻繁的讀寫(xiě)I/O
同理,在我更新完所有的配置文件資源之后,退出時(shí)也要更新緩存。
關(guān)鍵部分已經(jīng)過(guò)去,讓我們反過(guò)頭來(lái)重新審視一下遺漏的內(nèi)容:
還記得getAutoConfigurationEntry方法嗎?
我們最后來(lái)研究一下這個(gè)類(lèi)除了getCandidateConfigurations還干了哪些事情:
可以看到,這里對(duì)加載進(jìn)來(lái)的配置進(jìn)行了去重、排除的操作,這是為了使得用戶(hù)自定義的排除包生效,同時(shí)避免包沖突異常,在SpringBoot的入口函數(shù)中我們可以通過(guò)注解指定需要排除哪些不用的包:
例如我不使用RabbitMQ的配置包,就把它的配置類(lèi)的class傳給exclude
@SpringBootApplication(exclude = {RabbitAutoConfiguration.class})
我的理解:
Spring Boot的自動(dòng)裝配特性可以說(shuō)是Spring Boot最重要、最核心的一環(huán),正是因?yàn)檫@個(gè)特性,使得我們的生產(chǎn)復(fù)雜性大大降低,極大地簡(jiǎn)化了開(kāi)發(fā)流程,可以說(shuō)是給我們帶來(lái)了巨大的福音了~~
筆者本人對(duì)源碼的理解仍然沒(méi)有那么深刻,只是喜歡分享自己的一些學(xué)習(xí)經(jīng)驗(yàn),希望能和大家共同學(xué)習(xí),畢竟掌握一門(mén)新技術(shù)的快感嘛… 大家都懂的!
寫(xiě)這篇文章耗費(fèi)了巨大的精力,每一個(gè)字均是手碼,真的希望喜歡的朋友可以點(diǎn)贊收藏關(guān)注支持一波,這就是對(duì)我這個(gè)未出世的學(xué)生的最大激勵(lì)了!
最后,我畫(huà)了一份Spring Boot自動(dòng)裝配詳細(xì)流程圖,分享給大家。
Spring Boot自動(dòng)裝配詳細(xì)流程圖:
如有文章對(duì)你有幫助,
“在看”和轉(zhuǎn)發(fā)是對(duì)我最大的支持!
聯(lián)系客服