1. 程式人生 > >Spring方法注入的使用與實現原理

Spring方法注入的使用與實現原理

# 一、前言   這幾天為了更詳細地瞭解``Spring``,我開始閱讀``Spring``的官方文件。說實話,之前很少閱讀官方文件,就算是讀,也是讀別人翻譯好的。但是最近由於準備春招,需要了解很多知識點的細節,網上幾乎搜尋不到,只能硬著頭皮去讀官方文件。雖然我讀的這個``Spring``文件也是中文版的,但是很明顯是機翻,十分不通順,只能對著英文版本,兩邊對照著看,這個過程很慢,也很吃力。但是這應該是一個程式設計師必須要經歷的過程吧。   在讀文件的時候,我讀到了一個叫做方法注入的內容,這是我之前學習``Spring``所沒有了解過的。所以,這篇部落格就參照文件中的描述,來講一講這個方法注入是什麼,在什麼情況下使用,以及簡單談一談它的實現原理。
# 二、正文 ## 2.1 問題分析   在說方法注入之前,我們先來考慮一種實際情況,通過實際案例,來引出我們為什麼需要方法注入。在我們的``Spring``程式中,可以將``bean``的依賴關係簡單分為四種: 1. 單例``bean``依賴單例``bean``; 2. 多例``bean``依賴多例``bean``; 3. 多例``bean``依賴單例``bean``; 4. 單例``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``的程式碼可以是這樣: ```java 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``方法,正常情況下,它的實現應該是這樣的: ```java public B getB() { return b; } ```   但是,為了實現每次獲取``B``時,能夠讓``Spring``容器建立一個新的``B``,我們在上面的程式碼中將它修改成了下面這個樣子: ```java public B getB() { // 每次獲取B時,都向容器申請一個新的B b = context.getBean(B.class); return b; } ```   但是,我們之前也說過,這種方式並不好,因為這直接依賴於``Spring``容器,增加了耦合性。而方法注入可以幫助我們解決這一點。方法注入能幫我們完成上面的替換,而且這種替換是隱式地,由``Spring``容器自動幫我們替換。我們並不需要修改編寫程式碼的方式,仍然可以將``getB``方法寫成第一種形式,而``Spring``容器會自動幫我們替換成第二種形式。這樣就可以在不增加耦合的情況下,實現我們的目的。
## 2.3 方法注入的實現原理   那方法注入的實現原理是什麼呢?我之前說過,方法注入和``AOP``類似,不僅僅是功能類似,實際上它們的實現方式也是一樣的。**方法注入的實現原理,就是通過CGLib的動態代理**。關於``AOP``的實現原理,可以參考我的這篇部落格:[淺析Spring中AOP的實現原理——動態代理](https://www.cnblogs.com/tuyang1129/p/12878549.html)。   如果我們為一個類的方法,配置了方法注入,那麼在``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``,它們的定義如下: ```java 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``配置為多例。則配置檔案如下: ```xml
```   好,到此為止,我們就配置完成了,下面就該測試一下通過``user``的``getCar``方法拿到的多個``car``,是不是不相同。如果方法注入沒有生效,那麼按理來講,我們呼叫``getCar``方法返回的應該是``null``,因為我們並沒有配置將car的值注入user中。但是如果方法注入生效,那麼我們通過``getCar``,就可以拿到``car``物件,因為它將去``Spring``容器中獲取,而且每次獲取到的都不是同一個。測試方法如下: ```java @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``的父類,來判斷是否和我們之前說的一樣。輸出結果如下: ```txt 1392906938 708890004 255944888 class cn.tewuyiang.pojo.User // 父類果然是User ```   可以看到,我們果然能夠通過``getCar``方法,獲取到``bean``,並且每一次獲取到的都不是同一個,因為``hashcode``不相等。同時,``user``這個``bean``的父型別果然是``User``,說明``user``這個``bean``確實是``CGLib``生成的一個代理``bean``。到此,也就證明了我們之前的敘述。
**(二)通過註解配置**   上面通過``xml``的配置方式,大致瞭解了查詢方法注入的使用,下面我們再來看看使用註解,如何實現。其實使用註解的方式更加簡單,我們只需要在方法上使用``@Lookup``註解即可,``User``和``Car``的配置如下: ```java @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``類改為抽象,如下所示: ```java // 就算為抽象類使用了@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``方法配置了方法注入,那生成的代理物件中,這個方法還是原來那個,並沒有被重寫,比如像下面這樣: ```java @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``。 ```java @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``文件中瞭解。最後,若以上描述存在錯誤或不足,歡迎指正,共同進步。
# 四、參考 - [Spring-4.3.21官方文件——方法注入](https://www.docs4dev.com/docs/zh/spring-framework/4.3.21.RELEASE/reference/beans.html#beans-factory-method-in