99%的人把Spring的自動注入理解錯了!(範例→源碼證明)

突然發現現在有很多人把 @Autowired 註解加在屬性上,認爲是自動注入。

筆者閱讀官網之後,發現這是一個天大的誤解
於是,筆者試着,寫一些代碼案例,來證明,這和 Spring 所提供的自動注入是有很大差別的。
當然,通過源碼,更能直觀的說明,這不屬於 Spring 所提供的自動注入。

再談自動注入之前,首先得先明白,什麼是依賴注入:
官網上的描述有些過於複雜,對於我們來說,簡單點理解,就是把一個 bean 作爲另一個 bean 的屬性填充進去。

不過,這麼一句簡單的話,背後隱藏着許許多多的知識點:

  • 首先,怎麼注入呢?
  • Spring 又該怎麼尋找的注入的對象呢?
  • Spring 又要如何判斷,注入的對象,是否是正確的呢?

這是最根本的問題,到後文再進行討論。

首先,要弄明白的是,Spring 有哪些注入的方式?
(我們至少要先把應用理解清楚)

在 Spring 的官網上,官方給出的解釋是:
DI 存在兩個主要變體:基於構造函數的依賴注入基於 Setter 的依賴注入

也就是說,其實 Spring 的依賴注入,粗劃分,就是兩種:

  • 一種是用構造方法,傳參注入;
  • 還有一種,就是 new 了對象之後,再注入;

也就是說,我們通常使用的 set 方法注入,就是基於 Setter 的依賴注入的一種變體;
@Autowired 註解注入,也是基於 Setter 的依賴注入的一種變體,它們本質上是出於一家的。

不過,雖是同根生,但畢竟,它們已經不再是同一種方式了。

現在,我們來一點點演示。

先:
最普通的,我們此時,通過一個 xml 來配置 bean 的依賴關係。

@Component
public class A {
	private B b;
	public void setB(B b) {
		this.b = b;
	}
	public B getB() {
		return b;
	}
}
<bean id="a" class="com.jiang.bean.A">
	<property name="b" ref="b"></property>
</bean>

<bean id="b" class="com.jiang.bean.B">
</bean>
public static void main(String[] args) {
	ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
	A a = (A) applicationContext.getBean("a");
	// 我們來打印a中注入的b
	System.out.println(a.getB());
}

在這裏插入圖片描述
可以發現,注入成功了。

然後,我們再來看一個基於註解 @Autowired 的例子,大家可能平時會把它當做自動注入。
(這麼說實際上不對,馬上我來解釋)

@Component
public class A {
	@Autowired
	private B b;
	// 沒有提供set方法
	public B getB() {
		return b;
	}
}
public static void main(String[] args) {
	ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
	A a = (A) applicationContext.getBean("a");
	System.out.println(a.getB());
}

在這裏插入圖片描述
然後我們可以發現,雖然沒有 set 方法,但是仍然注入成功了。
而且,由於沒有配置,指定的注入的對象是誰,所以人們公認爲這是自動注入。

不要心急,我繼續寫一個例子,慢慢對比,你就會慢慢理解:
這時,我把第一個例子,配置的 xml 裏面,a 的屬性指定註釋掉,而給 bean 添加了一個 autowire=“byType”。

<bean id="a" class="com.jiang.bean.A" autowire="byType">
	<!--<property name="b" ref="b"></property>-->
</bean>

在這裏插入圖片描述
這時,我們發現,A能注入B,但是在 xml 配置文件中並沒有去手動維護、描述他們之間的依賴關係,而是在 xml 的那個 bean 上面寫了 autowire=“byType”。
而關於自動注入的歧義或者被人誤解的地方就是這個 autowire=“byType” 引起的。

有些人發現了 @Autowired 不等於 byType,因爲,它百度出來,@Autowired 會先根據類型找,然後又根據名字找。
所以說 @Autowired 同時包含了 byType 和 byName。
但是,這麼說就是誤人子弟!!!

那麼,我舉了這些例子,有什麼用意呢?

這就牽扯到了一個很常見的面試題:
spring有幾種依賴注入方式?

