天天看點

Spring 泛型依賴注入

Spring 4.0版本中更新了很多新功能,其中比較重要的一個就是對帶泛型的Bean進行依賴注入的支援。Spring4的這個改動使得代碼可以利用泛型進行進一步的精簡優化。

泛型依賴注入的優點

泛型依賴注入就是允許我們在使用spring進行依賴注入的同時,利用泛型的優點對代碼進行精簡,将可重複使用的代碼全部放到一個類之中,友善以後的維護和修改。同時在不增加代碼的情況下增加代碼的複用性。下面我們用一個例子來簡單講解一下泛型的優點:

假設我們已經定義好了兩個實體類Student和Faculty:

public class Student {
    private int id;
    private String name;
    private double grade;
    
    public Student(int id, String name, double grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }
 
    @Override
    public String toString() {
        return "Student [id=" + id + ", name=" + name + ", grade=" + grade + "]";
    }
}           

public class Faculty {
    private int id;
    private String name;
    private String evaluation;
    
    public Faculty(int id, String name, String evaluation) {
        this.id = id;
        this.name = name;
        this.evaluation = evaluation;
    }
 
    @Override
    public String toString() {
        return "Faculty [id=" + id + ", name=" + name + ", evaluation=" + evaluation + "]";
    }
}           

然後我們需要持久層Bean來調用Student和Faculty裡面的方法。在Spring支援泛型依賴注入之前,我們需要為兩個實體類分别定義一個持久層Bean,然後從資料庫擷取我們的實體類,再調用實體類中的方法。使用這種原始方法的代碼如下:

public class StudentRepository {    
    public Student getBean(String beanName) {
        //擷取對應的Student
    }
    
    public void printString(Student s) {
        System.out.println(s);
    }
}
public class FacultyRepository {    
    public Faculty getBean(String beanName) {
        //擷取對應的Faculty
    }
    
    public void printString(Faculty f) {
        System.out.println(f);
    }
}           

大家可以看到,這樣的代碼每個實體類都需要編寫一個新的持久層Bean,每一個持久層Bean中的實體類類型都是寫死的,複用性很差。更重要的是,由于每個持久層Bean中所包含的實體類不同,持久層Bean中重複的方法(如上面例子中的printString)需要在每一個持久層Bean中都實作一次,這大大增加了代碼的維護成本。

當然,有一些方法可以部分解決這個問題。比如我們可以定義一個持久層Bean的父類BaseRepository,然後在裡面編寫一個通用的pirntString方法:

public class BaseRepository {
    public void printString(Object o) {
        System.out.println(o);
    }
}           

接着,我們可以在各個持久層Bean中調用BaseRepository的方法來實作printString:

public class StudentRepository extends BaseRepository{    
    public Student getBean(String beanName) {
        //擷取對應的Student
    }
    
    public void printString(Student s) {
        super.printString(s);
    }
}           

這樣的話,printString的實作實際上隻編寫了一遍,是以我們提高了代碼的複用性。同時,當printString方法不是簡單的列印到控制台,而具有複雜的代碼和邏輯時,我們可以把代碼全部放在BaseRepository中,友善以後的修改和維護。但是,這種方法仍然要求每一個持久層Bean編寫一個printSring方法來調用父類的方法,盡管這個方法隻有簡單的一行,當類似的方法多起來之後代碼的數量還是很可觀的。

除了加入父類之外,還有一些其他的方法可以減少代碼量,提高代碼的複用性。比如我們可以在父類中加入setter方法使得業務層可以為持久層手工注入實體類的類别(如Student.class),但是并沒有非常好的解決方案。

但是當我們使用泛型時,這些問題就迎刃而解了。我們隻需要定義一個持久層Bean,BaseRepository,也就是上面例子中的父類,而不需要任何子類:

public class BaseRepository<T> {
    public T getBean(String beanName) {
        //擷取對應的t
    }
    
    public void printString(T t) {
        System.out.println(t);
    }
}           

這個持久層Bean可以包含所有我們在持久層想要複用的方法。通過泛型,我們的持久層代碼可以用在所有實體類身上,并且我們還可以通過繼承友善的添加某些實體類特有的方法。我們沒有增加額外的代碼,但是提高了代碼複用程度,同時我們把可重複使用的代碼全部集中起來,友善了以後的維護和修改。

