學習SpringBoot,絕對避不開自動裝配這個概念,這也是SpringBoot的關鍵之一
本人也是SpringBoot的初學者,下面的一些總結都是結合個人理解和實踐得出的,如果有錯誤或者疏漏,請一定一定一定(不是歡迎,是一定)幫我指出,在評論區回覆即可,一起學習!
篇幅較長,希望你可以有耐心.
如果只關心SpringBoot裝配過程,可以直接跳到第7部分
想要理解spring自動裝配,需要明確兩個含義:
-
裝配,裝配什麼?
-
自動,怎麼自動?
文章目錄
- 1. Warm up
- 2. Warm up again
- 3. BeanDefinition
- 4. BeanDefinition結構
- 5. 裝配對象
- 6. My自動裝配
- 6.1 自動裝配之再思考
- 6.2 一個例子
- 6.3 @Import註解
- 6.3.1 @Import(A.class)
- 6.3.2 @Import(MyImportBeanDefinitionRegister.class)
- 6.3.3 @Import(MyImportSelector.class)
- 6.4 例子的研究
- 6.5 將偷懶進行到底
- 7. 自動裝配源碼分析
- 7.1 @SpringBootConfiguration
- 7.2 @ComponentScan
- 7.3 @EnableAutoConfiguration
- 7.4 loadFactoryNames方法
- 7.5 cache探祕
- 7.6 getAutoConfigurationEntry再探
- 8. 自動裝配本質
- 9. 總結
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的自動裝配原理是非常重要的。
首先我們在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類對象
@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>一個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註解,正是我們此行的起點啊…
到這裏,自動裝配到底是什麼,應該比較清楚了,原來他是幫我們加載了各種已經寫好的Config類文件,實現了這些JavaConfig配置文件的重複利用和組件化
7.4 loadFactoryNames方法
行程不能到此結束,學習不能淺嘗輒止。
我們還有最後一塊(幾塊)面紗沒有解開,現在還不能善罷甘休。
讓我們進入loadFactoryNames方法:
這個方法非常簡短,因爲他調用了真正實現的方法:loadSpringFactories
這一行return代碼我複製在下面:
loadSpringFactories(classLoader)
.getOrDefault(factoryTypeName, Collections.emptyList());
可以分析得出:loadSpringFactories方法的返回值又調用了一個getOrDefault方法,這明顯是一個容器類的方法,目的是從容器中拿點東西出來
就此推測:loadSpringFactories返回了一個包含我們需要的Config全類名(字符串)的集合容器,然後從這個集合容器中拿出來的東西就是我們的configurations
讓我們看這個loadSpringFactories方法:
它確實返回了一個容器:Map<String, List<String>>
這個容器的類型是:MultiValueMap<String, String>
這個數據結構就非常牛逼了,多值集合映射(我自己的翻譯)簡單來說,一個key可以對應多個value,根據他的返回值,我們可以看到在這個方法中一個String對應了一個List<String>
那麼不難想到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就實現了自動。
9. 總結
Spring Boot的自動裝配特性可以說是Spring Boot最重要、最核心的一環,正是因爲這個特性,使得我們的生產複雜性大大降低,極大地簡化了開發流程,可以說是給我們帶來了巨大的福音了~~
筆者本人還是一名學生,對源碼的理解仍然沒有那麼深刻,只是喜歡分享自己的一些學習經驗,希望能和大家共同學習,畢竟掌握一門新技術的快感嘛… 大家都懂的!
寫這篇文章耗費了巨大的精力,每一個字均是手碼,真的希望喜歡的朋友可以點贊收藏關注支持一波,這就是對我這個未出世的學生的最大激勵了!
最後,我整理了本文中用到的項目源碼,還有一份Spring Boot自動裝配詳細流程圖(自己畫的),如果有需要的朋友可以在文章下面留下你的郵箱,我會盡快回復!
如果文章內容有錯誤,歡迎在評論區指出,感謝捉蟲!