Spring 在官網中描述了有 4 種自動裝配模式(Autowiring modes):

  • no
  • byName
  • byType
  • constructor

在這裏插入圖片描述
我覺得大部分人都是會回答這個問題的,都早已耳熟能詳。

但是假設面試官深入問:
依賴注入和裝配模型有什麼關係和區別,很多人可能就會回答不出來。

對於依賴注入和注入模型,我們從一個教簡單的角度來回答:

  • 依賴注入是一個過程,主要通過 setter 和構造方法以及一些變體的方式完成把對象依賴、或者填充上的這個過程叫做依賴注入,不管手動裝配還是自動裝配都有這個過程;
  • 而自動裝配模型是一種完成自動裝配依賴的手段體現,每一種模型都使用了不同的技術去查找和填充 bean;

那麼,它們之間又有什麼聯繫呢?
還記不記得我之前說的:
依賴注入存在兩個主要變體:基於構造函數的依賴注入基於 Setter 的依賴注入

  • 對於 byName 和 byType,都是基於 setter 的依賴注入;
  • 對於 constructor,則是基於構造函數的依賴注入。

可能很多人會不理解,這麼說好像跟沒說沒有什麼區別!
那麼,接下來,我就要給你演示,其中的注意點:

首先,我把代碼 A 進行了修改,沒有屬性,只有一個 set 方法。

@Component
public class A {
	public void setB(B b) {
		System.out.println("set方法被調用");
	}
}

然後,不論是 ref=“b” 或者是 byType 或者 byName
都會輸出如下結果:
在這裏插入圖片描述

也就是,基於 Setter 的依賴注入與屬性無關!

也就是,不論是你手動注入,還是你自動注入,只要是基於 Setter 的依賴注入,那麼就一定會去調用我們的 setter 方法,而根本不關心是否真的有這麼一個屬性。

所以,對於 Spring 來說,set 注入,不過是一個 set 方法調用,只不過我們平時都用於完成屬性注入,這已經成爲了業內的一種規範。
而對於 Spring 這樣的底層實現來說,它要完成的,只是一個對需求的實現,對這麼一個標準的實現,調用 set 方法,就是完成了它的實現,我們只要讓 set 方法注入屬性,就能完成屬性填充。
而至於,這個方法到底做了什麼,則不是 Spring 關心的事。

現在,我再修改一下代碼:

public void setXXXXXXX(B b) {
	System.out.println("set方法被調用");
}

在這裏插入圖片描述
發現,set 後面即使胡亂寫一堆名字,方法仍然會被調用。

然後,嘗試把方法名的 set 也去掉,改成亂七八糟的名字:

public void xxx(B b) {
	System.out.println("set方法被調用");
}

在這裏插入圖片描述
發現,程序跑完了都沒有執行那個方法。

所以,這個現象也可以證明,基於 Setter 的依賴注入,Spring 看的,就是方法的名字,是不是 setxxxxx…
如果是的,並且容器裏存在這個 bean 可以傳參給它,就會調用這個方法。

所以,setter 的依賴注入,Spring 關注的是 setter 方法,而根本不關心 bean 的屬性。

那麼,Autowired 的呢?
我到這裏還沒有對上面拋出的問題進行解答。

首先,@Autowired 在不需要 setter 方法的情況下也可以注入,所以,這個已經不屬於常規的 set 方法注入。
不過我們可以說,是 setter 依賴注入方式的一種的變體。
不過,也可以說是一種不同於以上的一種新的方式。因爲 Spring 並沒有給出明確的定義,我們程序員也不必鑽牛角尖,非要給一個定義,我們更重要的,是理解它的實現和原理。

除了不用 set 方法之外,爲什麼說 @Autowired 的不是自動裝配呢?
我們看起來沒有指定注入的 bean,很顯然是 Spring 自動找到 bean 來進行注入的,那爲什麼說不是自動注入呢?

首先,並不是說它完全剔除了自動,只是爲了區別於 Spring 所聲明的 4 種注入模型。
很多人把 @Autowired 當做是一種 byType 的注入模型,這是完全不對的。