上面所講的内容都是泛型本身的優點,和Spring 4.0的泛型依賴注入并沒有直接聯系。但是,Spring 4.0開始支援的泛型依賴注入對于我們使用泛型非常重要:在Spring 4.0之前,Spring的依賴注入功能是不能自動識别上面例子中泛型的類,而給不同的持久層Bean加以區分的。是以在Spring 4.0之前,BaseRepository<Student>和BaseRepository<Faculty>會被認為是同一類型,一般需要用名字等其他方式加以區分。但是現在,Spring會正确的識别聲明的泛型類别,并且根據泛型給持久層Bean進行分類。是以Student和Faculty的持久層Bean可以被正确的區分,并且注入到上一層。這為我們在代碼中使用泛型提供了極大的便利。

 泛型依賴注入的實作

下面我們就來看看使用泛型的Bean的依賴注入應該如何實作:

使用泛型Bean的依賴注入與普通Bean的依賴注入在實作方法上基本相同,同樣可以通過xml配置檔案和注解兩種方式進行依賴注入。但是由于泛型中尖括号(“<>”)的存在,使得xml配置檔案依賴注入過程中會出現編譯報錯的情況。有的編譯器即使對尖括号進行轉義也依然會報錯。是以,為了避免不必要的麻煩,建議大家使用注解的方式進行帶泛型Bean的依賴注入。

使用注解進行依賴注入有如下幾種方式:

2.1 使用注解@Configuration進行依賴注入

與普通Bean一樣,我們可以利用注解@Configuration來聲明我們需要的使用泛型的Bean,并且進行依賴注入。

首先我們需要建立一個類,作為我們的配置類:

@Configuration
public class MyConfiguration {
 
}           

其中,注解@Configuration的作用是告訴spring這個類是一個配置類,這樣Spring就會自動掃描這個類中聲明的所有Bean,并把它們加入Spring容器中。不過在此之前,我們需要在spring的配置檔案中添加component-scan:

<context:component-scan base-package="com.somepackage.**" ></context:component-scan>

之後注解@configuration才會被掃描到,裡面聲明的Bean才會被添加進Spring容器中

在這之後,我們就可以使用注解對帶泛型的Bean進行依賴注入了。

首先,我們需要聲明兩個需要用到的持久層Bean,一個是Student的持久層Bean,另外一個是Faculty的。隻有在聲明了這兩個Bean并且添加到Spring容器中後,Spring才能為我們進行依賴注入。

在配置類中聲明這兩個Bean的方法如下:

@Configuration
public class MyConfiguration {
    @Bean
    public BaseRepository<Student> studentRepository() {
        return new BaseRepository<Student>() {};
    }
    
    @Bean
    public BaseRepository<Faculty> facultyRepository() {
        return new BaseRepository<Faculty>() {};
    }
}           

其中,注解@Bean與Spring的正常使用方法相同,就是聲明一個新的Bean。Spring在掃描配置類時,就會把這裡聲明的Bean加入到Spring容器中,供以後使用。這裡每個Bean的名稱就是方法名,如studentRepository,而Bean的類型就是傳回的Object的類型(不是方法的傳回類型,方法的傳回類型可以是Interface等不能執行個體化的類型)。

如果你還需要聲明其他的Bean,比如你不需要從資料庫擷取資料,也可以把它們加入到這個配置類中。

然後我們就可以定義我們的業務層Bean,并且用業務層Bean調用持久層的方法來對資料進行操作。我們這裡使用printString方法作為例子:

@Service
public class ExampleService {
    @Autowired private BaseRepository<Student> studentRepo; //自動注入BaseRepository<Student>() {}
    @Autowired private BaseRepository<Faculty> facultyRepo; //自動注入BaseRepository<Faculty>() {}
    
    public void test() {
        Student s = studentRepo.getBean("studentBean");
        studentRepo.printString(s);
        
        Faculty f = facultyRepo.getBean("facultyBean");
        facultyRepo.printString(f);
    }
}           

在業務層中,我們可以使用注解@Autowired進行依賴注入。@Autowired預設按照字段的類進行依賴注入,而Spring4的新特性就是把泛型的具體類型(如上文業務層中BaseRepository<Student>中的Student)也作為類的一種分類方法(Qualifier)。這樣我們的studentRepo和facultyRepo雖然是同一個類BaseRepository,但是因為泛型的具體類型不同,也會被區分開。

這裡我先建立了兩個實體類執行個體,并且加入到了剛才提到的配置類中。這樣這兩個Bean就會被加入到Spring容器之中,而我們可以在getBean方法當中擷取他們。這兩個Bean的名字分别是studentBean和facultyBean,與業務層Bean中填寫的名字保持一緻:

