日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

SpringBoot自動(dòng)裝配原理,這一篇就夠了!

 昵稱10087950 2022-06-16 發(fā)布于江蘇
來(lái)源:blog.csdn.net/weixin_43826242?type=blog

前言

學(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)?

1. Warm up

在開(kāi)始之前,讓我們先來(lái)看點(diǎn)簡(jiǎn)單的開(kāi)胃菜:spring中bean注入的三種形式

首先我們先來(lái)一個(gè)Person類,這里為了篇幅長(zhǎng)度考慮使用了lombok

如果你不知道lombok是什么,那就最好不要知道,加了幾個(gè)注解之后我的pojo類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種注入方式:

1.1 setter注入

這是最基本的注入方式

首先我們創(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類

/**
 * 第一種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)獲取這個(gè)Person的Bean對(duì)象,結(jié)果成功輸出:

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

1.2 構(gòu)造器注入

接下來(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)

1.3 屬性注入

使用注解方式的屬性注入Bean是比較優(yōu)雅的做法

首先我們需要在applicationContext.xml中開(kāi)啟注解支持和自動(dòng)包掃描:

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

在pojo類中對(duì)Person類加上@Component注解,將其標(biāo)記為組件,并且使用@Value注解為各屬性賦初值

@Component
public class Person {
    
    @Value("dzzhyk")
    private String name;
    @Value("20")
    private Integer age;
    @Value("true")
    private Boolean sex;
}

然后添加新的測(cè)試類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)

2. Warm up again

什么?還有什么?接下來(lái)我們來(lái)聊聊Spring的兩種配置方式:基于XML的配置和基于JavaConfig類的配置方式,這對(duì)于理解SpringBoot的自動(dòng)裝配原理是非常重要的。

首先我們?cè)赑erson的基礎(chǔ)上再創(chuàng)建幾個(gè)pojo類:這個(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;
}

2.1 基于XML的配置

接下來(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))

2.2 基于JavaConfig類的配置

想要成為JavaConfig類,需要使用@Configuration注解

我們新建一個(gè)包命名為config,在config中新增一個(gè)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("旺財(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))

3. BeanDefinition

AbstractBeanDefinition

是spring中所有bean的抽象定義對(duì)象,我把他叫做bean定義

當(dāng)bean.class被JVM類加載到內(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è)接口的類,可以在bean實(shí)例化之前完成一些對(duì)bean的改動(dòng)。

大致流程我畫(huà)了個(gè)圖:

圖片

至此我們能總結(jié)出springIOC容器的本質(zhì):(我的理解)

由BeanDefinitionMap、BeanFactoryPostProcessor、BeanPostProcessor、BeanMap等等容器共同組成、共同完成、提供依賴注入和控制反轉(zhuǎn)功能的一組集合,叫IOC容器。

4. BeanDefinition結(jié)構(gòu)

既然講到了BeanDefinition,我們來(lái)看一下BeanDefinition里面究竟定義了些什么

讓我們點(diǎn)進(jìn)AbstractBeanDefinition這個(gè)類,一探究竟:

圖片

哇!好多成員變量,整個(gè)人都要看暈了@_@

我們來(lái)重點(diǎn)關(guān)注以下三個(gè)成員:

  private volatile Object beanClass;
  private int autowireMode = AUTOWIRE_NO;
  private ConstructorArgumentValues constructorArgumentValues;

4.1 beanClass

這個(gè)屬性決定了該Bean定義的真正class到底是誰(shuí),接下來(lái)我們來(lái)做點(diǎn)實(shí)驗(yàn)

我們定義兩個(gè)Bean類,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)配置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è)試類:

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類,輸出了一些信息后:報(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類對(duì)象,你怎么給我一個(gè)B類對(duì)象呢?這明顯不對(duì)

綜上所述,我們可以得出beanClass屬性控制bean定義的類

4.2 autowireMode

我們繼續(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ò)名稱裝配,通過(guò)類型裝配,通過(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è)類,現(xiàn)在在A類中添加B類對(duì)象

@Component
public class A {
    @Value("我是AAA")
    private String name;
    @Autowired
    private B b;
}

我們希望b對(duì)象能夠自動(dòng)裝配,于是我們給他加上了@Autowired注解,其他的完全不變,我們自定義的MyBeanPostProcessor中也不做任何操作,讓我們運(yùn)行測(cè)試類:

這里是MyBeanPostProcessor,我拿到了:pojo.A
最終拿到了==> A(name=我是AAA, b=B(name=我是BBB))

自動(dòng)裝配成功了!我們拿到的A類對(duì)象里面成功注入了B類對(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)裝配

4.3 constructorArgumentValues

constructorArgumentValues的字面含義是構(gòu)造器參數(shù)值

改變這個(gè)參數(shù)值,我們可以做到在實(shí)例化對(duì)象時(shí)指定特定的構(gòu)造器

話不多說(shuō),show me your code:

因?yàn)橐芯繕?gòu)造器,只能先”忍痛“關(guān)掉lombok插件,手寫(xiě)一個(gè)pojo.Student類

/**
 * Student類
 * @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è)類就是一個(gè)普通的包裝類,包裝的對(duì)象是ValueHolder,里面一個(gè)List一個(gè)Map<Integer, ValueHolder>

而ValueHolder這個(gè)對(duì)象繼承于BeanMetadataElement,就是構(gòu)造器參數(shù)的一個(gè)包裝類型

通過(guò)這個(gè)例子我們可以看到ConstructorArgumentValues就是用來(lái)管控構(gòu)造器參數(shù)的,指定這個(gè)值會(huì)在進(jìn)行bean注入的時(shí)候選擇合適的構(gòu)造器。

5. 裝配對(duì)象

現(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)

6. My自動(dòng)裝配

看到這里又自然會(huì)產(chǎn)生疑問(wèn):不會(huì)吧,上面可都是自動(dòng)裝配啊,我在配置文件或者使用注解都配置了變量的值,然后加個(gè)@Autowired注解就OK了,spring也是幫我自動(dòng)去裝配。

再高端一點(diǎn)話,我就把XML文件寫(xiě)成JavaConfig配置類,然后使用@Configuration注解,這樣也能自動(dòng)裝配,這不是很nice了嗎?

6.1 自動(dòng)裝配之再思考

我的理解,上面的自動(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

6.2 一個(gè)例子

讓我們?cè)谕祽械牡缆飞侠^續(xù)前進(jìn)。

來(lái)看下面這個(gè)例子:

仍然是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注解來(lái)自動(dòng)注入b,測(cè)試類如下:

@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配置類會(huì)使用到的加載類,于是我們順利成章地點(diǎn)開(kāi)它所加載的MyAutoConfig類文件

文件內(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啊…

6.3 @Import注解

@Import的功能就是獲取某個(gè)類的bean對(duì)象,他的使用形式大致如下:

@Import(A.class)
@Import(MyImportBeanDefinitionRegister.class)
@Import(MyImportSelector.class)

6.3.1 @Import(A.class)

第一種形式@Import(A.class),是最簡(jiǎn)單易懂的形式

我們需要哪個(gè)Bean定義,直接Import他的class即可

6.3.2 @Import(MyImportBeanDefinitionRegister.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定義,他的類型就是A,然后把這個(gè)bean定義注冊(cè)到BeanDefinitionMap(還記得吧!)里面,key值我們可以人為設(shè)置,這里就設(shè)置成"a"

這樣在傳遞一個(gè)注冊(cè)器的時(shí)候,我們就可以把注冊(cè)器中新增的bean定義注冊(cè)進(jìn)來(lái)使用

6.3.3 @Import(MyImportSelector.class)

可以看到,這種使用方式就是我們剛才的注解中使用的方式

他傳遞了一個(gè)叫MyImportSelector的類,這個(gè)類依然是我們自己定義的,具體內(nèi)容如下:

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 導(dǎo)入配置類
        return new String[]{"config.MyConfig"};
    }
}

這個(gè)類實(shí)現(xiàn)了ImportSelector接口,并且重寫(xiě)了selectImports方法,返回一個(gè)字符串?dāng)?shù)組

我們可以看到,返回的字符串?dāng)?shù)組中是我們要導(dǎo)入類的全類名

這個(gè)Importer返回的類如果是組件bean對(duì)象,就會(huì)被加載進(jìn)來(lái)使用;如果是一個(gè)配置類,就會(huì)加載這個(gè)配置類

第三種和第二種的區(qū)別是第三種可以一次性寫(xiě)很多類,而且比較簡(jiǎn)潔,只需要清楚類的全包名即可。而第二種方式需要自己清楚包類名,手動(dòng)創(chuàng)建bean定義,然后手動(dòng)加入BeanDefinitionMap。

6.4 例子的研究

我們打開(kāi)MyImportSelector,發(fā)現(xiàn)里面赫然寫(xiě)著幾個(gè)大字:

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

然后我們找到config.MyConfig類,發(fā)現(xiàn)這個(gè)類竟然就是我們剛才寫(xiě)的JavaConfig版本的配置文件:

@Configuration
public class MyConfig {
    @Bean
    public A a(){
        return new A();
    }

    @Bean
    public B b(){
        return new B();
    }
}

加載這個(gè)MyConfig配置類,就相當(dāng)于加載了A和B兩個(gè)Bean定義

喂!你是不是搞我!繞了一大圈,怎么還是加載這個(gè)配置文件??!這個(gè)配置文件明明就是我自己寫(xiě)的。

圖片

總結(jié)一下,我們這個(gè)例子大概繞了這些過(guò)程:

圖片

6.5 將偷懶進(jìn)行到底

"沒(méi)有會(huì)偷懶的人解決不掉的問(wèn)題“ —— 魯迅

上面的例子也沒(méi)有多大優(yōu)化啊,我怎么覺(jué)得更加麻煩了?不但繞了一大圈,定義了許多新東西,到最后還是加載了我寫(xiě)好的JavaConfig類,說(shuō)到底我不是還在寫(xiě)javaConfig類嗎…

但是你注意到?jīng)]有:有了上面的機(jī)制,我只需要把JavaConfig類寫(xiě)一次,然后放在某個(gè)地方,在MyImportSelector中加入這個(gè)地方的全包名路徑,下次用的時(shí)候直接導(dǎo)入最頂層的MyAutoConfig類,所有有關(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)專門(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文件的工具類

之所以要自己再定義這樣一個(gè)工具類,是為了以后在其中可以做一些其他操作(比如:去重、預(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的類名,也就是根據(jù)這個(gè)注解,就會(huì)導(dǎo)入后面的MyConfig這個(gè)Bean,這個(gè)Bean就是我們的配置文件

如此一來(lái)我們讀取這個(gè)配置文件,然后加載跟這個(gè)注解名稱相符的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)行程序,可以看到成功拿到了配置文件的全類名

圖片

程序的運(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)了

7. 自動(dòng)裝配源碼分析

終于來(lái)到了大家喜聞樂(lè)見(jiàn)的部分:源碼分析

在我們前面6節(jié)學(xué)習(xí)了各種”招式“之后,讓我們請(qǐng)出對(duì)手:SpringBoot

現(xiàn)在在你面前的是一個(gè)SpringBoot”空項(xiàng)目“,沒(méi)有添加任何依賴包和starter包

圖片

啟動(dòng)項(xiàng)目:

圖片

正常啟動(dòng),讓我們從@SpringBootApplication開(kāi)始研究

7.1 @SpringBootConfiguration

會(huì)看到@SpringBootApplication這個(gè)注解由好多注解組成

主要的有以下三個(gè):

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

先來(lái)看第一個(gè):@SpringBootConfiguration

進(jìn)入這個(gè)注解之后會(huì)發(fā)現(xiàn)

圖片

原來(lái)你就是一個(gè)@Configuration啊,一個(gè)JavaConfig配置類

那我們使用JavaConfig不就是用來(lái)配置bean嗎,所以有了這個(gè)注解之后我們可以在SpringBoot運(yùn)行的主類中使用@Bean標(biāo)簽配置類了,如下圖所示:

圖片

7.2 @ComponentScan

這個(gè)注解相信大家都認(rèn)識(shí)了,組件掃描

這個(gè)掃描的范圍是:SpringBoot主啟動(dòng)類的同級(jí)路徑及子路徑

圖片

7.3 @EnableAutoConfiguration

來(lái)看這個(gè)注解,也是最核心的內(nèi)容

這個(gè)注解怎么這么眼熟啊,還記得剛才的@MyEnableAutoConfig注解嗎?就是我們自己寫(xiě)的那個(gè)注解

進(jìn)入@EnableAutoConfiguration:

圖片

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

是不是跟我們上面自己寫(xiě)的內(nèi)容一樣!

這里的作用便是導(dǎo)入了 AutoConfigurationImportSelector 這個(gè)類的bean定義

我們都知道,如果這個(gè)類實(shí)現(xiàn)了ImportSelector接口,那他肯定重寫(xiě)了一個(gè)方法,就是我們上面重寫(xiě)過(guò)的selectImports方法:

果然,在這個(gè)類里面確實(shí)有這個(gè)selectImports方法:

圖片

我的天,好長(zhǎng)的一串代碼,一行都放不下!

此時(shí)此刻,我又回想起了在家鄉(xiāng)的母親,夏天的蟬鳴,池塘的荷花…

等等等等,這個(gè)類我們當(dāng)時(shí)返回的是什么?是一個(gè)字符串?dāng)?shù)組String[ ],那這個(gè)類無(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)查找,并且最終得到需要加載的Config類的全類名,最后返回的。

因此,這里必然有一個(gè)根據(jù)注解類名字來(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è)類似于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)從一個(gè)配置文件中讀取出我們需要的Config配置類,這里configurations就代表了Config配置類,那么我們應(yīng)該找到一個(gè)入口,這個(gè)入口跟注解相關(guān),并且返回了configurations這個(gè)參數(shù)。

正如我們所料,這個(gè)方法的參數(shù)確實(shí)傳遞過(guò)來(lái)了一個(gè)東西,跟注解有關(guān):

看見(jiàn)那個(gè)大大的Annotation(注解)了嗎!

圖片

那么根據(jù)這條”線索“,我們按圖索驥,找到了三行代碼,范圍進(jìn)一步縮小了!

此時(shí)再加上返回了configurations,我們最終確定了一行代碼:

圖片

就是這個(gè)getCandidateConfigurations方法,符合我們的要求!

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

OK,讓我們繼續(xù)前進(jìn),進(jìn)入這個(gè)方法:

圖片

這個(gè)方法是不是也似曾相識(shí)呢?我們之前寫(xiě)過(guò)一個(gè)專門(mén)用于讀取配置文件的類MyPropertyReader,還記得嗎?

如果你還記得的話,我們自己寫(xiě)的工具類里面也是一個(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)配置類Config,你要檢查balabala。。。。

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

于是我們打開(kāi)spring boot自動(dòng)裝配的依賴jar包:

圖片

那這個(gè)配置文件里面的內(nèi)容,是不是跟我們想的一樣呢?

圖片

原來(lái)如此。

這里的EnableAutoConfiguration注解,正是我們此行的起點(diǎn)啊…

到這里,自動(dòng)裝配到底是什么,應(yīng)該比較清楚了,原來(lái)他是幫我們加載了各種已經(jīng)寫(xiě)好的Config類文件,實(shí)現(xiàn)了這些JavaConfig配置文件的重復(fù)利用和組件化

7.4 loadFactoryNames方法

行程不能到此結(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è)容器類的方法,目的是從容器中拿點(diǎn)東西出來(lái)

就此推測(cè):loadSpringFactories返回了一個(gè)包含我們需要的Config全類名(字符串)的集合容器,然后從這個(gè)集合容器中拿出來(lái)的東西就是我們的configurations

讓我們看這個(gè)loadSpringFactories方法:

圖片

它確實(shí)返回了一個(gè)容器:Map<String, List> 這個(gè)容器的類型是:MultiValueMap<String, String>

這個(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中存放的形式:是”注解的類名——多個(gè)Config配置類“ 讓我們打個(gè)斷點(diǎn)來(lái)驗(yàn)證一下:

圖片

果然是這樣,并且@EnableAutoConfiguration注解竟然加載了多達(dá)124個(gè)配置類!

接下來(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)。

7.5 cache探秘

到這里有的人又要問(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í)也要更新緩存。

7.6 getAutoConfigurationEntry再探

關(guān)鍵部分已經(jīng)過(guò)去,讓我們反過(guò)頭來(lái)重新審視一下遺漏的內(nèi)容:

還記得getAutoConfigurationEntry方法嗎?

圖片

我們最后來(lái)研究一下這個(gè)類除了getCandidateConfigurations還干了哪些事情:

  • removeDuplicates
  • configurations.removeAll(exclusions)

可以看到,這里對(duì)加載進(jìn)來(lái)的配置進(jìn)行了去重、排除的操作,這是為了使得用戶自定義的排除包生效,同時(shí)避免包沖突異常,在SpringBoot的入口函數(shù)中我們可以通過(guò)注解指定需要排除哪些不用的包:

例如我不使用RabbitMQ的配置包,就把它的配置類的class傳給exclude

@SpringBootApplication(exclude = {RabbitAutoConfiguration.class})

8. 自動(dòng)裝配本質(zhì)

我的理解:

  • SpringBoot自動(dòng)裝配的本質(zhì)就是通過(guò)Spring去讀取META-INF/spring.factories中保存的配置類文件然后加載bean定義的過(guò)程。
  • 如果是標(biāo)了@Configuration注解,就是批量加載了里面的bean定義
  • 如何實(shí)現(xiàn)”自動(dòng)“:通過(guò)配置文件獲取對(duì)應(yīng)的批量配置類,然后通過(guò)配置類批量加載bean定義,只要有寫(xiě)好的配置文件spring.factories就實(shí)現(xiàn)了自動(dòng)。

9. 總結(jié)

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ì)我最大的支持!

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多