1.1 Sring IOC容器和Beans簡介
本章介紹了Spring Framework中控制反轉(IOC)的實現原理。IOC也被稱爲依賴注入(DI)。它是一個對象定義依賴關係的過程,即對象只能通過構造函數參數、工廠方法的參數、對象實例在被構造完成後或從工廠方法返回後設置的屬性來定義與之工作的其他對象。然後容器在創建bean時注入這些依賴項。常規創建bean依賴關係的過程,通過使用被依賴方的類的直接構造或類似服務定位器模式的機制,用以控制被依賴項的實例化或位置。和常規的創建bean依賴關係的過程比,這個過程(IoC)是完全相反的,因此被稱爲控制反轉(IoC)。
org.springframework.beans 和 org.springframework.context 兩個包是 Spring Framework IoC 容器的基礎。BeanFactory 接口提供了一種高級的配置機制,用以管理各種類型的對象。ApplicationContext 是 BeanFactory 的子接口,它能夠更加容易地集成Spring 的 AOP 特性,諸如消息資源處理(用以國際化)、事件發佈,還有應用層特定的上下文,例如用於web應用程序的WebApplicationContext。
簡而言之,BeanFactory接口提供了配置框架和基本功能,而ApplicationContext在前者基礎上添加了更多企業特定的功能。ApplicationContext是對BeanFactory完全的擴展,在本章中它被用來專門描述Spring的IoC容器。想要了解更多關於何時選擇使用BeanFactory而不是ApplicationContext的信息,請參閱後面專門介紹BeanFactory的章節。
在Spring中,構成應用程序主幹並由Spring IoC容器管理的對象被稱爲bean。bean是由Spring IoC容器實例化、組裝並管理的。否則,bean僅僅是應用程序中衆多對象之一。Beans包括他們之間的各種依賴,全都由容器所使用的配置元數據表示。
1.2 IoC容器概覽
org.springframework.context.ApplicationContext 接口代表了Spring IoC容器,並且負責實例化、配置、組裝上一節介紹的beans。容器通過讀取配置元數據來獲取關於如何實例化、配置、組裝對象的指令。這些配置元數據可通過XML、Java註解或者Java代碼表示,通過配置元數據,你可以表示組成應用程序的多個對象以及對象之間龐雜的互相依賴關係。
Spring提供了幾種ApplicationContext接口的實現,這些實現方式可做到開箱即用。在獨立的應用程序中,創建ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext實例是很常見的。儘管XML一直是定義配置元數據的傳統格式,但你可以通過提供少量XML配置來聲明支持一些額外的元數據格式,例如指示容器使用Java註解或代碼作爲元數據格式。
在大多數應用程序場景中,開發人員無需顯式地使用代碼來初始化一個或多個Spring IoC容器實例。例如,在web應用程序場景下,只需在應用程序的web.xml文件中寫上大約8行符合web模板的代碼就足夠了(請參閱web應用程序的快速ApplicationContext實例化)。如果你使用的是基於eclipse的Spring工具套件開發環境,只需要點擊鼠標或者敲擊鍵盤就可輕鬆創建滿足條件的配置模板。
下面的圖高度抽象了Spring的工作原理,先將應用程序的類與配置元數據相關聯,待應用程序上下文被創建和初始化後,你將獲得一個完全配置好且可執行的系統或者應用程序。
1.2.1配置源數據
如前圖所示,Spring IoC容器使用元數據配置的形式來呈現作爲程序開發者該如何告知Spring容器實例化、配置、組裝程序中的對象。
配置元數據傳統上以簡單直觀的XML格式提供,這是本章大部分內容用來傳達Spring IoC容器的關鍵概念和特性的方式。
提示:基於xml的元數據不是配置元數據的唯一允許形式。Spring IoC容器本身與實際編寫配置元數據的格式完全分離。現在許多開發人員爲他們的Spring應用程序選擇基於java的配置。
有關使用Spring容器的其他元數據形式的信息,請參閱:
- 基於註釋的配置:Spring 2.5引入了對基於註解的配置元數據的支持。
- 基於java的配置:從Spring 3.0開始,Spring JavaConfig項目提供的許多特性成爲Spring框架的核心部分。因此,您可以通過使用Java而不是XML文件來定義應用程序類外部的bean。要使用這些新特性,請參閱@Configuration、@Bean、@Import和@DependsOn註解。
Spring配置包含至少一個bean定義,但通常都包含多個bean定義,這些bean定義都由容器進行管理。基於XML的配置元數據展示的beans通過頂級元素<beans/>中的<bean/>元素進行配置。基於Java代碼的配置,通常採用被@Configuration註解的類中被@Bean註解的方法的方式來展示beans。
這些bean定義相當於實際的對象,構成了你的應用程序。通常,你會定義業務層對象,數據讀寫層對象(DAOS),展示層對象(比如Struts Action實例),一些諸如Hibernate Session工廠、JMS隊列等等的基礎對象。通常不會在容器中定義細粒度的領域對象,因爲創建和加載領域對象是數據存取層和業務邏輯層的職責。但是,你可以使用Spring與AspectJ的集成來配置不受IoC容器控制而創建的對象。具體內容請參閱後續章節:Using AspectJ to dependency-inject domain objects with Spring。
下面的例子展示了基於XML的配置元數據的基礎結構:
id屬性是一個用來標識單個bean定義的字符串,class屬性定義了bean的類型並且使用完全限定的類名。id屬性的值可以被其他bean引用,這個例子中沒有引用其他bean,更多細節請參考後面Dependencies章節。
1.2.2實例化容器
實例化一個Spring IoC容器是非常容易的。將一個或多個位置路徑提供給ApplicationContext
的構造方法就可以讓容器加載配置元數據,可以從多種外部資源進行獲取,例如文件系統、Java的CLASSPATH
等等。
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
提示:學完Spring的IoC容器後,你或許想了解更多關於Spring的抽象資源,就像在Resources章節中描述的一樣,它提供了一 種從符合URI語法的位置中讀取輸入流的便捷機制。特別是在應用上下文和資源路徑章節中,資源路徑被用來構造應用上 下文。
下面的例子展示了service層對象的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>
下面的例子是數據訪問層daos.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for data access objects go here -->
</beans>
在前例中,service層由PetStoreServiceImpl類和兩個類型分別爲JpaAccountDao、JpaItemDao(基於JPA對象/關係映射標準)的數據訪問對象組成。property標籤中的
name元素指代JavaBean屬性的名稱,ref元素引用了另外一個bean定義的名稱。這種id與ref元素之間的鏈接關係表達出了合作對象之間的依賴。更多關於配置對象的依賴細節,請參考Dependencies章節。
編寫基於XML的配置元數據
bean定義可以跨越多個XML文件是非常有用的。通常每個單獨的XML配置文件表示一個邏輯層或者是你架構中的一個模塊。你可以使用應用上下文的構造器從所有的XML片段加載bean定義。像上面例子中展示的那樣,構造器可以接受多個Resource位置。另外,也可以使用一個或多個<import/>元素從其他文件中加載bean定義。例如:
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>
在上面例子中,外部的bean定義通過services.xml、messageSource.xml、themeSource.xml三個文件被加載。所有的位置路徑都是相對於定義執行導入的文件,所以services.xml必須存在於導入文件相同的目錄下或類路徑位置,而messageSource.xml和themeSource.xml必須位於導入文件之下的resources位置。可以看到,這裏開頭的斜槓(/)被忽略了,因爲給定的都是相對路徑,因此建議不使用任何斜槓。被導入文件的內容,包括頂級元素<beans/>在內,相對於Spring Schema必須是合法的XML bean定義。
提示: 有可能會存在使用"../"直接引用上級目錄的文件的情況,但不建議這樣做。這樣做會造成對當前應用程序之外的文件的依 賴。特別不推薦在"classpath:"路徑中使用"../"這種引用(例如,"classpath:../services.xml"),這種情況下程序在運 行時會先選擇導入文件最近的classpath根路徑,然後再查找它的上級文件夾。Classpath配置的改變可能會導致程序選 擇不同、非正確的目錄。
你可能會使用全限定資源路徑(例如,file:C:/config/services.xml或者classpath:/config/services.xml)來替代相對 路徑。但需要注意,這樣做會使你的應用配置同特定的絕對位置耦合在一起。通常更合適的方式是通過間接的方式來使 用絕對路徑,例如通過"${…}"佔位符,在運行時解析JVM的系統屬性。
導入指令是由beans命名空間自身提供的功能。在Spring提供的XML命名空間中,除了可以使用普通bean定義,還可以使用其他配置功能(例如“context”和“util”命名空間)。
Groovy Bean Definition DSL(Domain Specified Language)
作爲外部化配置元數據的進一步示例,bean定義也可以在Spring的Groovy bean定義DSL中表示,從Grails框架中可以知道這一點。通常,這樣的配置將存在於以".groovy"爲後綴的文件中,其結構如下:
beans {
dataSource(BasicDataSource) {
driverClassName = "org.hsqldb.jdbcDriver"
url = "jdbc:hsqldb:mem:grailsDB"
username = "sa"
password = ""
settings = [mynew:"setting"]
}
sessionFactory(SessionFactory) {
dataSource = dataSource
}
myService(MyService) {
nestedBean = { AnotherBean bean ->
dataSource = dataSource
}
}
}
這種配置方式相當於XML bean定義,它甚至支持Spring的XML配置命名空間,同時還允許通過使用"importBeans"指令導入XML bean定義的文件。
1.2.3使用容器
ApplicationContext是高級的工廠接口,它能夠維護註冊其中的不同beans和beans之間的依賴。通過T getBean(String name, Class<T> requiredType)方法,你可以獲取beans實例。
ApplicationContext可以讓你以如下這種方式讀取和獲取bean定義:
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();
通過Groovy配置進行引導同上面的方式非常相似,只是上下文實現類Groovy-aware(但同樣可以理解爲XMLbean定義)不同存在差異罷了:
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
最靈活的變體是GenericApplicationContext和reader delegate相結合,例如XmlBeanDefinitionReader用於讀取XML文件:
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();
或者GroovyBeanDefinitionReader
用於讀取Groovy文件:
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
這種reader delegate可以在相同的ApplicationContext上混合和匹配,如果需要,可以從不同的配置源讀取bean定義。
然後可以使用getBean來獲取你的bean實例。ApplicationContext接口擁有一些其它獲取bean的方法,但理想情況下,應用程序代碼永遠不應該使用它們。事實上,你的應用程序代碼中不應存在對getBean()方法的任何調用,即不存在對Spring API的任何依賴。舉個例子,Spring與web框架之間的集成,爲各種各樣web框架組件(如controllers和JSF管理的bean)提供依賴注入,爲了不使應用代碼與Spring API耦合,你可以通過元數據(比如autowiring註解)聲明對特定bean的依賴。
1.3Bean概覽
Spring IoC容器管理一個或多個bean,這些bean通過你提供給容器的配置元數據被創建出來,例如,在XML格式中的<bean/>定義。
在容器本身中,這些bean定義以BeanDefinition對象來表示,包含(以及其他信息)如下元數據:
-
包限定類名,通常是定義的bean的實際實現類。
-
Bean行爲配置元素,表示bean在容器內的行爲 (作用域,生命週期回調,等)。
-
Bean實現功能時對其它bean的引用,這些引用被稱作協作者或依賴關係。
-
在新創建對象中設置其它配置項,例如,bean所管理連接池使用的連接的數量,或連接池的規模限制。
由元數據翻譯來的屬性集構成了每一個bean定義。
Property | Explained in… |
---|---|
class(類) |
Instantiating beans(實例化bean) |
name(名稱) |
Naming beans(命名bean) |
scope(作用域) |
Bean scopes(bean作用域) |
constructor arguments(構造參數) |
Dependency Injection(依賴注入) |
properties(屬性) |
Dependency Injection(依賴注入) |
autowiring mode(自動裝配方式) |
Autowiring collaborators(自動裝配協作者) |
lazy-initialization mode(延遲初始化方式) |
Lazy-initialized beans(延遲初始化的bean) |
initialization method(初始方法) |
Initialization callbacks(初始化回調) |
destruction method(銷燬方法) |
Destruction callbacks(銷燬回調) |
除了包含如何創建特定bean的信息的bean定義之外,ApplicationContext的實現還允許用戶註冊在容器之外創建的存在的對象。這是通過訪問ApplicationContext的BeanFactory中getBeanFactory()方法完成的,該方法返回BeanFactory實現類DefaultListableBeanFactory。DefaultListableBeanFactory通過registerSingleton(..)和registerBeanDefinition(..)方法來支持前面提及的外部註冊的方式。然而,典型的應用程序只使用通過元數據bean定義定義的bean。
提示: Bean元數據和手動提供的單實例需要被儘可能早地註冊,以便容器在進行自動裝配和其他內省步驟期間更好地關聯 它們。儘管覆蓋已存在的元數據和單實例在某種程度上是被支持的,在運行時註冊新的bean(與實時訪問工廠同時)並 未得到官方支持,並且可能導致bean容器的併發訪問異常和/或狀態不一致。
1.3.1bean命名
每個bean都有一個或多個標識符。這些標識符能在容器中唯一地表示bean,一個bean通常只會有一個標識符,但如果需要的數量不止一個,其他的標識符可被視爲別名。
在基於XML的配置元數據中,你可以使用id和/或name屬性來指定bean標識符。id屬性允許你精確地指定一個id。通常這些名字是字母數字(如‘myBean’,‘fooService’,等等),但是也可能包含特殊字符。如果想要爲bean引入其它別名,你可以在name屬性中指定它們,以逗號(,)、分號(;)或者空格隔開即可。作爲歷史記錄,在Spring 3.1版本之前,id屬性被定義爲 xsd:ID類型,它約束了可能的字符。從3.1開始,它被定義爲 xsd:string類型。需要注意,bean id唯一性仍是被強制執行的,但不再由XML解析器執行而改由容器執行。
如果沒有id或名稱明確地被提供,容器會爲bean生成一個唯一的名稱,你不再需要爲bean提供名稱或id。但是,如果你需要通過名稱引用bean,例如通過使用ref元素或者Service Locator檢索方式,則必須提供名稱。不提供名稱的動機與使用內部bean和自動裝配合作者有關。
Bean命名習慣
在爲bean命名時採用標準的Java實例屬性命名規範,即bean名稱以小寫的字母開始,然後是駝峯命名風格。這類名字的例子(沒有引號)‘accountManager’,‘accountService’,‘userDao’,‘loginController’,等等。
一致的bean命名風格使得你的配置易讀和你易於理解,並且如果使用Spring AOP的話,當你應用切面到一堆與名稱相關的bean時,這將很有幫助。
提示:在類路徑中的組件掃描中,Spring爲未命名的組件生成bean名稱,遵從上面的規則:本質上,使用類名並將首字母轉爲 小寫。然而,在(不尋常的)特殊情況下,當有一個以上的字符,並且第一和第二字符都是大寫時,原始的大小寫保留。這 同java.beans.Introspector.decapitalize(Spring在此處使用它)中定義的你規則一致。
在bean定義之外爲bean起別名
在bean定義自身中, 你可以爲bean提供不止一個名稱,通過使用(id屬性中指定的最多一個名稱和name屬性中任意數量的名稱之間的)組合來實現。這些名稱相當於bean的別名,它們對於有些場景很有用,例如允許應用程序中的每個組件,通過使用特指該組件本身的bean名稱引用一個公共的依賴項。
然而,在bean被實際定義處指定所有別名通常是不夠的。有時會有這種需求產生,在bean被定義的其它位置引入別名。這種場景在大型系統中非常常見,在這樣系統中配置配置拆分到各個子系統中,每個子系統有它自己的對象定義集合。在基於XML的配置元數據中,你可以使用<alias/>元素來實現上述需求。
<alias name="fromName" alias="toName"/>
此處,在同一個容器中,命名爲fromName的bean,在使用別名定義後,也可以被稱爲toName。
舉個例子,子系統A的配置元數據可能通過subsystemA-dataSource名稱來引用DataSource(數據源),而子系統B則通過subsystemB-dataSource名稱引用DataSource(數據源)。當整合使用這兩個子系統的主應用程序時,使用myApp-dataSource名稱來引用DataSource(數據源)。要使三個名稱都引用你添加到MyApp配置元數據中的同一對象,請使用以下別名定義:
<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />
現在,每個組件和主應用程序都可通過唯一的名稱引用到數據源,並且保證不會和任何其他定義發生衝突(有效地創建命名空間),而引用到同一bean。
如果你正在使用Java配置,可以通過使用@Bean註解提供別名,更多細節請參考Using the @Bean annotation。
1.3.2. 實例化beans
bean定義本質上是一個用於創建一個或多個對象的配方。容器被詢問時會查看特定命名對應的配方(bean definition),然後使用由該bean定義封裝的配置元數據來創建(或獲取)實際的對象。
如果你使用基於XML的配置元數據,在<bean/>元素的class屬性中,你可以指定需要被實例化的bean的類型或(類)。這個class屬性,是BeanDefinition實例中的一個內部成員變量,通常是強制性的。(異常情況,請參考Instantiation using an instance factory method and Bean definition inheritance——使用實例工廠方法和bean定義繼承的實例化。)你可以通過以下兩種方式中的任意一種來使用Class屬性:
- 通常,在容器自己通過反射的方式調用bean的構造方法直接創建bean的情況下指定要構造的bean類,某種程度上等同於使用Java代碼的new操作。
- 指定包含靜態工廠方法的實際類,在不太常見的情況下,容器調用類的靜態工廠方法創建bean。通過調用靜態的工廠方法返回的對象類型可能和調用類相同,也有可能是一個完全不同的類。
內部類名
如果要爲靜態內部類配置bean定義,則必須使用內部類的二進制名稱。例如,在com.example包下有一個命名爲Foo的類,並且Foo類有一個靜態的內部類Bar,'class'屬性的值將會是:com.example.Foo$Bar。注意在名字中使用$字符將嵌套類名同外部類名分隔開。
通過構造器實例化
當你使用構造器的方式創建bean時,所有普通類都可用,並且與Spring兼容。也就是說,正在開發的類無需實現任何指定的接口或者採用特定的編碼風格。僅僅指定bean類就足矣。但是,根據對指定bean所使用IoC的類型,你或許需要一個默認(空)的構造方法。
Spring IoC容器幾乎能夠管理任何你想交由管理的類,不僅僅侷限於管理標準的JavaBean。大多數Spring使用者喜歡只含有一個默認(無參)構造函數,以及根據容器中的屬性生成恰當的set、get方法。當然,你也可以在容器中使用非bean形式(non-bean-style)的類。例如,你需要使用遺留系統中的連接池,顯然它完全不遵循JavaBean規範,Spring仍舊能夠很好的管理它。
在基於XML的配置元數據中,你可以採用如下方式指定bean類:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
想要了解爲構造函數指定參數(有必要的話)的實現機制以及爲構造好的對象設置對象實例屬性,請參考Injecting Dependencies章節。
通過靜態工廠方法實例化
當定義採用靜態工廠方法創建的bean時,使用class屬性指定包含靜態工廠方法的類和名爲factory-method的屬性來指定工廠方法本身的名稱。Spring將調用此方法(其他可選參數稍後介紹)返回實例對象,這個對象隨後會像使用通過構造函數創建的對象一樣被使用。這種bean定義的一個用處類似於在傳統代碼中調用靜態工廠。
下面的bean定義指定了通過調用工廠方法創建的bean。定義並未指明返回對象的類型(類),僅僅指定了包含工廠方法的類。在這個例子中,createInstance()方法必須是一個靜態方法。在例中將展示如何指定一個工廠方法:
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
接下來的例子將會展示配合上例中的bean定義所使用的類:
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
想知道關於在工廠方法中應用(可選)參數和給從工廠中返回的對象設置對象實例屬性機制的更多細節,請參考Dependencies and Configuration in Detail章節。
通過實例工廠方法實例化
和使用靜態工廠方法實例化類似,採用實例工廠方法實例化調用存在於容器中的bean身上的非靜態方法以創建新的bean。採用這種機制實例化,需要將class屬性置之不理,並在factory-bean屬性中,指定bean名稱,這個名稱屬於當前(或父級或祖級)容器中存在的bean,而且該bean包含一個可以通過調用它以創建對象的實例方法。使用factory-method屬性設置工廠方法本身的名稱。下面的例子展示瞭如何配置一個這樣的bean:
<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<!-- the bean to be created via the factory bean -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
下面的例子展示了與之相關的Java類:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
一個工廠類也可以持有不止一個工廠方法,就像下面例子中展示的一樣:
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
<bean id="accountService"
factory-bean="serviceLocator"
factory-method="createAccountServiceInstance"/>
下面的示例代碼是與上例相關的Java類:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
private static AccountService accountService = new AccountServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
public AccountService createAccountServiceInstance() {
return accountService;
}
}
這種方式顯示了工廠bean本身也能夠通過依賴注入(DI)被管理和配置。參考Dependencies and Configuration in Detail章節。
在Spring文檔中,“factory bean”是指在Spring容器中配置的能夠通過實例或者靜態工廠方法創建對象的bean。相比較而言,FactoryBean(注意大小寫)是指Spring特有的 |
1.4依賴
一個典型的企業應用程序不只是由單個對象組成(或者Spring中使用的bean)。即便是最簡答的應用程序也會擁有不止一個對象,這些對象相互協作爲終端用戶呈現一個條理分明的應用程序。接下來的這一節會闡述,如何定義多個獨立於完全實現的應用程序的bean定義,這些bean定義在程序中協同完成某個目標。
1.4.1依賴注入
依賴注入(DI)是一個過程(一道工序),在這個過程中,對象通過構造器的參數、工廠方法的參數、被設置到(已創建的或工廠方法返回的)對象實例上的屬性,來定義它的依賴(也就是說,它需要使用的其他對象)。然後容器在創建bean的時候注入這些依賴關係。通常情況下,bean自己採用類的直接構建或服務定位器模式來控制它依賴對象的實例化或位置,依賴注入卻不同,它是前面過程根本上地反轉(也因此得名,控制反轉)。
使用依賴注入使得代碼看起來更加清爽,並且爲對象提供依賴關係時解耦也會顯得更爲有效。對象不用查找它的依賴對象並且無需知道依賴對象的位置或類。這會直接導致,你的類變得更加容易測試,尤其是對接口或抽象基類的依賴,這允許你在單元測試中stub或mock實現。
DI主要有兩種實現方式:基於構造函數的依賴注入和基於set方法的依賴注入。
基於構造器的依賴注入
基於構造器的DI由容器調用多參數構造方法完成,每個參數表示一個依賴項。使用具體的參數調用靜態工廠方法以構建bean近乎等效,本文將類似地對待構造函數和靜態工廠方法的參數。下面的例子展示了一個僅能採用構造器方式進行依賴注入的類:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
注意,關於上面的類並無什麼特別之處。它就是一個不依賴於容器特定接口、基類或註解的普通Java對象。
構造器參數解析
構造器參數解析時使用參數的類型進行匹配。在bean定義的構造參數中如果沒有潛在歧義,在bean定義中被定義的構造器參數的順序,即是在實例化bean時對應構造器中那些參數的順序。看下面這個類:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
假設ThingTwo和ThingThree兩個類之間沒有繼承關係,不存在潛在的歧義。那麼,下面的配置能夠很好的運轉,你無需在<constructor-arg/>元素中明確地指明構造器參數的索引或者類型。
<beans>
<bean id="thingOne" class="x.y.ThingOne">
<constructor-arg ref="thingTwo"/>
<constructor-arg ref="thingThree"/>
</bean>
<bean id="thingTwo" class="x.y.ThingTwo"/>
<bean id="thingThree" class="x.y.ThingThree"/>
</beans>
當另一個bean被引用時,類型是已知的,並且匹配也沒問題(就像前例中一樣)。當使用簡單類型時,例如<value>true</value>,Spring無法決定值的類型,在沒有其它幫助的情形下也就無法根據類型進行匹配。看下面的類:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private int years;
// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
構造器參數類型匹配
在前面的場景中,如果使用type屬性明確地指出構造器參數的類型,容器就可以對簡單類型使用類型匹配。就像下面例子中展示的一樣:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
構造器參數索引
你可以使用index屬性明確地指出構造器參數的索引,如下例:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
除了解決多個簡單類型值的模糊性之外,指定索引還可以解決同一構造器中兩個參數類型相同的模糊性問題。
參數索引是從0開始遞增的。 |
構造器參數名稱
你也可以通過使用構造器中參數的名字來規避模糊性,就像下面例子中展示的那樣:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
需要記住一點,爲使這一工作開箱即用,代碼必須在啓用調試標記的情況下進行編譯,以使Spring能夠從構造器中查找參數名。
如果你不能或不想在調試標記狀態下編譯代碼,你可以使用JDK註解@ConstructorProperties指出你構造器參數的名稱。必須像下面的樣例一樣:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基於set方法的依賴注入
基於set方法的依賴注入,由容器在調用無參構造或者無參靜態工廠方法實例化bena之後,再調用beans上的set方法來完成。接下來的例子將展示只能使用純set注入的類。該類是普通Java類,且不包含對容器特定接口、基類或註解的依賴。
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
應用上下文對於它所管理的beans,支持構造注入和set注入。它還支持在一些依賴通過構造器方式被注入過後再使用set注入的方式。以BeanDefinition的格式配置依賴項,並通過與PropertyEditor實例配合使用,將屬性從一種形式轉換爲另外一種形式。然而,大部分Spring使用者在代碼層不會直接用到這些類,而是使用XML bean定義、含有註解的組件(例如,通過@Component、@Controller等註解的類)或者基於Java代碼含有@Configuration註解的類中被@Bean註解的方法。這些源文件隨後會在內部被轉換爲BeanDefinition的實例並被用於加載整個Spring IoC容器實例。
使用構造注入還是set注入?
因爲能夠混合使用構造注入和set注入,所以使用構造注入處理強制依賴關係和set注入或者配置方法處理可選依賴關係,是一條很好的經驗法則。需要記住在set方法上使用@Required註解能夠將屬性成爲必須的依賴項。
Spring團隊一般提倡構造注入,因爲它能讓應用程序組件實現爲不可變對象,並保證所需的依賴項非空。此外構造注入的組件通常以一種完全初始化的狀態被返還給客戶端(調用方)代碼。作爲補充說明,大量的構造器參數是不友好的編碼風格,這意味着類可能有太多的職責,這樣的類應當被重構使得它所關注點更爲簡單清晰。
set注入主要用於可選依賴項,這些依賴項在類中可以被分配合理的默認值。否則,在代碼使用依賴項的任何地方都必須執行非空檢查。set注入的好處之一是,set方法使類的對象在後面的某個時刻能重新配置或重新注入。因此,通過JMX MBeans進行管理是一個非常顯著的set注入例子。
對特定類使用最爲合理的DI方式。有時候,在處理沒有源碼的第三方類時,後面介紹的內容將爲你提供選擇。例如,如果第三方類沒有暴露任何set方法,那麼構造注入將會成爲唯一的注入方式。
依賴解析過程
容器按照如下步驟執行bean依賴解析:
-
使用描述所有beans的配置元數據,創建並初始化應用上下文(ApplicationContext)。配置元數據可以通過XML、Java代碼或者註解來指定。
-
對於每個bean,它的依賴項通過屬性、構造參數、靜態工廠方法參數來表示(如果你使用DI而不是標準的構造函數)。當bean被實際創建的時候,這些依賴將會被提供給它。
-
每個屬性或者構造器參數既可以是一個實際的值,也可以是容器中另一個bean的引用。
-
每個作爲值的屬性或構造器參數,都是從它的指定格式轉換爲其實際類型。默認情況下,Spring會將提供的字符串格式的值轉換爲所有內置的類型,例如int、long、String、boolean等等。
Spring容器被創建時,由其自己校驗每個bean的配置。然而,直到實際創建bean時,纔會設置bean屬性本身。作用域爲單例的beans,會提前到創建容器時被創建。作用域在Bean scopes章節中有介紹。除此之外,bean僅會在被請求時才創建。bean創建可能會導致一系列的bean被創建,因爲bean的依賴和它的依賴的依賴(等等)被創建和分配。需要注意,這些依賴之間的解析不匹配可能會滯後出現,即在第一次創建受影響的bean時。
循環依賴
如果你主要使用構造注入,有可能會出現創建不可解析的循環依賴的場景。舉個例子:類A在構造注入過程中需要一個類B的實例,而類B在構造注入過程中需要一個類A的實例。如果將A、B兩個類配置爲互相注入的beans,Spring IoC容器在運行時會檢測到這個循環引用,並拋出異常(BeanCurrentlyInCreationException)。
一個可能的解決方法是修改類的源代碼,將構造器注入改爲setter注入。或者,避免構造器注入而僅使用set注入。換句話說,你可以使用set注入配置循環依賴項,儘管不推薦這樣做。
與典型的場景不同(無循環依賴),bean A和bean B之間的循環依賴,導致其中一個bean在未完全初始化之前被注入到另一個bean(一個經典的場景,先有雞還是先有蛋)。
通常來講,你可以相信Spring會做正確的事情。它會在容器的加載週期檢測配置問題,例如引用不存在的beans和循環依賴。在在實際創建bean時,Spring會盡可能遲地設置屬性和解析依賴。這意味着,當你真正向容器請求所需對象時,已經正確加載的容器會創建你所請求的對象或者對象的依賴,如果創建過程中出現問題,即便Spring容器已經被正確加載仍舊會產生異常。舉個例子,bean因爲丟失的或非法的屬性而拋出異常。這種潛在的配置問題延遲暴露的風險,就是爲什麼應用上下文會以提前實例化單例beans作爲默認實現。在實際使用beans之前耗費時間和內存創建它們,帶來的好處是讓你在應用上下文創建bean的時候即可發現配置問題,而不用等到真正請求它時才發現問題。你仍然能夠覆蓋這種默認的行爲,以便單例beans能夠被延遲初始化而不是提前實例化。
如果不存在循環依賴,當一個或多個協作bean被注入依賴bean時,每個協作bean在被注入到依賴bean之前會被完全配置。這意味着,如果bean A依賴於bean B,Spring IoC容器會在調用bean A的setter方法之前完全配置好bean B。換句話說,實例化bean時(如果該bean不是提前實例化的但實例),它的依賴項會被提前設置,並且與之相關的生命週期方法(例如配置好的init方法或initializingBean回調方法)也都會被調用。
依賴注入的例子
下面的例子爲基於setter方法的依賴注入使用基於XML的配置元數據。Spring XML 配置文件的一小部分指定了一些bean定義:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
在上例中,setter方法被聲明用以匹配在XML文件中指定的屬性。下面的例子使用構造注入:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- constructor injection using the nested ref element -->
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<!-- constructor injection using the neater ref attribute -->
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
下面的例子展示了相應的 ExampleBean 類:
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public ExampleBean(
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
this.beanOne = anotherBean;
this.beanTwo = yetAnotherBean;
this.i = i;
}
}
在bean定義中指定的構造器參數被用作 ExampleBean 的構造器的參數。
現在考慮一下這個例子的變種,不再採用構造注入的方式,而是通過調用靜態工廠方法來返回一個對象實例:
<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
<constructor-arg ref="anotherExampleBean"/>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
下面的例子展示了相應的 ExampleBean 類:
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
靜態工廠方法的參數由<constructor-arg/>元素提供,與實際使用構造器完全相同。工廠方法所返回實例對象的類型不必與提供靜態工廠方法的類型相同(儘管在這個例子中是一樣的)。非靜態的實例工廠方法可以通過基本相同的方式使用(除了使用 factory-bean 屬性代替 class 屬性),所以此處不再討論那些細節。
1.4.2 依賴關係和詳細配置
如前一節所述,您可以將bean屬性和構造函數參數定義爲對其他託管bean(合作者)的引用,或者定義爲內聯的值。Spring基於XML的配置元數據支持其<property/>和<constructor-arg/>元素內的子元素類型。
直接值(基本類型,字符串,等等)
<property/>元素的value屬性將屬性或構造函數參數指定爲人類可讀的字符串表示形式。Spring的轉換服務用於將這些值從字符串轉換爲屬性或參數的實際類型。以下示例顯示正在設置的各種值:
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="masterkaoli"/>
</bean>
以下示例使用p-namespace進行更簡潔的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="masterkaoli"/>
</beans>
前面的XML更加簡潔。但是,除非使用支持創建bean定義時自動完成屬性的IDE(例如Intellij IDEA或Spring工具套件),否則會在運行時發現拼寫錯誤。強烈建議提供此類IDE協助。
您還可以配置一個java.util.Properties實例,如下所示:
<bean id="mappings"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- typed as a java.util.Properties -->
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>
</bean>
Spring容器通過JavaBeans PropertyEditor機制,將<value/>元素中的文本轉換爲java.util.Properties實例。這是一個很好的快捷方式,也是Spring團隊支持使用嵌套的<value/>元素而不是value屬性樣式的少數地方之一。
idref 元素
idref元素只是一種防止錯誤的方法,用於將容器中另一個bean的id(字符串值-而不是引用)傳遞給<constructor-arg/>或<property/>元素。下面的示例演示如何使用它:
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
上述的bean定義片段在運行時完全等同於下面的代碼片段:
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>
第一種方式比第二種方式更好,因爲使用idref標記可以讓容器在部署時驗證引用的、命名的bean實際存在。在第二個變體中,不會對傳遞給客戶機bean的targetname屬性的值執行任何驗證。只有當客戶機bean實際被實例化時,纔會發現拼寫錯誤(最有可能是致命的結果)。如果客戶機bean是原型bean,則只有在部署容器很長時間之後,才能發現此拼寫錯誤和由此產生的異常。
4.0 beans xsd不再支持idref元素的本地屬性,因爲它不再提供常規bean引用的值。在升級到4.0模式時,更改對idref bean的現有idref本地引用。
<idref/>元素帶來值的一個常見位置(至少在Spring2.0之前的版本中)是在proxyFactoryBean定義中AOP攔截器的配置中。指定攔截器名稱時使用<idref/>元素可防止您拼寫錯誤攔截器ID。
對其他bean(合作者)的引用
ref元素是<constructor arg/>或<property/>定義元素內的最後一個元素。在這裏,您將bean的指定屬性的值設置爲對容器管理的另一個bean(合作者)的引用。引用的bean是要設置其屬性的bean的依賴項,在設置該屬性之前,根據需要對其進行初始化。(如果合作者是單例bean,那麼它可能已經由容器初始化。)所有引用最終都是對另一個對象的引用。作用域和驗證取決於您是否通過bean、本地或父屬性指定另一個對象的ID或名稱。
通過<ref/>標記的bean屬性指定目標bean是最一般的形式,允許在同一容器或父容器中創建對任何bean的引用,而不管它是否在同一XML文件中。bean屬性的值可以與目標bean的id屬性相同,也可以與目標bean的name屬性中的一個值相同。以下示例顯示如何使用ref元素:
<ref bean="someBean"/>