@Bean
public Student studentBean() {
    return new Student(1, "Anna", 3.9);
}
 
@Bean
public Faculty facultyBean() {
    return new Faculty(2, "Bob", "A");
}           

當然,如果你有其他方法能夠擷取到實體類,比如你的工程整合了Hibernate ORM或者其他工具來連接配接資料庫,就不需要向Spring容器中加入對應的Bean了,getBean方法的實作也可以相應的改變。我這裡隻是用這兩個實體類的Bean作為例子。

然後當我們調用業務層的test方法時,控制台列印的結果是:

Student [id=1, name=Anna, grade=3.9]

Faculty [id=2, name=Bob, evaluation=A]

而當我們在業務層裡試圖錯誤的調用方法:

facultyRepo.printString(s);

的時候,會出現編譯錯誤。

讀到這裡,可能有的人已經發現了,這個例子存在兩個疑點。第一,這個例子不能證明我們在運作期成功實作了依賴注入,因為我們在運作期為printString方法傳入了Student和Faculty的執行個體。第二,我們在聲明Bean的時候,聲明的類不是BaseRepository,而是BaseRepository的一個匿名子類。

為了解答這兩個問題,我在BaseRepository中定義了一個新的方法:

public void printType() {
    Type genericSuperclass = this.getClass().getGenericSuperclass();
    Class<T> entityClass = (Class<T>) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
    System.out.println(entityClass);
}           

這段代碼前兩行的作用是在帶泛型的類中,在運作期确定泛型T的類。如果需要在BaseRepository中使用到T在運作期的具體類型,就應該使用這個方法來擷取。下面是一個比較詳細的解釋:

由于java的擦除機制,泛型T隻在編譯期有效,在運作期會被擦除,是以我們在運作期不能直接獲得T的類,一些對一般類有效的方法,比如T.class和t.getClass()對泛型都是非法的。是以我們需要通過反射機制擷取T的類。

這段代碼第一行的作用是擷取目前類的父類,然後在第二行中通過檢視父類的類參數獲得目前類泛型的類。也就是說,這是一個通過父類的參數來檢視子類中泛型的具體類型的方法。是以,這個方法有一些使用上的要求:首先,這個方法必須被帶泛型類的子類所使用,帶泛型類本身是不能使用這個方法的。另外,這個子類在泛型的位置必須繼承一個具體的類,而不是泛型T。舉個例子,當有一個類繼承BaseBaen<A>時,這個方法就可以使用,而繼承BaseBean<T>的時候就不能使用,因為A是具體的類,而T是泛型,在運作期就算我們從父類中取到了T,因為有擦除機制,我們仍然無法得知T是一個什麼類。

值得一提的是,Spring4也是通過同樣的方法添加了對泛型依賴注入的支援。是以我們如果想使用Spring4的新功能,在定義Bean的時候就必須定義為泛型類的子類,如上面例子中的new BaseBean<A>() {}。這個Bean是BaseBean的一個匿名子類,繼承的是BaseBean<A>。這樣的話Spring就可以正确擷取到泛型T的類(A),并且以此為根據幫助我們實行依賴注入。

Spring的文檔和源代碼裡都有關于注解依賴注入的說明,大家有興趣的話可以去看一下。

與此同時,我們會發現上面的printType方法是不接收任何執行個體的,是以這個方法可以幫我們判斷泛型的依賴注入是否成功。為了測試,我對業務層的test方法進行了如下修改:

public void test() {
    Student s = studentRepo.getBean("studentBean");
    //studentRepo.printString(s);
    studentRepo.printType();
        
    Faculty f = facultyRepo.getBean("facultyBean");
    //facultyRepo.printString(f);
    facultyRepo.printType();
}           

然後當我們調用test方法進行測試時,控制台會列印以下資訊:

class com.somepackage.Student

class com.somepackage.Faculty

這些資訊說明我們在沒有傳入執行個體的情況下也正确擷取到了泛型T的類,泛型的依賴注入成功了。

注:

1. 前文提到的@Bean注解聲明Bean的方法也可以使用在@Component注解标注的類當中,但是Spring建議這種做法隻在工廠類中使用,并不建議大規模使用。另外,Spring對不在@Component注解标注的配置類中聲明的Bean的關聯上有一些限制,詳細的情況請參照Spring文檔。關于注解@Component的正确使用方法,請看下一小節。

