一、前言
這幾天為了更詳細地了解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文檔中了解。最后,若以上描述存在錯誤或不足,歡迎指正,共同進步。
