1. 程式人生 > >淺談spring4泛型依賴注入

淺談spring4泛型依賴注入

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

1. 泛型依賴注入的優點

泛型依賴注入就是允許我們在使用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可以被正確的區分,並且注入到上一層。這為我們在程式碼中使用泛型提供了極大的便利。

2. 泛型依賴注入的實現

下面我們就來看看使用泛型的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> {

}


@Repository
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方法時,控制檯會打印出以下資訊:

Student [id=1, name=Anna, grade=3.9]
class com.somepackage.Student
Faculty [id=2, name=Bob, evaluation=A]
class com.somepackage.Faculty

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

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

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

@Repository
public class StudentRepository extends BaseRepository<Student> {
	@Override
	public void printString(Student s) {
		System.out.println("I am StudentRepo - " + s.toString());
	}
}

@Repository
public class FacultyRepository extends BaseRepository<Faculty> {
	@Override
	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

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

然後在進行依賴注入的時候,Spring可以通過欄位的型別來自動生成匿名子類,並進行注入:

@Autowired private BaseRepository<Student> studentRepo; //自動注入BaseRepository<Student>() {}
@Autowired private BaseRepository<Faculty> facultyRepo; //自動注入BaseRepository<Faculty>() {}
如果Spring可以做到這樣的話,我相信我們的開發會變的更加高效。

這些就是我最近學習到的Spring 4.0泛型依賴注入的全部內容,在這裡與大家分享。歡迎大家在評論裡進行討論與交流,共同學習,共同進步。