2. 除了使用@Autowired注解進行依賴注入外,我們還可以使用@Resource注解進行依賴注入。因為@Resource是優先根據名字進行依賴注入,我們最好讓字段的名字與Bean名字相同。

2.2 使用注解@Component等進行依賴注入

上一小節我們講述了利用@Configuration注解标注的配置類進行泛型依賴注入的實作方法和部分原理。其中我們提到,如果想要Spring的泛型依賴注入成功,我們必須把Bean定義為使用泛型的類的子類。而定義一個子類最常見的方法是定義一個新的類,然後進行繼承。

是以,我們可以使用@Component注解以及它的子注解(如@Controller,@Service和@Repository)來聲明一個新的Bean。如上一小節所說,這個子類在泛型的位置必須繼承一個具體的類型,而不能繼承泛型T,否則Spring的自動依賴注入不會成功。

除此之外,這些注解的使用方法都與沒有泛型時完全相同,下面我們就來看一下具體的代碼:

我為前面的兩種BaseRepository編寫了兩個子類StudentRepository和FacultyRepository:

@Repository

public class StudentRepository extends BaseRepository<Student> {

}

public class FacultyRepository extends BaseRepository<Faculty> {

并且使用注解@Repository進行标注。這樣的話,Spring在掃描時将會掃描到這兩個類,并建立兩個對應的Bean加入到Spring容器中。當然,如果你想要正确使用Spring的自動掃描功能,需要在Spring配置檔案中加入component-scan,詳細的做法請參考上一小節。

需要注意的是,使用了@Repository注解就已經往Spring容器中加入了一個Bean。是以,如果你在上一小節編寫了@Configuration配置類,請務必把@Configuration注解注釋掉,讓Spring不再掃描這個配置類,或者把配置類中兩個持久層Bean的@Bean注解注釋掉,讓Spring不再掃描這兩個持久層。否則Spring在使用@Autowired注解進行依賴注入時會因為同一類型的Bean有兩個而報錯。

下面是我們的業務層代碼:

public class ExampleService {
    @Autowired private BaseRepository<Student> studentRepo; //自動注入BaseRepository<Student>() {}
    @Autowired private BaseRepository<Faculty> facultyRepo; //自動注入BaseRepository<Faculty>() {}
    
    public void test() {
        Student s = studentRepo.getBean("studentBean");
        studentRepo.printString(s);
        studentRepo.printType();
        
        Faculty f = facultyRepo.getBean("facultyBean");
        facultyRepo.printString(f);
        facultyRepo.printType();
    }
}           

業務層的代碼與上一小節相同,沒有做任何修改。注意在需要依賴注入的兩個字段中,我們聲明的類型仍然是使用泛型的類BaseRepository,而不是我們剛才定義的子類StudentRepository和FacultyRepository。實際上,我們根本不需要知道這些子類的類型,就可以調用子類的方法,這正是Spring依賴注入的強大之處。

當我們調用test方法時,控制台會列印出以下資訊:

從這些資訊我們可以看到,Spring在聲明Bean的類型與依賴注入目标類型不同的情況下也可以成功注入。這是因為Spring4開始将泛型的具體類型作為Bean分類的一種方法(Qualifier),是以Spring能夠成功區分BaseRepository<Student>和BaseRepository<Faculty>,以及他們的子類。

但是,這個例子也存在一個問題:因為依賴注入的地方聲明的是父類BaseRepository,我們如何判定Spring為我們注入的是子類StudentRepository和FacultyRepository,還是父類BaseRepository呢?實際上我們根本不用擔心這個問題,因為我們根本沒有聲明任何父類BaseRepository類型的Bean,隻聲明了子類類型的Bean。是以如果Spring依賴注入成功了,就一定注入的是子類類型的Bean。但是在這裡,我們也通過代碼驗證一下我們的這個猜想。

為了進行驗證,我在StudentRepository和FacultyRepository中覆寫了父類BaseRepository的printString方法:

    @Override

    public void printString(Student s) {

        System.out.println("I am StudentRepo - " + s.toString());

    }

