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


一、前言

  這幾天為了更詳細地了解Spring,我開始閱讀Spring的官方文檔。說實話,之前很少閱讀官方文檔,就算是讀,也是讀別人翻譯好的。但是最近由於准備春招,需要了解很多知識點的細節,網上幾乎搜索不到,只能硬着頭皮去讀官方文檔。雖然我讀的這個Spring文檔也是中文版的,但是很明顯是機翻,十分不通順,只能對着英文版本,兩邊對照着看,這個過程很慢,也很吃力。但是這應該是一個程序員必須要經歷的過程吧。

  在讀文檔的時候,我讀到了一個叫做方法注入的內容,這是我之前學習Spring所沒有了解過的。所以,這篇博客就參照文檔中的描述,來講一講這個方法注入是什么,在什么情況下使用,以及簡單談一談它的實現原理。


二、正文

2.1 問題分析

  在說方法注入之前,我們先來考慮一種實際情況,通過實際案例,來引出我們為什么需要方法注入。在我們的Spring程序中,可以將bean的依賴關系簡單分為四種:

  1. 單例bean依賴單例bean
  2. 多例bean依賴多例bean
  3. 多例bean依賴單例bean
  4. 單例bean依賴多例bean

  前三種依賴關系都很好解決,Spring容器會幫我們正確地處理,唯獨第四種——單例bean依賴多例beanSpring容器無法幫我們得到想要的結果。為什么這么說呢?我們可以通過Spring容器工作的方式來分析。

  我們知道,Springbean的作用域默認是單例的,每一個Spring容器,只會創建這個類型的一個實例對象,並緩存在容器中,所以對這個bean的請求,拿到的都是同一個bean實例。而對於每一個bean來說,容器只會為它進行一次依賴注入,那就是在創建這個bean,為它初始化的時候。於是我們可以開始考慮上面說的第四種依賴情況了。假設一個單例bean A,它依賴於多例bean BSpring容器在創建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配置

  為了演示查找方法注入,我們需要幾個具體的類,假設我們有兩個類UserCar,而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>

  好,到此為止,我們就配置完成了,下面就該測試一下通過usergetCar方法拿到的多個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注解即可,UserCar的配置如下:

@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文檔中了解。最后,若以上描述存在錯誤或不足,歡迎指正,共同進步。


四、參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM