系統學習Spring(二)——裝配Bean
任何一個成功的應用都是由多個為了實現某個業務目標而相互協作的元件構成的,這些元件必須相互瞭解、能夠相互協作完成工作。
例如,在一個線上購物系統中,訂單管理元件需要與產品管理元件以及信用卡認證元件協作;這些元件還需要跟資料庫元件協作從而進行資料庫讀寫操作。
在Spring應用中,物件無需自己負責查詢或者建立與其關聯的其他物件,由容器負責將建立各個物件,並建立各個物件之間的依賴關係。
通俗的來說,Spring就是一個工廠,Bean就是Spring工廠的產品,對於Spring工廠能夠生產那些產品,這個取決於領導的決策,也就是配置檔案中配置。
因此,對於開發者來說,我們需要關注的只是告訴Spring容器需要建立哪些bean以及如何將各個bean裝配到一起
Bean的定義
在定義Bean的時候,通常要指定兩個屬性:id和class。其中id用來指明bean的識別符號,這個識別符號具有唯一性,Spring對bean的管理以及bean之間這種依賴關係都需要這個屬性;而class指明該bean的具體實現類,這裡不能是介面(可以是介面實現類)全路徑包名.類名。
//一個Bean的配置
<bean id="bean" class="實現類" />
或者
@Component("bean")
public class Bean {
...
}
當我們用XML配置了這個bean的時候,該bean實現類中必須有一個無參構造器,故Spring底層相當於呼叫瞭如下程式碼:
bean = new 實現類();
如果在bean的配置檔案中,通過構造注入如:
<bean id="bean" class="實現類" />
<constructor-arg value="bean"/>
</bean>
那麼Spring相當於呼叫了
Bean bean = new 實現類("bean");
Spring的配置方法
Spring容器負責建立應用中的bean,並通過DI維護這些bean之間的協作關係。作為開發人員,你應該負責告訴Spring容器需要建立哪些bean以及如何將各個bean裝配到一起。Spring提供三種裝配bean的方式:
絕大多數情況下,開發人員可以根據個人品味選擇這三種裝配方式中的一種。Spring也支援在同一個專案中混合使用不同的裝配方式。
《Spring實戰》的建議是:儘可能使用自動裝配,越少寫顯式的配置檔案越好;當你必須使用顯式配置時(例如,你要配置一個bean,但是該bean的原始碼不是由你維護),儘可能使用型別安全、功能更強大的基於Java檔案的裝配方式;最後,在某些情況下只有XML檔案中才又你需要使用的名字空間時,再選擇使用基於XML檔案的裝配方式。
自動裝配Bean
Spring通過兩個角度來實現自動裝配:
《Spring實戰》中用了一個例子來說明,假設你需要實現一個音響系統,該系統中包含CDPlayer和CompactDisc兩個元件,Spring將自動發現這兩個bean,並將CompactDisc的引用注入到CDPlayer中。
首先建立CD的概念——CompactDisc介面,如下所示:
package soundsystem;
/**
* @author 李智
* @date 2017/5/9
*/
public interface CompactDisc {
void play();
}
CompactDisc介面的作用是將CDPlayer與具體的CD實現解耦合,即面向介面程式設計。這裡還需定義一個具體的CD實現,如下所示:
package soundsystem;
import org.springframework.stereotype.Component;
/**
* @author 李智
* @date 2017/5/9
*/
@Component
public class SgtPeppers implements CompactDisc {
private String title = "Sgt.Pepper's Lonely Hearts Club Band";
private String artist = "The Beatles";
public void play() {
System.out.println("Playing" + title + "by" + artist);
}
}
這裡最重要的是@Component註解,它告訴Spring需要建立SgtPeppers bean。除此之外,還需要啟動自動掃描機制,有兩種方法:基於XML配置檔案;基於Java配置檔案,程式碼如下(二選一):
//這是XML配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="soundsystem"/>
</beans>
或
//這是Java配置
package soundsystem;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author 李智
* @date 2017/5/9
*/
@Configuration
@ComponentScan()
public class CDPlayerConfig {
}
在這個Java配置檔案中有兩個註解值得注意:@Configuration表示這個.java檔案是一個配置檔案;@ComponentScan表示開啟Component掃描,Spring將會設定該目錄以及子目錄下所有被@Component註解修飾的類。
自動配置的另一個關鍵註解是@Autowired,基於之前的兩個類和一個Java配置檔案,可以寫個測試
package com.spring.sample.soundsystem;
import com.spring.sample.config.SoundSystemConfig;
import org.junit.Assert;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author 李智
* @date 2017/5/9
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SoundSystemConfig.class)
public class SoundSystemTest {
@Autowired
private CompactDisc cd;
@Test
public void cdShouldNotBeNull() {
Assert.assertNotNull(cd);
}
}
執行測試,看到綠色就成功了,說明@Autowired註解起作用了:自動將掃描機制建立的CompactDisc型別的bean注入到SoundSystemTest這個bean中。
這裡需要注意兩個點,一個是junit需要用高階一點的版本,之前用3.8一直有問題,換成4.12之後就好了;還一個是SpringTest的測試包。
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- Sprint-test 相關測試包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.2.11.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
簡單得說,自動裝配的意思就是讓Spring從應用上下文中找到對應的bean的引用,並將它們注入到指定的bean。通過@Autowired註解可以完成自動裝配。
例如,考慮下面程式碼中的CDPlayer類,它的建構函式被@Autowired修飾,表明當Spring建立CDPlayer的bean時,會給這個建構函式傳入一個CompactDisc的bean對應的引用。
package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author 李智
* @date 2017/5/9
*/
@Component
public class CDPlayer implements MediaPlay {
private CompactDisc cd;
@Autowired
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
還有別的實現方法,例如將@Autowired註解作用在setCompactDisc()方法上:
@Autowired
public void setCd(CompactDisc cd) {
this.cd = cd;
}
或者是其他名字的方法上,例如:
@Autowired
public void insertCD(CompactDisc cd) {
this.cd = cd;
}
更簡單的用法是,可以將@Autowired註解直接作用在成員變數之上,我們開發一般都是直接這麼用的吧,例如:
@Autowired
private CompactDisc cd;
只要對應型別的bean有且只有一個,則會自動裝配到該屬性上。如果沒有找到對應的bean,應用會丟擲對應的異常,如果想避免丟擲這個異常,則需要設定@Autowired(required=false)。不過,在應用程式設計中,應該謹慎設定這個屬性,因為這會使得你必須面對NullPointerException的問題。
如果存在多個同一型別的bean,則Spring會丟擲異常,表示裝配有歧義,解決辦法有兩個:
(1)通過@Qualifier註解指定需要的bean的ID;
(2)通過@Resource註解指定注入特定ID的bean;
現在我們驗證一下上述程式碼,通過下列程式碼,可以驗證:CompactDisc的bean已經注入到CDPlayer的bean中,同時在測試用例中是將CDPlayer的bean注入到當前測試用例。
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import soundsystem.CDPlayerConfig;
import soundsystem.CompactDisc;
import soundsystem.MediaPlay;
/**
* @author 李智
* @date 2017/5/9
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CDPlayerConfig.class)
//@ContextConfiguration(locations = {"classpath:/applicationContext.xml"})
public class CDPlayerTest {
@Rule
public final StandardOutputStreamLog log = new StandardOutputStreamLog();
@Autowired
private CompactDisc cd;
@Autowired
private MediaPlay player;
@Test
public void cdShouldNotBeNull() {
assertNotNull(cd);
}
@Test
public void play() {
player.play();
assertEquals("Playing" + "Sgt.Pepper's Lonely Hearts Club Band" + "by" + "The Beatles\n", log.getLog());
}
}
這裡可以使用 public final Logger log = LoggerFactory.getLogger(CDPlayerTest.class);
來替代 public final StandardOutputStreamLog log = new StandardOutputStreamLog();
,要使用StandardOutputStreamLog,需要新增Jar包如下:
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.16.0</version>
</dependency>
基於Java配置檔案裝配Bean
Java配置檔案不同於其他用於實現業務邏輯的Java程式碼,因此不能將Java配置檔案業務邏輯程式碼混在一起。一般都會給Java配置檔案新建一個單獨的package,實際上之前就用了Java配置的。
@Configuration
@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})
public class SoundSystemConfig {
}
@Configuration註解表示這個類是配置類,之前我們是通過@ComponentScan註解實現bean的自動掃描和建立,這裡我們重點是學習如何顯式建立bean,因此首先將@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})這行程式碼去掉。
我們先通過@Bean註解建立一個Spring bean,該bean的預設ID和函式的方法名相同,即sgtPeppers。例如:
@Bean
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
//或註明id
@Bean(name = "lonelyHeartsClub")
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
可以利用Java語言的表達能力,實現類似工廠模式的程式碼如下:
@Bean
public CompactDisc randomBeatlesCD() {
int choice = (int)Math.floor(Math.random() * 4);
if (choice == 0) {
return new SgtPeppers();
} else if (choice == 1) {
return new WhiteAlbum();
} else if (choice == 2) {
return new HardDaysNight();
} else if (choice == 3) {
return new Revolover();
}
}
然後在JavaConfig中的屬性注入:
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
看起來是函式呼叫,實際上不是:由於sgtPeppers()方法被@Bean註解修飾,所以Spring會攔截這個函式呼叫,並返回之前已經建立好的bean——確保該SgtPeppers bean為單例。
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
@Bean
public CDPlayer anotherCDPlayer() {
return new CDPlayer(sgtPeppers());
}
如上程式碼所示:如果把sgtPeppers()方法當作普通Java方法對待,則cdPlayerbean和anotherCDPlayerbean會持有不同的SgtPeppers例項——結合CDPlayer的業務場景看:就相當於將一片CD同時裝入兩個CD播放機中,顯然這不可能。
預設情況下,Spring中所有的bean都是單例模式,因此cdPlayer和anotherCDPlayer這倆bean持有相同的SgtPeppers例項。
當然,還有一種更清楚的寫法:
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
return new CDPlayer(compactDisc);
}
@Bean
public CDPlayer anotherCDPlayer() {
return new CDPlayer(sgtPeppers());
}
這種情況下,cdPlayer和anotherCDPlayer這倆bean持有相同的SgtPeppers例項,該例項的ID為lonelyHeartsClub。這種方法最值得使用,因為它不要求CompactDisc bean在同一個配置檔案中定義——只要在應用上下文容器中即可(不管是基於自動掃描發現還是基於XML配置檔案定義)。
基於XML的配置方法
在之前Bean的定義有提到過,這裡就不復述了。
混合使用多種配置方法
之前有提到過,開發過程中也可能使用混合配置,首先明確一點:對於自動配置,它從整個容器上下文中查詢合適的bean,無論這個bean是來自JavaConfig還是XML配置。
在JavaConfig中解析XML配置
//通過@Import註解匯入其他的JavaConfig,並且支援同時匯入多個配置檔案;
@Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
public class SoundSystemConfig {
}
//通過@ImportResource註解匯入XML配置檔案;
@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath: cd-config.xml")
public class SoundSystemConfig {
}
在XML配置檔案中應用JavaConfig
//通過<import>標籤引入其他的XML配置檔案;
//通過<bean>標籤匯入Java配置檔案到XML配置檔案,例如
<bean class="soundsystem.CDConfig" />
通常的做法是:無論使用JavaConfig或者XML裝配,都要建立一個root configuration,即模組化配置定義;並且在這個配置檔案中開啟自動掃描機制:或者@ComponentScan。
總結
由於自動裝配幾乎不需要手動定義bean,建議優先選擇自動裝配;如何必須使用顯式配置,則優先選擇基於Java檔案裝配這種方式,因為相比於XML檔案,Java檔案具備更多的能力、型別安全等特點;但是也有一種情況必須使用XML配置檔案,即你需要使用某個名字空間(name space),該名字空間只在XML檔案中可以使用。
ps:上述例子都是直接用的《Spring實戰》