@Autowired 包含了一定的自動性,但是並不和其它的自動注入一樣,是完全的自動。

  • 我們之前其實可以發現,如果配置了自動注入,則不需要在 set 方法,以及屬性上添加任何標註,Spring 會自動的尋找需要注入的地方進行注入;
  • 而對於 @Autowired 註解,需要手動指定在某個屬性,或者某個方法上,來指定,要在這個地方進行注入。

也就是,自動注入,不用指定哪些需要注入,而 @Autowired,需要指定哪些屬性或者方法需要注入。

這是把 @Autowired 區別於 byType 的第一個原因,因爲沒有完全消除自動化

第二,@Autowired 實際上和 byType 的注入選擇也不一樣:

  • byType 是會根據類型去找合適的 bean,如果無法確定類型,就直接放棄注入;
  • 但是 @Autowired 會先根據類型找,如果找到多個同類型的,它不會直接放棄注入,
    而會繼續再找,是否有名字符合的,如果還找不到纔會罷休。

於是,我增加了一個接口 I;
讓 B、C 實現這個接口;
那麼 A 在注入 I 的時候,就會找到兩個 bean 可以注入:

@Component
public class A {
	private I b;

	public void setB(I b) {
		this.b = b;
	}
}

在這裏插入圖片描述
於是,在使用 byType 自動注入的時候,就把偶錯了,因爲找到兩個可以注入的 bean,所以 Spring 就會迷茫,無法再進行注入,就會報錯。

那麼,就不用 set 方法自動注入,我們用 @Autowired 來注入

@Component
public class A {
	@Autowired
	private I b;
}

在這裏插入圖片描述
這時,我們就會發現,用 @Autowired 注入成功了,並且注入的是 b,也就是我們屬性名字也進行了指定。
所以,@Autowired 區別於 byType 的第二個原因,就是因爲,它並不是只根據類型注入。

第三,我覺得說服力更大的則是,代碼中可以獲取 bean 的自動注入模型,所以,我們只要看一下,代碼返回的 bean 的自動注入模型,那麼就足以證明。
(因爲 Spring 的源碼都這麼寫了,還能有錯?)
在這裏插入圖片描述
查看 Spring 源碼,我們可以分別看到,這四種注入模型分別對應着 0、1、2、3。

這時,我給屬性添加了 @Autowired 註解,然後直接在控制檯,打印該 bean 的自動注入模型是哪一個。

public static void main(String[] args) {
	ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
	// 打印注入模型的信息
	System.out.println(((GenericBeanDefinition) ((AnnotationConfigApplicationContext) applicationContext).getBeanFactory().getBeanDefinition("a")).getAutowireMode());
}

在這裏插入圖片描述
然後,結果打印出是 0,再一次證明了,Spring 的 @Autowired 屬於注入模型的 no,就是無自動裝配。

所以,對於 @Autowired 註解來注入屬性,實際上僅僅只是一個 setter 注入的一個過程,而不屬於任何一種自動注入模型。
它屬於 Spring 後來開發出的一種新的注入方式,而不是 Spring 原始的自動注入模型中的一種。

那麼,既然區分清楚了自動注入的概念,接下來,就要從源碼的角度來進行分析。

首先,對於 Spring 來說,我們都比較清楚,bean 的創建有合併 bean 定義信息、實例化、查找元數據、屬性填充、調用生命週期回調方法、AOP 代理等等的過程。
而此處探討的自動注入,則一定在屬性填充這一個過程中,所以我們來找到屬性填充的源代碼。

我們可以先 debug 一下,查看 @Autowired 和 byType 分別是在什麼時候注入:
我們可以先看如果是自動注入,set 方法何時被調用:
在這裏插入圖片描述
在這裏插入圖片描述
我們發現,在 applyPropertyValues(beanName, mbd, bw, pvs); 這一行代碼處:
還沒有運行的時候,沒有執行 set 方法,所以控制檯沒有打印信息;
而等到這一行代碼執行了,控制檯就打印了信息,表示 set 方法被執行了,進行了自動注入。

