SpringBoot自動裝配原理,這一篇就夠了!

學習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文件,在裏面加入:

<!-- 手動配置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定義

喂!你是不是搞我!繞了一大圈,怎麼還是加載這個配置文件啊!這個配置文件明明就是我自己寫的。

總結一下,我們這個例子大概繞了這些過程:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mxB37Xzr-1588939783485)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508162724344.png)]


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方法:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jxz7sKtE-1588939783486)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508182925585.png)]

我的天,好長的一串代碼,一行都放不下!

此時此刻,我又回想起了在家鄉的母親,夏天的蟬鳴,池塘的荷花…

等等等等,這個類我們當時返回的是什麼?是一個字符串數組String[ ],那這個類無論多麼長,返回的肯定就是一個字符串數組,不信你自己看:

這個字符串數組存放的內容我們是否清楚呢?當然清楚了!我們返回的是要加載的Config配置文件的全包名,通過返回這個全包名,我們就能自動裝配上這些配置文件下定義的bean對象,從而達到了自動裝配的目的!

根據剛纔我們自己實現的selectImports方法,我們是通過註解類的名字來查找,並且最終得到需要加載的Config類的全類名,最後返回的。

因此,這裏必然有一個根據註解類名字來查找相應的Config文件的操作

我們繼續反推,看到返回時的定義如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zoGSMu1g-1588939783488)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508183913236.png)]

我們發現autoConfigurationEntry中保存着我們需要的配置信息,它是通過getAutoConfigurationEntry方法獲取的,於是我們繼續深入,進入getAutoConfigurationEntry方法

這一段代碼真是把人難住了,好大一片,不知道在做什麼

此時此刻,我又回想起了在家鄉的母親,夏天的蟬鳴,池塘的荷花…

回家!有了!我們先想這個方法應該返回什麼,根據我們前面的經驗,這裏應該返回一個類似於Entry的保存了我們需要的配置信息的對象

這個方法返回的是新建的AutoConfigurationEntry對象,根據最後一行的構造函數來看,給他了兩個參數:

configurations, exclusions

configurations顯然使我們需要的配置文件,也是我們最關心的,而exclusions字面意思是排除,也就是不需要的,那我們接下來應該關注configurations到底是怎麼來的

根據我們前面的經驗,我們是根據註解類名來從一個配置文件中讀取出我們需要的Config配置類,這裏configurations就代表了Config配置類,那麼我們應該找到一個入口,這個入口跟註解相關,並且返回了configurations這個參數。

正如我們所料,這個方法的參數確實傳遞過來了一個東西,跟註解有關:

看見那個大大的Annotation(註解)了嗎!
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vqCBeWrN-1588939783490)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508184921167.png)]
那麼根據這條”線索“,我們按圖索驥,找到了三行代碼,範圍進一步縮小了!

此時再加上返回了configurations,我們最終確定了一行代碼:
在這裏插入圖片描述
就是這個getCandidateConfigurations方法,符合我們的要求!

從字面意思上分析,獲取候選的配置,確實是我們需要的方法

OK,讓我們繼續前進,進入這個方法:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lrSIMzkO-1588939783491)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508185122648.png)]
這個方法是不是也似曾相識呢?我們之前寫過一個專門用於讀取配置文件的類MyPropertyReader,還記得嗎?

如果你還記得的話,我們自己寫的工具類裏面也是一個靜態方法readPropertyForMe來幫我讀取配置文件

但是我們的配置文件路徑一定是需要指定的,不能亂放。

從這個loadFactoryNames方法體來看,好像沒有給他傳遞一個具體路徑

但是從下面的Assert斷言中,我們發現了玄機:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-362Uw2un-1588939783492)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508185311730.png)]
在META-INF/spring.factories文件中沒有找到自動配置類Config,你要檢查balabala。。。。

根據我不太靈光的腦袋的判斷,他的這個配置文件就叫spring.factories,存放的路徑是META-INF/spring.factories

於是我們打開spring boot自動裝配的依賴jar包:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-20LlGwoO-1588939783493)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508185730439.png)]

那這個配置文件裏面的內容,是不是跟我們想的一樣呢?

原來如此。

這裏的EnableAutoConfiguration註解,正是我們此行的起點啊…

到這裏,自動裝配到底是什麼,應該比較清楚了,原來他是幫我們加載了各種已經寫好的Config類文件,實現了這些JavaConfig配置文件的重複利用和組件化


7.4 loadFactoryNames方法

行程不能到此結束,學習不能淺嘗輒止。

我們還有最後一塊(幾塊)面紗沒有解開,現在還不能善罷甘休。

讓我們進入loadFactoryNames方法:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-88lF8sNH-1588939783493)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508190213629.png)]
這個方法非常簡短,因爲他調用了真正實現的方法: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配置類“
讓我們打個斷點來驗證一下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Vg7Lh3VU-1588939783495)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508194518140.png)]
果然是這樣,並且@EnableAutoConfiguration註解竟然加載了多達124個配置類!

接下來我們繼續思考:我們來的目的是獲取configurations,所以無論你做什麼,必須得讀取配置文件,拿到configurations

於是我們在try方法體中果然發現了這個操作:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mnanTXyI-1588939783496)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508192007209.png)]
他獲取了一個路徑urls,那麼這個路徑是否就是我們前面驗證的META-INF/spring.factories呢?
我們查看靜態常量FACTORIES_RESOURCE_LOCATION的值:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CXkKntfH-1588939783497)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508192121252.png)]
果真如此,bingo!
繼續往下看,果然他遍歷了urls中的內容,從這個路徑加載了配置文件:
終於看到了我們熟悉的loadProperties方法!
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-c1nPBjBg-1588939783497)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508192706813.png)]
那我們大概就知道了,他確實是通過找到路徑,然後根據路徑讀取了配置文件,然後返回了讀取的result

這就是loadFactoryNames方法的內部實現。


7.5 cache探祕

到這裏有的人又要問了:是不是結束了?其實還遠沒有!

細心地朋友已經發現了玄機,隱藏在loadFactoryNames方法的開頭和結尾:

喂喂,這個返回的result好像並不是直接new出來的

它是從cache緩存中取出來的,你發現了沒有

根據下面的if判斷,如果從緩存中讀取出來了result,並且result的結果不爲空,就直接返回,不需要再進行下面的讀寫操作了,這樣減少了磁盤頻繁的讀寫I/O

同理,在我更新完所有的配置文件資源之後,退出時也要更新緩存。


7.6 getAutoConfigurationEntry再探

關鍵部分已經過去,讓我們反過頭來重新審視一下遺漏的內容:

還記得getAutoConfigurationEntry方法嗎?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fdxZanAC-1588939783498)(/Users/dzzhyk/Desktop/apeBook/src/image-20200508195119398.png)]
我們最後來研究一下這個類除了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自動裝配詳細流程圖(自己畫的),如果有需要的朋友可以在文章下面留下你的郵箱,我會盡快回復!

如果文章內容有錯誤,歡迎在評論區指出,感謝捉蟲!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章