一、前言
這幾天為了更詳細地瞭解Spring
,我開始閱讀Spring
的官方文件。說實話,之前很少閱讀官方文件,就算是讀,也是讀別人翻譯好的。但是最近由於準備春招,需要了解很多知識點的細節,網上幾乎搜尋不到,只能硬著頭皮去讀官方文件。雖然我讀的這個Spring
文件也是中文版的,但是很明顯是機翻,十分不通順,只能對著英文版本,兩邊對照著看,這個過程很慢,也很吃力。但是這應該是一個程式設計師必須要經歷的過程吧。
在讀文件的時候,我讀到了一個叫做方法注入的內容,這是我之前學習Spring
所沒有了解過的。所以,這篇部落格就參照文件中的描述,來講一講這個方法注入是什麼,在什麼情況下使用,以及簡單談一談它的實現原理。
二、正文
2.1 問題分析
在說方法注入之前,我們先來考慮一種實際情況,通過實際案例,來引出我們為什麼需要方法注入。在我們的Spring
程式中,可以將bean
的依賴關係簡單分為四種:
- 單例
bean
依賴單例bean
; - 多例
bean
依賴多例bean
; - 多例
bean
依賴單例bean
; - 單例
bean
依賴多例bean
;
前三種依賴關係都很好解決,Spring
容器會幫我們正確地處理,唯獨第四種——單例bean
依賴多例bean
,Spring
容器無法幫我們得到想要的結果。為什麼這麼說呢?我們可以通過Spring
容器工作的方式來分析。
我們知道,Spring
中bean
的作用域預設是單例的,每一個Spring
容器,只會建立這個型別的一個例項物件,並快取在容器中,所以對這個bean
的請求,拿到的都是同一個bean
例項。而對於每一個bean
來說,容器只會為它進行一次依賴注入,那就是在建立這個bean
,為它初始化的時候。於是我們可以開始考慮上面說的第四種依賴情況了。假設一個單例bean A
,它依賴於多例bean B
,Spring
容器在建立A
的時候,發現它依賴於B
,且B
是多例的,於是容器會建立一個新的B
,然後將它注入到A
中。A
建立完成後,由於它是單例的,所以會被快取在容器中。之後,所有訪問A
的程式碼,拿到的都是同一個A
物件。而且,由於容器只會為bean
執行一次依賴注入,所以我們通過A
訪問到的B
,永遠都是同一個,儘管B
被配置為了多例,但是並沒有用。為什麼會這樣?因為多例的含義是,我們每次向Spring
容器請求多例bean
,都會建立一個新的物件返回。而B
雖然是多例,但是我們是通過A
訪問B
,並不是通過容器訪問,所以拿到的永遠是同一個B
。這時候,單例bean
依賴多例bean
就失敗了。
那要如何解決這個問題呢?解決方案應該不難想到。我們可以放棄讓Spring
容器為我們注入B
,而是編寫一個方法,這個方法直接向Spring
容器請求B
;然後在A
中,每次想要獲取B
時,就呼叫這個方法獲取,這樣每次獲取到的B
就是不一樣的了。而且我們這裡可以藉助ApplicationContextAware
介面,將context
物件(也就是容器)儲存在A
中,這樣就可以方便地呼叫getBean
獲取B
了。比如,A
的程式碼可以是這樣:
class A implements ApplicationContextAware {
// 記錄容器的引用
private ApplicationContext context;
// A依賴的多例物件B
private B b;
/**
* 這是一個回撥方法,會在bean建立時被呼叫
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.context = applicationContext;
}
public B getB() {
// 每次獲取B時,都向容器申請一個新的B
b = context.getBean(B.class);
return b;
}
}
但是,上面的做法真的好嗎?答案顯然是不好。Spring
的一個很大的優點就是,它侵入性很低,我們在自己編寫的程式碼中,幾乎看不到Spring
的元件,一般只會有一些註解。但是上面的程式碼中,卻直接耦合了Spring
容器,將容器儲存在類中,並顯式地呼叫了容器的方法,這不僅增加了Spring
的侵入性,也讓我們的程式碼變得不那麼容易管理,也變得不再優雅。而Spring
提供的方法注入機制,就是用了實現和上面類似的功能,但是更加地優雅,侵入性更低。下面我們就來看一看。
2.2 方法注入的功能
什麼是方法注入?其實方法注入和AOP
非常類似,AOP
用來對我們定義的方法進行增強,而方法注入,則是用來覆蓋我們定義的方法。通過Spring
提供的方法注入機制,我們可以對類中定義的方法進行替換,比如說上面的getB
方法,正常情況下,它的實現應該是這樣的:
public B getB() {
return b;
}
但是,為了實現每次獲取B
時,能夠讓Spring
容器建立一個新的B
,我們在上面的程式碼中將它修改成了下面這個樣子:
public B getB() {
// 每次獲取B時,都向容器申請一個新的B
b = context.getBean(B.class);
return b;
}
但是,我們之前也說過,這種方式並不好,因為這直接依賴於Spring
容器,增加了耦合性。而方法注入可以幫助我們解決這一點。方法注入能幫我們完成上面的替換,而且這種替換是隱式地,由Spring
容器自動幫我們替換。我們並不需要修改編寫程式碼的方式,仍然可以將getB
方法寫成第一種形式,而Spring
容器會自動幫我們替換成第二種形式。這樣就可以在不增加耦合的情況下,實現我們的目的。
2.3 方法注入的實現原理
那方法注入的實現原理是什麼呢?我之前說過,方法注入和AOP
類似,不僅僅是功能類似,實際上它們的實現方式也是一樣的。方法注入的實現原理,就是通過CGLib的動態代理。關於AOP
的實現原理,可以參考我的這篇部落格:淺析Spring中AOP的實現原理——動態代理。
如果我們為一個類的方法,配置了方法注入,那麼在Spring
容器建立這個類的物件時,實際上建立的是一個代理物件。Spring
會使用CGLib
操作這個類的位元組碼,生成類的一個子類,然後覆蓋需要修改的那個方法,而在建立物件時,建立的就是這個子類(代理類)的物件。而具體覆蓋成什麼樣子,取決於我們的配置。比如說Spring
提供了一個具體的方法注入機制——查詢方法注入,這種方法注入,可以將方法替換為一個查詢方法,它的功能就是去Spring
容器中獲取一個特定的Bean
,而獲取哪一個bean
,取決於方法的返回值以及我們指定的bean
名稱。
比如說,上面的getB
方法,如果我們對它使用了查詢方法注入,那麼Spring
容器會使用CGLib
生成A
類的一個子類(代理類),覆蓋A
類的getB
方法,由於getB
方法的返回值是B
型別,於是這個方法的功能就變成了去Spring
容器中獲取一個B
,當然,我們也可以通過bean
的名稱,指定這個方法查詢的bean
。下面我就通過實際程式碼,來演示查詢方法注入。
2.4 查詢方法注入的使用
(一)通過xml配置
為了演示查詢方法注入,我們需要幾個具體的類,假設我們有兩個類User
和Car
,而User
依賴於Car
,它們的定義如下:
public class User {
private String name;
private int age;
// 依賴於car
private Car car;
// 為這個方法進行注入
public Car getCar() {
return car;
}
// 省略其他setter和getter,以及toString方法
}
public class Car {
private int speed;
private double price;
// 省略setter和getter,以及toString方法
}
好,現在有了這兩個類,我們可以開始進行方法注入了。我們模擬之前說過的依賴關係——單例bean
依賴於多例bean
,將User
配置為單例,而將User
依賴的Car
配置為多例。則配置檔案如下:
<!-- 將user的作用域定義為singleton -->
<bean id="user" class="cn.tewuyiang.pojo.User" scope="singleton">
<property name="name" value="aaa" />
<property name="age" value="28" />
<!--
配置查詢方法注入,替換getCar方法,讓他成為從spring容器中查詢car的一個工廠方法
name指定了需要進行方法注入的方法,而bean則指定了這個方法被覆蓋後,是用來查詢哪個bean的
-->
<lookup-method name="getCar" bean="car" />
</bean>
<!-- 將car的作用域定義為prototype -->
<bean id="car" class="cn.tewuyiang.pojo.Car" scope="prototype">
<property name="price" value="9999.35" />
<property name="speed" value="100" />
</bean>
好,到此為止,我們就配置完成了,下面就該測試一下通過user
的getCar
方法拿到的多個car
,是不是不相同。如果方法注入沒有生效,那麼按理來講,我們呼叫getCar
方法返回的應該是null
,因為我們並沒有配置將car的值注入user中。但是如果方法注入生效,那麼我們通過getCar
,就可以拿到car
物件,因為它將去Spring
容器中獲取,而且每次獲取到的都不是同一個。測試方法如下:
@Test
public void testXML() throws InterruptedException {
// 建立Spring容器
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
// 獲取User物件
User user = context.getBean(User.class);
// 多次呼叫getCar方法,獲取多個car
Car c1 = user.getCar();
Car c2 = user.getCar();
Car c3 = user.getCar();
// 分別輸出car的hash值,看是否相等,以此判斷是否是同一個物件
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
// 輸出user這個bean所屬型別的父類
System.out.println(user.getClass().getSuperclass());
}
上面的測試邏輯應該很好理解,除了最後一句,為什麼需要輸出user
這個bean
所屬型別的父類。因為我前面說過,方法注入通過CGLib
動態代理實現,而CGLib
動態代理的原理就是生成類的一個子類。我們為User
類使用了方法注入,所以我們拿到的user
這個bean
,應該是一個代理bean
,並且它的型別是User
的子類。所以我們輸出這個bean
的父類,來判斷是否和我們之前說的一樣。輸出結果如下:
1392906938
708890004
255944888
class cn.tewuyiang.pojo.User // 父類果然是User
可以看到,我們果然能夠通過getCar
方法,獲取到bean
,並且每一次獲取到的都不是同一個,因為hashcode
不相等。同時,user
這個bean
的父型別果然是User
,說明user
這個bean
確實是CGLib
生成的一個代理bean
。到此,也就證明了我們之前的敘述。
(二)通過註解配置
上面通過xml
的配置方式,大致瞭解了查詢方法注入的使用,下面我們再來看看使用註解,如何實現。其實使用註解的方式更加簡單,我們只需要在方法上使用@Lookup
註解即可,User
和Car
的配置如下:
@Component
public class User {
private String name;
private int age;
private Car car;
// 使用Lookup註解,告訴Spring這個方法需要使用查詢方法注入
// 這裡直接使用@Lookup,則Spring將會依據方法返回值
// 將它覆蓋為一個在Spring容器中獲取Car這個型別的bean的方法
// 但是也可以指定需要獲取的bean的名字,如:@Lookup("car")
// 此時,名字為car的bean,型別必須與方法的返回值型別一致
@Lookup
public Car getCar() {
return car;
}
// 省略其他setter和getter,以及toString方法
}
@Component
@Scope("prototype") // 宣告為多例
public class Car {
private int speed;
private double price;
// 省略setter和getter,以及toString方法
}
可以看到,通過註解配置方法注入要簡單的多,只需要通過一個@Lookup
註解即可實現。測試方法與之前類似,結果也一樣,我就不貼出來了。
(三)為抽象方法使用方法注入
實際上,方法注入還可以應用於抽象方法。既然方法注入的目的是替換原來的方法,那麼原來的方法是否有實現,也就不重要了。所以方法注入也能用在抽象方法上面。但是有人可能會想一個問題:抽象方法只能在抽象類中,那這個類被定義為抽象類了,Spring
容器如何為它建立物件呢?我們之前說過,使用了方法注入的類,Spring
會使用CGLib
生成它的一個代理類(子類),Spring
建立的是這個代理類的物件,而不會去建立源類的物件,所以它是不是抽象的並不影響工作。如果配置了方法注入的類是一個抽象類,則方法注入機制的實現,就是去實現它的抽象方法。我們將User
類改為抽象,如下所示:
// 就算為抽象類使用了@Component,Spring容器在建立bean時也會跳過它
@Component
public abstract class User {
private String name;
private int age;
private Car car;
// 將getCar宣告為抽象方法,它將會被代理類實現
@Lookup
public abstract Car getCar();
// 省略其他setter和getter,以及toString方法
}
以上方式,方法注入仍然可以工作。
(四)final方法和private方法無法使用方法注入
CGLib
實現動態代理的方法是建立一個子類,然後重寫父類的方法,從而實現代理。但是我們知道,final
方法和private
方法是無法被子類重寫的。這也就意味著,如果我們為一個final
方法或者一個private
方法配置了方法注入,那生成的代理物件中,這個方法還是原來那個,並沒有被重寫,比如像下面這樣:
@Component
public class User {
private String name;
private int age;
private Car car;
// 方法宣告為final,無法被覆蓋,代理類中的getCar還是和下面一樣
@Lookup
public final Car getCar() {
return car;
}
// 省略其他setter和getter,以及toString方法
}
我們依舊使用下面的測試方法,但是,在呼叫c1.hashCode
方法時,丟擲了空指標異常。說明getCar
方法並沒有被覆蓋,還是直接返回了car
這個成員變數。但是由於我們並沒有為user
注入car
,所以car == null
。
@Test
public void testConfig() throws InterruptedException {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AutoConfig.class);
User user = context.getBean(User.class);
Car c1 = user.getCar();
Car c2 = user.getCar();
Car c3 = user.getCar();
// 執行到這裡,丟擲空指標異常
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
user.spCar();
user.spCar();
user.spCar();
System.out.println(user.getClass().getSuperclass());
}
三、總結
以上大致介紹了一下方法注入的作用,實現原理,以及重點介紹了一下查詢方法注入的使用。查詢方法注入可以將我們的一個方法,覆蓋成為一個去Spring
容器中查詢特定bean
的方法,從而解決單例bean
無法依賴多例bean
的問題。其實,方法注入能夠注入任何方法,而不僅僅是查詢方法,但是由於任何方法注入使用的不多,所以這篇部落格就不提了,感興趣的可以自己去Spring
文件中瞭解。最後,若以上描述存在錯誤或不足,歡迎指正,共同進步。