然後我們再看 @Autowired 註解:
在這裏插入圖片描述
在這裏插入圖片描述
我們可以發現,對於 @Autowired 註解,注入的時機則在 metadata.inject(bean, beanName, pvs); 這一行代碼這裏。
執行了這一行代碼過後,屬性才被注入。

所以,又可以體現出,@Autowired 註解,和真實的自動注入,並不是同一回事。

那麼,我們可以好好研究 Spring 的自動注入:
在這裏插入圖片描述
在這裏,我們光看方法的名字,我們就可以猜想出,根據 byType 找出 bean 要填充的屬性就是在這裏。
於是,我加一個斷點,debug 一下:
在這裏插入圖片描述
可以發現,確實在這裏,把需要填充的那一個屬性找出來了。

不過,既然是 byType,那麼一定是根據類型去找的,我們可以發現,裏面的 beanName 也是 “b”;
不過,由於我提到,Spring 的自動注入是基於方法的,也就是和屬性無關,那麼,我們就可以繼續研究一下,它是怎麼推斷出這個屬性的:

於是,我把 set 方法的名字,做了一個修改,把方法名改了;
方法的參數名字也改了;
然後,繼續 debug 到剛纔的斷點。

public void setBlalala(B bbbb) {
	System.out.println("set方法被調用");
}

在這裏插入圖片描述
然後,我們可以很明確地看出,這個要 set 填充的屬性,屬性名就是方法 set 的名字,而屬性名填什麼,都是無關的;
不過,由於是 byType,所以,名字都無關緊要,我們發現,即使名字是亂七八糟的,但是,Spring 根據屬性,也已經找到了要填充的那個屬性。

然後,我們可以再嘗試一下 byName。

public void setB(B bbbb) {
	System.out.println("set方法被調用");
}

在這裏插入圖片描述
我們可以發現,即使參數的名字隨便寫,但是,只要方法名寫對,就能獲取到要自動注入的屬性。
而當方法名亂寫的話,即使參數名正確,那麼結果也是不會被找到:
在這裏插入圖片描述
我們查看一下源碼,可以發現,對於自動注入,Spring 是用了 Java 的 BeanInfo 來實現的:
在這裏插入圖片描述
那麼,對於 @Autowired 註解呢?
在這裏插入圖片描述
看到這樣的代碼,一般不用點進去看你都知道里面會做什麼。
我們可以看到,在這個 doWithLocalFields 方法裏面傳入了一個 lambda 表達式,那一定意味着,裏面的某一處一定調用了這個方法;

所以,就會在這個方法中,找到加了 @Autowired 屬性;
然後,開始執行判斷,這個 field 是不是 static 的,如果不是,就把屬性和 required 值存起來。

那麼之後,就可以根據找到的屬性來進行屬性注入了。
在這裏插入圖片描述
我們可以發現,當 @Autowired 加載屬性上的時候,就是直接 Field.set 填充屬性,
所以根本沒有用到 set 方法;

當然,由於 @Autowired 也可以加在方法上,所以當 @Autowired 加在方法上的時候,就會調用一次該方法。
所以我們也可以看到代碼裏有一個 method.invoke。

既然之前提到,在源碼中判斷的時候,有一步判斷是非 static,那麼就可以來測試一下:
我們把 a 的屬性改成 static,然後嘗試打印 a 的屬性:
在這裏插入圖片描述
我們也可以發現確實如此,Spring 也打印了一句日誌,來提醒不可以注入 static 屬性。

當然,我再這裏提到的,仍然只是一部分,由於 Spring 的源碼太過龐大,筆者也不可能把所有的地方全部看一遍,寫一遍。
所以,我在這裏提及的,也僅僅是有關方法注入,以及 @Autowired 的屬性注入。

但是,除去這些,Spring 還有構造方法自動注入,@Autowired 也可以加在構造方法上,所以這裏也涉及到很多的知識點,有關 Spring 是如何去推斷出構造方法的。

所以,對於屬性的注入,還得去繼續研究構造方法,你可以去我的這篇博客先看看,開頭的連環構造方法轟炸,你能答對幾輪。
99%的人答不對Spring推斷構造方法!打破你三觀!!!(範例→源碼)

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