    public void printString(Faculty f) {

        System.out.println("I am FacultyRepo - " + f.toString());

然後當我們調用業務層的test方法進行測試時,控制台打出了如下資訊:

I am StudentRepo - Student [id=1, name=Anna, grade=3.9]

class com.hpe.bboss.autotest.dao.Student

I am FacultyRepo - Faculty [id=2, name=Bob, evaluation=A]

class com.hpe.bboss.autotest.dao.Faculty

這說明Spring為我們注入的是我們所希望的子類StudentRepository和FacultyRepository,而不是父類BaseRepository。

1. 當@Component注解标注的多個子類同時繼承一個父類,并且泛型的具體類型也相同時,按照以上方法進行依賴注入會抛出異常。這是因為@Autowired注解預設隻有一個Bean與指定字段的類型相同,當擁有多個Bean滿足條件的時候,就會抛出異常。這個問題的解決辦法有使用@Primary注解,使用@Qualifier注解和它的子注解,使用Bean名字注入等。由于這個問題是Spring依賴注入的問題,而不是泛型依賴注入獨有的,是以不再贅述,請大家查閱Spring文檔和其他資料來獲得具體解決辦法。

2.    泛型賴注入并不僅限于在持久層使用。我們也可以在持久層使用泛型依賴注入的基礎上,在業務層等其他地方也使用泛型依賴注入。相關的例子在網上很好找到,我就不複制粘貼了,有興趣的話請自行查閱。

2.3   兩種依賴注入方式的比較

前文所講的兩種依賴注入方式,本質上是兩種不同的聲明Bean的方式。如前文所說,Spring對這兩種聲明方式都擁有很好的支援,但是這兩種聲明方式本身還是擁有比較大的差異。第一種方式中,我們通過@Configuration注解标注配置類來進行聲明。第二種方式中,我們通過注解直接在子類進行聲明。下面我就來簡單探讨一下兩種方式的優劣。

第一種聲明方式中,所有的Bean都會在配置類中進行聲明。是以在後續進行維護時,我們不需要檢視每個類的源代碼就可以對Bean的狀态進行一些修改。另外,這種方式也意味着我們不需要為每一個Bean都建立一個子類,使得目錄的管理變得簡單。但是使用這種方法意味着我們每聲明一個新的Bean就需要對配置類添加一個方法和至少一個注解,并且有時還需要向匿名子類中添加一些方法,在Bean數量很多時配置類的長度會變得很長,不便于了解和管理。

而第二種聲明方式中,我們根本不需要維護配置檔案,所有聲明Bean所需要的工作,例如名字,類,加入Spring容器等,都由一個注解完成。于此同時,由于子類的存在,我們可以很友善的進行添加方法和字段,覆寫方法等工作。但是使用這種方法也意味着當我們需要對Bean的狀态進行修改時,我們必須找到相應的類才能進行操作。而且大量的子類會讓我們的目錄更加繁雜,尤其是空子類,本身沒有太大意義,卻讓目錄的管理變得很麻煩。

綜上所述,兩種方式各有各的優缺點,而使用哪種方法應該根據項目的具體情況而定。一般來說,當子類中空類較多時,可能使用第一種方法比較合适,反之第二種方法比較合适。在一些難以決定的情況下,兩種方法同時使用有時也是一種可以考慮的選擇。但是兩種方法同時使用會提高維護的難度,建議謹慎使用。

3. 泛型依賴注入總結與展望

Spring 4.0版本新加入的泛型依賴注入功能是一個很實用的功能。它幫助我們利用泛型極大的精簡了代碼,降低了維護成本。根據我這次的學習和使用來看,Spring對泛型依賴注入的支援總體品質還是很不錯的。泛型依賴注入的實作與普通依賴注入差别并不大,學習起來簡單易懂,使用上也沒有什麼難度。希望看到這篇文章的大家在以後使用Spring的時候也試着試用一下泛型依賴注入。

不過,Spring4的泛型依賴注入也有一些可以改進的地方。我這次研究Spring泛型注入的初衷就是找到一種簡單的注入方法,可以讓我在使用Spring依賴注入的同時,盡可能的減少聲明的類的數量。但是經過我這段時間的學習,我發現Spring目前必須喂每一個Bean聲明一個新的類,無論是匿名子類還是空子類,否則Spring就不能正确進行依賴注入。但是當我們不需要往子類裡添加任何功能時,匿名子類或者空子類過多,這個配置類就變得很低效,無論是聲明還是維護管理都非常麻煩。我希望以後的Spring更新時,能夠自動為我們建立這些匿名子類,或者通過一些别的方式,讓我們既不需要配置類又不需要子類就可以成功的聲明一些使用泛型的Bean,并且根據泛型的類型進行依賴注入。比如我希望這樣聲明Bean