Spring 框架文件(核心篇1和2)
Version 5.1.3.RELEASE
最新的, 更新的筆記, 支援的版本和其他主題,獨立的釋出版本等, 是在Github Wiki 專案維護的.
總覽 歷史, 設計哲學, 反饋, 入門.
核心 IoC容器, 事件, 資源, 國際化(i18n), 驗證, 資料繫結, 型別轉化, Spring表示式語言(SpEL), 面向切面程式設計(AOP).
測試 Mock物件, 測試上下文框架(TestContext framework), Spring MVC 測試, WebTestClient.
資料訪問 事務, DAO支援, JDBC, ORM, 編組XML.
Web Servlet Spring MVC, WebSocket, SockJS, STOMP 訊息.
Web Reactive Spring WebFlux, WebClient, WebSocket.
整合 Remoting, JMS, JCA, JMX, Email, Tasks, Scheduling, Cache.
語言 Kotlin, Groovy, 動態語言(Dynamic languages).
第一部分 總覽
內容:
- 我們為什麼以"Spring"命名
- Spring及Spring框架的歷史
- 設計哲學
- 反饋和貢獻
- 入門
Spring 簡化了Java企業應用的建立. 可以提供在企業應用環境下Java生態所需的一切, 同時也支援Kotlin和Groovy作為JVM的替代語言, 根據實際需要,也可以建立多種不同的架構.(architecture). 從Spring Framwork 5.0 開始, Spring需要JDK 8+ 並且已經為JDK9提供開箱即用支援
Spring提供了廣泛的應用場景. 在大型企業應用中,應用往往要存在很長一段時間,並且不得不執行在一個升級週期超出開發人員控制的JDK和伺服器上. 而其他的應用則使用內嵌伺服器單獨執行jar包,或者部署在雲環境. 還有一些應用可能獨立部署, 根本不需要伺服器(例如批處理或整合負載).
Spring是開源的.背後有長期大量而活躍的根據實際應用案例而提交的反饋.這將幫助Spring成功長期進化.
1. 命名"Spring"的含義
"Spring"意思是在不同環境中不同的東西. 能夠用來指代Spring專案本身(這是它的發端起始點). 隨著時間推移, 其他建立在Spring之上的專案被建立出來. 通常我們稱"Spring", 其實是指所有這些專案. 本文件主要聚焦基礎: 也就是Spring框架本身.
Spring框架分模組. 可以根據情況選擇需要的模組. 核心模組是核心容器, 包含配置模型和依賴注入機制. 還有更多,Spring 框架提供了對不同應用架構的基礎支援. 包含訊息,事務和持久化,還有Web. 它還包含基於Servlet的MVC框架, 同時提供了對應的互動式框架Web Flux.
關於模組的提醒: Spring框架jar檔案允許JDK9支援的模組路徑("Jigsaw"). 在此類應用中, Spring Framework 5 的jar檔案帶有自動模組名稱清單. 它定義了獨立於jar工件的語言級別的模組名稱(“spring.core”,“spring.context”等). 當然, Spring在JDK8和9的類路徑上都可以正常工作.
2. Spring 和 Spring Framework 的歷史
Spring 是為了回應早期複雜的J2EE規範於2003年誕生. 有人認為 Java EE 和 Spring 是競爭關係,實際上,Spring是Java EE 的補充. Spring的程式設計模型並不是完全擁抱Java EE平臺規範, 而是小心地有選擇地從EE生態中整合了獨立的規範:
Servlet API (JSR 340)
WebSocket API (JSR 356)
Concurrency Utilities (JSR 236)
JSON Binding API (JSR 367)
Bean Validation (JSR 303)
JPA (JSR 338)
JMS (JSR 914)
如果需要的話,還有 JTA/JCA 做事務協調
Spring Framework 還支援依賴注入(JSR 330)和普通註解(JSR 250)規範. 這些規範的實現由開發人員可以用來替換Spring預設提供的機制.
Spring Framework 5.0 開始起, Spring要求Java EE 7以上(e.g. Servlet 3.1+, JPA 2.1+).同時使用新的Java EE 8(e.g. Servlet 4.0, JSON Binding API)以上的新api提供開箱即用.這就保證了Spring完全相容Tomcat8和9, WebSphere9, 還有JBoss EAP 7.
慢慢的,Java EE 在開發中的角色發生了變化. 早期Java EE 和 Spring建立的程式是被部署到應用程式伺服器上. 而今天, 歸功於Spring Boot, 應用程式以devops或雲的方式建立,使用內嵌的Servlet容器, 而且經常變化.自從Spring Framework 5 , WebFlux程式甚至都不需要直接呼叫Servlet Api了, 它可以執行在非Servlet規範的容器(如Netty)中.
Spring是持續革新和進化的. 超出Spring Framework, 有很多其他專案如Spring Boot, Spring Security,Spring Data,Spring Cload, Spring Batch,還有很多. 每個專案都有它自己的原始碼倉庫, 問題跟蹤和釋出週期. 從spring.io/projects 可以看到所有專案的列表.
3. 設計哲學
當你學習一個框架的時候, 不僅要知道它能幹什麼, 更重要的是知道它所遵循的原則. 下面是Spring Framework遵循的指導原則.
在所有層面提供選擇權. Spring允許你儘量延遲設計選擇. 例如, 你可以通過配置而不是修改程式碼就替換掉資料持久化的提供程式.這也同樣適用於其他基礎設施概念並能整合很多三方API.
容納不同的觀點. Spring 擁抱伸縮性, 它並不堅持認為事情應該就這樣做. 根據觀點不同, 它提供了廣泛的應用選擇.
保持強大的向後相容性. Spring演化經過精心設計和管理, 可以防止版本之間出現破壞性改變. Spring支援一定範圍版本的JDK和第三方庫. 便於維護依賴於Spring的程式和庫.
關心API設計. Spring團隊花費大量精力和時間設計API, 使其直觀並且能保持多個版本和持續很多年.
高質量的編碼, Spring強調有意義的, 實時的,準確的javadoc. 是極少數聲稱程式碼簡潔且包之間沒有迴圈依賴的專案之一.
4. 反饋和貢獻
對於如何操作或診斷或除錯問題, 我們強烈建議使用StackOverflow, 並且我們有一個問題頁清單, 列出了使用建議. 如果你完全確定Spring Framework有問題或者想提交特性, 請使用JIRA問題跟蹤.
如果你解決了問題或者修正了建議, 你可以在GitHub上發起PR. 總之,請記住, 除了微不足道的問題,我們希望有個問題存根進行討論並記錄下來供未來參考.
更多問題請參看頂級頁面"貢獻"頁上的指南.
5 入門
如果你剛開始使用Spring, 你可能想要通過建立一個Spring Boot的專案開始Spring之旅. Spring Boot提供了一個快速(也是固化的)方式建立生產就緒的 Spring 程式, 它基於Spring 框架, 信奉約定優於配置,並且設計為快速啟動執行.
你可以使用start.spring.io來生成基礎專案, 或者按照"入門"指南一步步建立, 例如"Getting Started Building a RESTful Web Service". 這些指南只關注於當前主題任務, 可以比較容易的理解, 很多都是Spring Boot專案. Spring portfolio還包含其他專案, 當你解決特定問題時你可能會考慮關注下相關的專案.
核心技術
這部分指導文件涵蓋了Spring Framework不可或缺的所有技術
這些技術中最重要的,是Spring Framework的Ioc容器. 在吃透了Spring Framework 的IoC容器之後,緊接著是理解Spring的AOP技術. Spring Framework有其自身的AOP框架, 概念上很好理解並且能夠滿足實際應用中80%的熱點需要.
Spring提供了AspectJ整合(這是目前特性最為豐富,當然也是Java企業領域最成熟的AOP實現).
1. IoC容器
本章涵蓋Spring的IoC容器.
1.1 介紹Spring IoC容器和Beans
本節涵蓋了Spring Framework對IoC原則的實現. DI是與其密切相關的另一個概念. IoC是一個處理過程,通過這個過程,物件只能通過建構函式引數, 工廠方法引數或在從工廠方法構造或返回的物件例項上設定的屬性來定義他們的依賴關係. 當建立這些bean時, 容器去注入這些依賴. 這個過程從根本上反轉了由物件自身去控制它所需依賴的方式, 通過直接類構造或類似Service Locator模式的機制.
org.springframework.beans
和 org.springframework.context
這兩個包是IoC容器的基礎. BeanFactory
介面提供了能夠管理任何物件型別的高階配置機制. ApplicationContext
是 BeanFactory
的一個子類介面. 增加以下功能:
- 易於與Spring的AOP特性整合.
- 訊息資源處理(國際化)
- 事件釋出
- 應用程式層次的特定上下文,例如:在Web程式中的
WebApplicationContext
.
簡言之, BeanFactory
提供了配置框架和基本的功能, ApplicationContext
增加了諸多企業特性功能. ApplicationContext
是 BeanFactory
的一個完整超集, 在本章中專門用於Spring IoC容器的描述. 如果想用BeanFactory
代替ApplicationContext
可以參看後面有關BeanFactory
的內容.
Spring中,構成你程式的骨架並且被Spring IoC容器管理的物件被稱為beans. bean就是一個被Spring IoC容器例項化,裝配和管理的物件. bean也可以簡單的是應用中諸多物件中的一個.bean和他們之間的依賴被對映到容器的配置後設資料中.
1.2 容器概覽
org.springframework.context.ApplicationContext
介面代表了Spring IoC容器並且負責例項化,配置,組裝bean. 容器通過讀取配置後設資料獲取指令來例項化,配置,組裝bean.配置後設資料使用XML,Java註解或者Java程式碼的方式表現.它允許您表達組成應用程式的物件以及這些物件之間豐富的依賴.
Spring提供了ApplicationContext
介面的幾個實現. 在獨立應用中, 通常會建立一個ClassPathXmlApplicationContext
或FileSystemXmlApplicationContext
的例項.XML是傳統的定義配置的格式, 你也可以通過一小段XML配置來啟用這些支援的格式, 指定容器使用Java註解或者程式碼格式配置.
在很多的應用場景下, 並不需要顯式的例項化一個或多個Spring的IoC容器. 例如, 在Web應用中,web.xml
檔案中大概八行類似的樣板化的XML描述就足夠了(參看Web程式中便捷的ApplicationContext例項). 如果你使用Spring Tool Suite(一種Eclipse增強開發環境), 能夠很輕鬆地用幾次點選滑鼠和幾個按鍵生成這樣的樣板配置.
下圖從較高層次展示了Spring如何工作. 你的程式類和配置後設資料時結合在一起的, 因此,當ApplicationContext
建立並例項化後, 你就有了一個可執行系統或程式.
1.2.1 配置後設資料
如同上圖展示的, Spring IoC 容器使用配置後設資料. 配置後設資料表現了你作為開發者如何告知Spring容器去例項化,配置並組裝程式中的物件.
配置後設資料以傳統而直觀的XML格式提供, 這是本節大部分內容傳達的關於Spring IoC容器的關鍵概念和特性.
XML不是唯一允許描述後設資料的格式. Spring IoC 容器已經弱化了配置以何種格式書寫. 當今,許多開發人員更願意在程式中選擇Java配置的方式.
如何使用其他格式的配置,可以參考下面的資訊:
- 註解配置: Spring 2.5引入了註解配置支援
- Java配置: 從Spring3.0開始, Spring JavaConfig專案中的一些特性已經成為Spring Framework的核心. 因此,你可以使用Java而不是XML檔案擴充套件你的應用. 要使用這些新特性, 請參看
@Configuration
,@Bean
,@Import
,@DependsOn
註解.
Spring配置由至少一個,或典型的超過一個由容器管理的bean的定義. XML格式使用<bean/>
元素配置這些beans, 它巢狀在頂層<beans/>
元素裡面. Java配置則包含在使用@Configuration
註解的class中,並使用@Bean
註解方法.
這些bean定義與構成你程式的物件相吻合. 例如, 你定義服務層物件,資料訪問層物件,變現層物件如Struts Action
例項, 基礎物件如Hibernate SessionFactories
, JMS 佇列等. 一般不會在容器中定義細粒度的域物件.因為這通常是DAO或業務邏輯層的任務去建立和載入這些物件. 儘管如此, 你可以使用AspectJ去配置在容器之外建立物件.參看在Spring中使用AspectJ依賴注入領域物件.
下面例子展示了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="..." class="...">
<!-- collaborators and configuration for this bean go here
1. id是區分bean的一個字串識別符號
2. class 定義bean的型別,使用全限定類名
-->
</bean>
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>
id
屬性的值指向協作的物件. 本例中沒有明確寫出.可參看依賴項.
1.2.2 例項化容器
ApplicationContext
構造引數中的定位引數字串是讓容器從外部變數載入配置. 引數可以是多種資源格式, 例如本地檔案系統, Java CLASSPATH
等.
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
學習Spring容器後, 你可能想要了解下Spring的Resource
抽象, 它提供了一種便捷的從URI格式的資源中讀取流的機制. 尤其是,Resource
路徑通常用來構建程式上下文, 這點可參看"程式上下文和資源路徑"
下面例子展示了服務層物件的配置檔案:
<?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>
接下來的例子展示了資料訪問層配置:
<?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>
前面例子中, 服務層由PetStoreServiceImpl
類和兩個資料讀取物件JpaAccountDao
和JpaItemDao
(根據JPA物件關係對映標準). name
指類中的屬性,表示bean的名稱, ref
元素指向另一個bean定義. 在id
與ref
元素之間的聯絡表明了物件間的協作依賴關係. 關於物件依賴配置的更多細節, 參看"依賴項".
結合XML格式的配置
使用多個xml檔案定義bean是有用的. 通常各自的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
必須是與匯入檔案處在相同路徑目錄下. 就像你看到的, '/'可以忽略. 雖然路徑是相對的,但儘量不要使用'/'. 匯入的這些檔案的格式必須符合Spring的Schema, 需要有頂級<beans/>
元素. 必須是有效的XML的bean定義.
可以但不提倡在父級目錄中使用'../'引用檔案. 這樣會在當前應用程式外建立依賴檔案.特別不提倡使用classpath:
URLs(例如,classpath:../services.xml
),執行時解析時會選擇最近的根路徑並且轉到它的父目錄.Classpath的配置可能會錯誤地引導到其他的目錄下.
可以使用絕對路徑替代相對路徑,例如file:C:/config/services.xml
或classpath:/config/services.xml
. 但這樣就不靈活地將路徑耦合到程式配置中了.一般的做法是可以使用一種間接方式-例如佔位符"${...}", 這樣系統JVM可以在執行時解析到正確路徑上
名稱空間本身提供了匯入指令特性. 比純bean定義更高階的配置特性Spring也有提供. 例如context
和util
名稱空間.
Groovy的Bean定義DSL
外部化後設資料配置的更高階例子, bean定義也可以使用Spring的Groovy Bean Definition DSL, 因Gails框架而熟知. 下面演示了".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配置相同, 甚至支援XML名稱空間. 可以通過importBeans
指令從XML檔案匯入bean定義.
1.2.3 使用容器
ApplicationContext
介面是一個能管理註冊的bean以及他們之間依賴的高階工廠. 通過方法T getBean(String name, Class<T> requiredType)
, 可以獲取Bean的例項.
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風格的不同上下文實現(同時也支援XML). 下面展示了Groovy配置:
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
最靈活的變數是GenericApplicationContext
, 其中包含讀取器代理, 例如: 對於XML檔案, 它使用XmlBeanDefinitionReader
讀取. 如下例:
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();
對於Groovy, 可是使用GroovyBeanDefinitionReader
, 如下所示:
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
在相同的ApplicationContext
中可以混合使用這些讀取代理器, 從而從不同資源中讀取配置.
可以使用getBean
獲取bean的例項. ApplicationContext
介面還有一些其他的方法獲取bean, 但是理想狀態下, 你的程式應該永遠不要使用它們. 確實, 你的程式程式碼壓根不應該呼叫getBean
方法,因此就一點也不會依賴Spring API. 例如, Spring為多種Web框架的元件整合提供DI功能, 例如controller和JSF管理的Bean, 允許你通過後設資料宣告依賴的bean(類似包裝(autowiring)註解).
1.3 Bean 概覽
Spring IoC 容器管理一到多個bean. 這些bean是根據你提供給容器的配置建立的. (例如, 通過XML格式的<bean />
定義)
在容器內部, 這些bean定義表現為BeanDefinition
物件. 其包含如下資訊 :
- 包含包名的類名: 典型地,bean定義的實際實現類;
- Bean行為配置元素, 標記bean在容器中行為(作用域scope, 生命週期回撥等);
- bean的協同或依賴的其他bean的引用.
- 最近建立物件的其他配置資訊,例如當使用bean時連線池的池大小或連結數
後設資料被解析為一系列組成bean定義的屬性, 下面表格列出了這些屬性:
表 1. Bean定義中的屬性
屬性 | 參看 |
---|---|
Class | 初始化bean |
Name | 命名bean |
Scope | Bean的作用域 |
Constructor arguments | 依賴注入 |
Properties | 依賴注入 |
Autowiring mode | 自動裝配協作物件 |
Lazy initialization mode | 懶載入Bean |
Initialization method | 初始化回撥 |
Destruction method | 銷燬回撥 |
bean定義包含如何建立特定bean, 除此之外ApplicationContext
的實現允許將容器外建立的bean註冊進來. 這是通過getBeanFactory()
方法訪問ApplicationContext的 BeanFactory , 該方法預設返回DefaultListableBeanFactory
實現. DefaultListableBeanFactory
支援通過registerSingleton(..)
和 registerBeanDefinition(..)
方法註冊. 儘管可以這樣做,應用程式一般還是單純使用規則的bean定義後設資料.
bean後設資料和單例的手工支援必須儘早註冊, 這是為了容器能夠在自動裝配和其他自省階段合理解析.重寫已經存在的後設資料以及已經存在的單例在某些級別上是支援的, 但執行時註冊新的bean(與工廠的併發訪問)沒有得到正式的支援,而且可能導致併發訪問異常或bean狀態不一致,或兩者都有.
1.3.1 命名Bean
每個bean都有一個或多個識別符號. 容器內這些識別符號必須是唯一的. 一般一個bean只有一個識別符號. 但是也可以有多個,多出來的識別符號是別名.
XML配置中,id
或name
屬性用來做識別符號. id
用來精確指定一個id. 按照習慣, 這些名稱是字母數字組成的('myBean', 'someService', etc.), 但他們也可以包含特殊字元. 如果你想給bean指定別名,你可以將他們賦值給name
屬性, 用逗號,分號或者空格分割. 在Spring3.1之前, id
是定義為一個xsd:ID
型別, 只能是字母. 自從3.1開始將其定義為xsd:string
型別. 注意id
的唯一性依然是容器強制的, 而不是XML解析器.
給beanname
或id
屬性不是必須的. 如果沒有指定, 容器會為bean生成一個唯一名稱. 儘管這樣, 如果你想通過名稱引用bean, 或者通過ref
元素或者是Service Locator風格的查詢, 你就必須給bean指定名稱. 不給bean指定名稱的動機是使用內部類和自動裝配.
bean命名約定
給bean命名遵循與給例項域命名相同的約定. 也就是,使用小寫字母開頭的駱駝命名法. 例如:
accountManager
,accountService
,userDao
,loginController
等.
堅持命名bean可以使你的配置易讀易理解. 還有, 如果使用Spring AOP, 當給名字相關的一些列bean應用通知時也會有很大幫助.
掃描類路徑時, Spring會給未命名元件生成名稱, 遵循前面描述的規則: 本質上是取類名,然後將首字元小寫. 當有多餘一個字母並且第一個和第二個字母都是大寫時將保留大小寫. 這些規則定義在java.beans.Introspector.decapitalize
(Spring使用)
在Bean定義之外新增別名
在bean定義內, 你可以給它指定多個名稱, 可以使用給id
指定一個名稱, 同時也可以給name
指定多個(使用分隔符).使用這些名稱指定的bean都是等效的, 在某些情況下也是有用的, 例如: 讓應用程式中的每個元件通過使用特定於該元件本身的bean名稱來引用公共依賴項。
然而在定義bean的時候為其指定別名有時候並不夠. 有時候需要將別名bean定義外的其他地方指定. 通常的例子是, 在大型系統內各個子系統分別有各自配置, 每個子系統有一組其bean的定義. XML的配置中,你可以使用<alias/>
元素實現. 如下:
<alias name="fromName" alias="toName"/>
本例中, bean的名稱(同一容器)被命名為fromName
, 在使用別名定義後, 這個bean通過toName
也可以引用.
例如, 子系統A引用了一個資料來源叫subsystemA-dataSource
. 子系統B引用資料來源叫subsystemB-dataSource
. 當主程式都使用這兩個系統時, 主程式引用了資料來源myApp-dataSource
. 這三個資料來源都指向相同的物件, 你可以將下面的別名配置新增到後設資料:
<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>
現在, 雖然每個元件和主程式都通過一個名稱引用了各自唯一的資料來源, 並且能保證不會與其他定義衝突(有效建立了名稱空間), 然而實際上他們引用的同一個物件.
Java 配置
如果使用Java配置,
@Bean
註解可以提供別名, 參看:如何使用@Bean
註解.
1.3.2 例項化Bean
bean定義的本質是建立物件的配方. 當需要時容器將檢視bean的配方, 並使用該bean的定義封裝的配置後設資料來建立(或獲取)實際物件.
如果使用XML配置, 要例項化的物件的型別是通過<bean/>
節點的class
屬性來指定的. class
屬性(對應到BeanDefinition
例項是Class
屬性)通常是強制的. (例外的情況請參看:使用工廠方法例項化,和Bean定義的繼承.) 有兩種使用Class
屬性的方法:
- 典型的, 直接指定class, 由容器通過構造器反射的形式直接建立bean. 有點等同於java程式碼的
new
操作. - 為包含建立物件的靜態工廠方法指定物件的實際類, 少數情況下容器通過靜態工廠方法建立bean. 被靜態工廠方法建立出來的物件可能是相同的型別或者壓根是另一個型別.
內部類名稱
如果你想要為一個靜態內部類配置bean, 你就必須使用內部類的雙名稱.
例如, 你有個類定義為
SomeThing
,在com.example
包下.SomeThing
有個靜態內部類為OtherThing
, 那它的class
屬性的值將是com.example.SomeThing$OtherThing
注意內部類和外部類之間需要使用
$
字母分割.
使用構造器例項化
當使用構造器建立bean時. 所有標準類都可用且都是可與Spring相容的. 也就是開發時不需要實現任何介面或者遵循特定的程式設計風格. 簡單定義為一個class即可. 儘管如此, 根據使用的IoC容器, 可能你需要定義一個預設構造器.
IoC容器可以管理任何你想要被託管的類. 它不僅限於管理JavaBeans. 大多數Spring使用者喜歡在屬性後定義getter和sertter模組. 你也可以在容器中定義非bean風格的類. 例如, 如果你想使用遺留程式碼中完全不遵循JavaBean規範的連線池, Spring也是可以管理的.
使用XML配置指定bean定義 如下:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
更多關於建構函式引數和物件構造後屬性的賦值, 參看:依賴注入.
靜態工廠方法例項化
當定義使用靜態工廠構建的bean時, 需要使用class
屬性指定包含靜態工廠方法的類, 並且使用factory-method
屬性指定工廠方法.你可以呼叫該方法(帶可選引數, 後面有表述)返回一個物件, 接著這個物件就可以像使用構造器建立出來的一樣使用了. 一種這麼使用bean定義的場景是在遺留程式碼中呼叫靜態工廠.
下面指定了通過呼叫靜態工廠方法建立bean的配置. 這個定義沒有指定返回型別, 只是指定了這個類包含靜態方法. 在本例中, createInstance()
方法必須是靜態方法. 下面例子展示瞭如何指定靜態方法:
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
下面展示了使用上述定義的類的程式碼:
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
有關靜態方法的引數和物件從工廠返回後屬性的賦值, 參看: 依賴和配置細節.
使用物件工廠方法例項化
與靜態工廠方法例項化類似.容器可以通過呼叫已存在的bean的非靜態工廠方法去建立bean. 要使用這種機制, 可以將class
留空, 並且在factory-bean
屬性指定當前(或父或祖先)容器中包含用來建立物件的工廠方法的bean. 使用factory-method
屬性設定工廠方法的名稱. 下面展示了怎麼配置:
<!-- 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管理和配置, 參看:依賴和配置細節.
Spring文件中,"factory bean"(工廠bean)是指Spring容器中配置的用來通過例項或靜態工廠方法建立物件的bean. 相比而言, FactoryBean
(注意大小寫)指Spring特有的FactoryBean
1.4 依賴
從此處開始升級到5.2.0版本
典型的企業應用不是由單個物件組成的(或用Spring語法來說的bean).就算是最簡單的程式也有一些終端使用者看起來相互合作的物件來呈現.接下來的這節闡述如何在一個真實的系統中定義一系列的bean, 從而讓他們協作達成目標.
1.4.1 依賴注入
依賴注入是一個處理過程, 在物件被構造後或者從工廠返回後, 僅僅通過建構函式引數, 工廠方法的引數或者屬性賦值的方式來定義他們的依賴(也就是與其他物件協作). 容器在建立bean後注入他們的依賴. 這個處理過程本質上是bean自己去使用類構造和服務定位模式管理初始化和定位它的依賴項的反轉(因此叫控制反轉).
使用DI原則的程式碼是清晰的, 並且做到了與提供的依賴項更有效地解耦. 物件不自己定位查詢依賴項, 也不知道依賴項的位置和型別.因此, 你的類就更容易被測試, 特別是依賴於介面和抽象類的情況下, 允許你單元測試中使用樁物件或模擬實現.
DI有兩個主要的變種: 建構函式依賴注入和屬性Setter依賴注入.
建構函式依賴注入
建構函式注入是通過容器呼叫有若干引數的建構函式完成的, 每個引數代表一個依賴項. 呼叫帶參的靜態工廠方法構造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...
}
注意這個類沒啥特別之處. 它本身就是一個沒有實現容器相關介面,基類或使用註解的普通POJO.
構造引數解析
構造引數是通過型別解析匹配的. 如果bean的構造引數沒有潛在的二義性, 那麼在bean中定義的引數順序就是bean初始化時的引數順序. 看下面的程式碼:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
假設ThingTwo
和ThingThree
沒有繼承關係, 沒有潛在的二義性. 因此, 下面的配置能很好的起作用, 在<constructor-arg/>
元素中你不需要制定構造引數的索引或明確制定其型別.
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" 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>
記住, 要不受限制的使用此功能, 你的程式碼必須要啟用debug標記編譯, 這樣Spring才能從建構函式查詢引數名稱. 如果不能或不想啟用debug標記, 可以使用@ConstructorProperties
JDK註解顯式新增到建構函式的引數上. 看起來如同下面例子:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
Setter方式的依賴注入
Setter方式的注入是呼叫無參建構函式例項化bean或靜態工廠方法返回bean之後, 再由容器呼叫bean的setter方法.
下例展示了只能用純setter方式進行注入的類定義. 這個類是傳統的java. 是一個沒有實現容器相關的介面,基類或新增註解的POJO.
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...
}
ApplicationContext
支援建構函式注入和Setter注入. 也支援通過建構函式注入部分依賴後再由Setter注入. 你使用BeanDefinition
類的形式配置依賴, 這個類與PropertyEditor
例項協作將屬性從一種格式轉化為另一種格式. 儘管如此, 大多Spring使用者不會直接使用這些類(在程式設計方面), 而是使用XML bean
定義, 或者註解註釋元件(也就是使用@Component
,@Controller
等),或者用@Configuration
註解的類中使用@Bean
的java程式碼配置. 這些資源將在內部轉化為BeanDefinition
例項並用於在IoC容器例項載入.
構造器注入還是Setter注入
因為可以同時混用構造器和Setter注入, 一種比較好的原則是: 強制的依賴使用構造器注入, 可選的依賴則可以使用setter或配置方法. 注意: setter方法上使用
@Required
註解將使得對應屬性成為必須的依賴.
Spring團隊提倡構造器注入, 這會讓你實現的程式元件是不可變物件並且可以保證依賴不是
null
. 更多的好處是, 構造器注入返回給客戶端的元件總是完全被初始化的狀態. 還有個邊緣效應, 就是大量的建構函式引數是一種不好的程式碼味道, 暗示著這個類承載了太多的責任並且需要被合理的重構, 實現關注點分離.
Setter注入應該在依賴是可選的前提下優先使用, 這些依賴可以被賦以合理的預設值. 另外, 非null驗證必須要在程式碼使用依賴的所有地方進行. 使用setter注入的一個好處是: setter方法使得物件的重配置和重注入更易於控制. 通過JMX MBeans管理就是一種setter注入的案例.
使用DI使得特定類更加有意義. 有時候, 當你使用三方類庫時, 你並沒有原始碼, 此時你將手握DI選擇權. 例如, 如果三方類沒有暴露任何setter方法, 那麼使用構造器注入將是唯一途徑.
依賴解析過程
容器按照下面闡述的對bean依賴進行解析:
ApplicationContext
被建立, 並且使用配置的bean後設資料進行初始化. 配置後設資料可以是XML,Java code或註解.- 對於每個bean, 它的依賴以屬性,建構函式引數或者靜態工廠的引數(如果使用代替普通的建構函式)形式表現. 這些依賴將在bean被建立後提供給bean.
- 每個屬性或引數是要被設定的值的定義, 或者是容器中另一個bean的引用.
- 每個屬性或引數的值是由特定的格式轉換到實際型別的. 預設情況下, Spring能夠將字串格式轉化為所有內建型別如:
int
,long
,String
,boolean
等.
Spring容器在建立後對每個bean的配置進行校驗. 儘管如此, 在bean被建立後bean的屬性才會被賦值. 單例域的並且設定為預例項化(預設情況)的bean在容器建立後被建立. 域的定義參看bean的作用域. 除此之外, bean只有在被請求時才建立. 一個bean的建立會潛在地導致bean的整個圖被建立, 也就是bean的依賴,它的依賴的依賴等都被建立和分配. 注意: 依賴解析的不匹配可能會後期表現出來, 也就是第一次建立受影響的bean時.
迴圈依賴
如果用占主導地位的構造器注入, 就可能會導致無法解析的迴圈依賴.
例如: 類A需要類B,通過構造器引數注入, 相反類B也需要類A,通過構造器注入. 如果配置A,B相互注入給對方, Spring的IoC容器就會在執行時檢測到迴圈引用, 並丟擲
BeanCurrentlyInCreationException
.
一種處理這種情況的方法是編輯原始碼, 將一個的setter注入改為構造器注入. 相應地避免使用構造器注入而僅僅使用setter. 換句話說, 雖然不提倡, 但可以使用setter注入配置迴圈依賴.
不同於典型的案例(即沒有迴圈依賴), 在bean A和bean B之間的迴圈依賴將迫使bean在完全例項化自身前將其注入給對方(這是典型的雞生蛋蛋生雞的場景).
總體上你可以信任Spring去做正確的事情. 它在容器載入期間檢測配置問題, 如不存在的bean或迴圈依賴.當bean被真正建立後, Spring會盡量延後設定屬性和解析依賴. 這意味著在容器正確載入後, 如果你請求的bean有問題或它的依賴有問題,可能會丟擲異常--例如, bean丟擲缺失或無效屬性的異常. 這種潛在的配置問題的延遲導致的不可見性就是為什麼ApplicationContext
的實現預設會預例項化單例bean. 這種會導致前期一些時間和記憶體消耗的代價能換來配置問題的及時發現, 就是在ApplicationContext
被建立時, 而不是以後呼叫bean時. 你也可以覆蓋這種預初始化的配置為延遲初始化.
如果沒有迴圈依賴, 當一個或多個協作的bean被注入到依賴的bean裡面, 每個協作bean其實是優先於依賴bean就被完全配置好了. 這意味著如果A依賴B, 容器會在呼叫A的setter方法之前完全配置好B. 換句話說,這個bean被例項化了(如果它不是預例項化的),它的依賴被設定了, 並且他的生命週期函式(如配置的初始化函式或初始化bean回撥函式)也被呼叫了.
DI的例子
下例使用XML配置setter形式的DI. 一小段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"/>
下面展示了對應的ExampleBean
類:
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/>
元素包含在bean定義元素中.
純值資料(原始資料,String等型別)
元素<property/>
的屬性value
可以指定為書寫或構造引數的純值資料. Spring的轉化服務用來將這些值從String
轉化為合適的型別. 下例展示了一組這樣的配置:
<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-
名稱空間展示更簡潔的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 Tool Suite). 而這樣的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 容器轉化內部<value/>
元素的內容為java.util.Properties
的例項, 這是通過使用JavaBeans的PropertyEditor
機制來實現的. 這是一個捷徑, 也是幾種Spring團隊喜歡使用巢狀<value/>
而不是value
屬性風格的情況之一.
idref
元素
idref
元素僅是一種防止錯誤的方法, 可以將容器中的另一個bean的id(一個字串,不是引用)傳遞給屬性或構造引數. 如下所示:
<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名為client
的targetName
屬性的值不會被校驗. 僅僅是在client
被實際例項化的時候會發生型別轉化(大多數情況下是嚴重錯誤). 如果client
bean是個原型bean, 則只有在部署容器很久之後才會發現錯誤和由此產生的異常.
idref
元素的local
屬性不再支援4.0版本的beans XSD, 因為它不再提供常規bean
引用的值. 當升級到4.0時, 請修改現有的idref local
引用, 修改為idref bean
.
<idref/>
元素帶值的一個地方(至少在Spring2.0之前的版本中)是在ProxyFactoryBean
定義中AOP攔截器的配置. 當你為了防止拼寫錯攔截器ID而指定攔截器名稱時使用<idref/>
元素.
引用其他bean(協作者)
ref
元素是在<constructor-arg/>
或者 <property/>
中的不可更改的元素. 在這裡, 你將另一個由容器管理的bean(協作者)作為引用的值賦給一個bean的屬性. 被引用的bean作為引用被賦值給這個bean的屬性, 並且它是在被賦值前就按需初始化的. (如果這個協作者是個單例的話,它已經被容器初始化了).所有引用最終都是另一個物件的引用. 作用域和校驗則取決於你是否通過bean
,local
,partent
屬性為另一個物件指定ID或名稱.
通過<ref/>
tag的bean
屬性指定目標bean是常見的方式, 並且允許其在同一個容器或父容器中建立任何bean的引用. 不管是不是配置在XML格式的檔案. bean
屬性的值可以是目標bean的ID後者是name中的任一值. 下面展示了ref
元素:
<ref bean="someBean"/>
通過parent
屬性建立的引用指定目標bean是在當前容器的父容器. 其值可能是目標bean的id或name的其中任一值. 目標bean必須在當前容器的父容器中. 主要使用這個屬性的場景是: 當你使用了有層次的容器並且在父容器中通過代理proxy包裝了一個同名的父bean. 下面是一對使用parent
的例子:
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>
4.0的beans XSD 後
ref
元素的local
屬性不再支援, 因此不需要用它為一個正常的bean
引用提供值了. 當升級到4.0時請注意修改現有的ref local
到ref bean
.
內部的bean
<bean/>
元素如果定義在<property/>
或者<constructor-arg/>
內部, 則表示定義了內部bean, 如下所示:
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
內部bean定義不需要指定ID或name, 如果指定了, 容器也不會使用它們作為bean的識別符號. 容器也會在建立時忽略它們的scope
標記, 因為內部bean通常都是匿名的, 並且總是跟外部bean一起建立. 一般不可能去單獨的訪問內部bean, 或者將他們注入到協作bean而不是包裝bean中.
作為旁例, 從一個自定義的scope中獲取到銷燬回撥是可能的, 例如對於一個單例bean中作用域為request-scope的內部bean. 內部bean的建立是與外部bean的建立是繫結的, 但是銷燬回撥使它特定於request生命週期. 這並不是一個普遍的場景, 內部bean一般是與包含它的bean有著相同的作用域.
集合
<list/>
,<set/>
,<map/>
和 <props/>
元素分別
對應於Java集合型別(Collection
)的List
, Set
, Map
, 和 Properties
. 下例展示其用法:
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>
字典map的鍵值,或者集set的值, 可以是下面元素的任一:
bean | ref | idref | list | set | map | props | value | null
集合合併
Spring容器支援合併集合. 開發人員可以定義一個父的<list/>
,<set/>
,<map/>
和 <props/>
元素,並且子元素的<list/>
,<set/>
,<map/>
和 <props/>
可以繼承和覆蓋父集合的值. 也就是說, 子集合的值是父集合與子集合元素的合併, 子集合的元素覆蓋了父集合中的值.
本節討論父子bean的合併機制. 讀者要是不瞭解父子bean的定義可以參看相關章節,然後再回來.
下例展示了集合的合併:
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the child collection definition -->
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>
注意merge=true
的使用, 它在beanchild
的<props>
元素中名為adminEmails
. 當child
被容器解析和初始化後, 最終的例項將有一個adminEmails
的Properties
集合, 包含了合併父集合與子集合中adminEmails
的全部元素. 下面展示了結果:
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
子Properties
集合的值繼承了父<props/>
, 並且子集合中support
值覆蓋了父集合中的值.
這種合併行為也同樣類似於<list/>
, <map/>
, 和 <set/>
等集合型別. 在<list/>
元素的特定情況下, 其語義與List
集合型別相關聯(也就是一個一系列值的有序集合). 父集合的值優先於子集合的值. 在Map
, Set
, 和 Properties
型別情況下, 元素間不存在順序. 因此,容器內部使用構築在Map
,Set
,Properties
實現型別之上的無序集合型別.
集合合併的限制
不能合併不同的集合型別(如Map
和List
進行合併). 如果這樣做, 會丟擲相應異常. merge
屬性必須在繼承或者較低的子定義上. 指定在父集合定義上的merge
是多餘的,也不會產生期望的合併.
強型別集合
從Java5的泛型集合開始, 你可以使用強型別的集合了. 也就是宣告一個值包含(例如)String 型別的元素集合成為可能. 如果使用Spring的DI去注入一個強型別集合, 你可以得到Spring型別轉化支援, 將先前新增到Collection
的元素轉化為正確的型別. 下例展示了用法:
public class SomeClass {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
<beans>
<bean id="something" class="x.y.SomeClass">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>
當beansomething
的account
屬性準備注入時, 它的泛型型別資訊被反射獲取到. 因此, Spring的型別轉化基礎設施辨別出元素的值是Float
, 並且將字串值(9.99,2.75 和 3.99
)轉化為實際的Float
型別.
Null和空字元值
Spring將properties的空引數視為空字串. 下面的XML配置片段設定email
屬性為空值("").
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
上面的例子等效於下面的Java程式碼:
exampleBean.setEmail("");
<null/>
元素處理null值. 如下所示:
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
上面程式碼等效於:
exampleBean.setEmail(null);
XML的p-名稱空間
p-名稱空間可以在bean
元素的屬性中使用,(而不是巢狀的<property/>
元素), 可以用來描述屬性值或協作bean.
Spring支援用名稱空間擴充套件配置格式, 這基於XML架構定義. 這節討論的bean
配置格式定義在XML架構文件中. 儘管如此, p-名稱空間沒有定義在XSD檔案中, 值存在於Spring的核心.
下面展示了兩段XML(第一段是標準的XML, 第二段是p-名稱空間), 他們有相同的結果:
<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 name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="someone@somewhere.com"/>
</beans>
這個例子展示了bean定義中有個p-名稱空間叫email
. 這實際是告訴Spring有個屬性宣告. 如前面提到的, p-名稱空間沒有架構定義, 因此你可以用屬性的名字設定標籤屬性名稱.
下面展示了兩個bean定義中同時引用了另一個bean.
<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 name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>
這個例子包含了不止是用p-名稱空間的一個屬性, 而且使用了宣告屬性引用的格式. 第一個bean定義使用<property name="spouse" ref="jane"/>
建立了從beanjohn
到jane
的引用, 第二個定義則使用了p:spouse-ref="jane"
來完成相同的事情. 本例中, spouse
是屬性名, 同時-ref
表明這不是一個表值而是對另一個bean的引用.
p-名稱空間不如標準xml格式靈活. 例如, 宣告屬性引用的這種格式與使用
ref
的格式衝突. 而標準的XML則不會. 我們強烈建議你細心選用合適的方式, 與團隊溝通來避免產生同時使用三種格式的XML文件.
XML的c-名稱空間
類似於p-名稱空間. 從Spring3.1開始, c-名稱空間允許將構造引數配置在行內, 而不是單獨巢狀的<constructor-arg/>
元素內.
下例使用c:
名稱空間實現與建構函式注入相同的事情:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>
<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>
</beans>
c:
名稱空間使用與p:
名稱空間相同的約定來設定建構函式引數(對於引用使用-ref
字尾).相似的, 需要宣告在XML檔案中, 雖然在XSD架構中未定義(它存在於Spring的核心中).
對於少數建構函式名稱不可用的情況(通常是沒有debug資訊的已編譯二進位制檔案),可以使用引數索引, 如下:
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="something@somewhere.com"/>
鑑於XML語法, 索引必須以
_
開頭, 因為XML屬性名稱不能以數字開頭(雖然一些IDE可以). 相應的,元素<constructor-arg>
中也可使用索引數字,但不常用, 因為宣告時的順序已經足夠了.
實踐中, 建構函式解析機制對於匹配引數已經足夠了, 所以除非你真正需要, 我們建議使用名稱標記貫穿整個程式.
複合屬性名稱
在設定bean屬性時可以使用複合或者巢狀的屬性名稱. 只要路徑下所有元件不為null
即可. 如下定義:
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
something
bean有個fred
屬性, fred
又有個bob
屬性, bob
有個sammy
屬性, 最終sammy
屬性被賦值為123
. 為了能使其起作用, 在bean被構建後, fred
和其bob
必須不為null
.否則NullPointerException
將被丟擲.
1.4.3 使用depend-on
如果一個bean是另一個bean的依賴, 也就意味著一個bean會作為另一個bean的屬性值. 在XML配置中你使用<ref/>
元素來配置. 但有時候bean之間的依賴關係不是直接的. 舉個例子, 一個類的靜態初始化器需要被觸發,比如對於資料庫驅動註冊. depends-on
屬效能夠顯式的迫使一個或多個bean的初始化, 這發生在使用了這個元素的bean初始化之前. 下例使用depends-on
展示只有一個bean的依賴:
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
對於多個依賴, 可以為depends-on
屬性指定多個值(用分號,空格, 逗號分隔)
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
depends-on
屬效能夠指定初始化期間的依賴. 僅在單例bean中, 指定相應的銷燬期依賴. 用depends-on
關係定義的依賴bean將被首先銷燬, 優先於它自身修飾的bean. 因此,depends-on
可以用來控制關閉順序.
1.4.4 延遲載入的bean
預設情況下,ApplicationContext
實現會立馬建立和配置所有單例bean, 作為其初始化步驟的一部分. 通常,預初始化時令人滿意的, 因為配置和環境錯誤可以被及時發現, 而不是經過幾小時,幾天. 當這種行為不令人滿意時, 你可以通過將bean定義為延遲載入而阻止預初始化發生. 一個延遲載入的bean告知IoC容器,這個bean是在第一次請求時建立,而不是容器啟動時.
XML中, 通過<bean/>
元素的lazy-init
屬性控制這種行為. 如下:
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>
當上面的配置被ApplicationContext
處理時, 其啟動時lazy
bean不會被立即初始化, 而not.lazy
bean將立即被初始化.
儘管如此, 當一個延遲初始化bean是一個非延遲初始化bean的依賴時, ApplicationContext
在啟動時建立了延遲bean, 因為它必須滿足單例的依賴. 延遲初始化bean被注入到非延遲的單例bean中了.
也可以在容器級別通過<beans/>
元素的default-lazy-init
屬性控制延遲載入行為. 如下所示:
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
1.4.5 自動裝配
Spring容器能夠自動裝配協作bean之間的關係. 你可以讓容器通過檢查ApplicationContext
中的內容自動解析協作bean. 自動裝配有如下好處:
自動裝配可以顯著減少指定屬性和構造引數的需要. (對於本章其他地方討論的bean模板等機制也是有價值的).
自動配置可以隨著物件發展而更新配置. 例如, 如果需要向類新增依賴,則可以自動滿足依賴而不需手工配置. 因此,自動裝配在開發階段很有用, 不會在程式碼變得更穩定時拒絕切換到顯式書寫的選項.
當時用XML配置時(參看依賴注入), 你可以通過<bean/>
元素的autowire
屬性為一個bean指定自動裝配模式. 自動裝配功能有四種模式. 你可以任選其一. 下表描述了這四種模式:
表2. 自動裝配模式
模式 | 說明 |
---|---|
no | (預設)不使用自動裝配. bean的引用必須使用ref 元素. 對於較大部署不推薦修改這個模式設定. 因為顯式指定的協作者提供了各大的控制權和可讀性. 某種意義上是一個系統的架構文件. |
byName | 通過屬性名稱自動裝配. Spring通過名稱查詢需要自動裝配的bean. 例如: 如果一個bean定義按名字自動裝配, 並且包含了一個master 屬性(也就是同時有setMaster(..) 方法), Spring會查詢名為master 的bean定義, 並且將其設定到屬性. |
byType | 如果容器中有匹配property型別的bean就自動裝配. 如果多於一個, 將丟擲致命異常, 這表明可能不應該使用byType 匹配. 如果沒有匹配的bean, 則忽略(屬性不被賦值) |
constructor | 與byType 類似但是是提供給建構函式引數的. 如果容器中沒有精確型別的bean, 致命錯誤將發生 |
通過byType
和constructor
裝配模式, 你可以裝配陣列和泛型集合. 這種情況下, 容器中所有型別匹配的候選物件都將提供以滿足依賴. 對於Map
,如果key的型別是String
,你就可以自動裝配. 一個自動裝配的Map
例項的值是由所有匹配型別的bean組成的, 這個例項的key包含對應bean的名稱.
自動裝配的限制和不足
自動裝配使用在貫穿整個專案的過程中能工作得很好. 如果不是普遍使用, 而只是使用到一兩個bean上時會搞得開發人員頭暈.
參考自動裝配的限制和不足:
property
和constructor-arg
設定的顯式依賴總是會覆蓋自動裝配. 不能自動裝配簡單型別,String
和Class
(或者這些型別的陣列). 這個限制是專門設計的.比起顯式裝配, 自動裝配精確度較低. 雖然, 正如前面表格提到的, Spring非常小心地避免在多個期望結果下導致的二義性進行猜測. Spring管理的物件間的關係以及不是顯式文件定義的了.
裝配資訊可能是不可用的, 對於從Spring容器生成的文件的工具而言.
容器內多個bean定義可能會匹配到自動裝配的setter型別或構造引數型別.對於陣列,或者
Map
例項, 這不是一個問題. 然而對於期望單一值的依賴, 這種二義性不能被隨意處理, 如果沒有唯一bean可用,異常將會丟擲.
對於後面的幾種場景, 你可能有如下幾個選擇:
拋棄自動裝配, 擁抱顯式裝配
設定bean的
autowire-candidate
為false
以防止自動裝配, 正像下一節描述的.通過設定
<bean/>
元素的primary
屬性為true
將其定義為優先匹配物件.使用有更多細粒度控制的註解驅動配置, 如在註解配置中描述的.
從自動配置中排除bean
在單個bean級別, 你可以從自動裝配中排除bean.在XML配置中, 可以通過設定bean的autowire-candidate
為false
. 容器能使得特定bean排除在自動裝配之外(包括註解格式的配置如@Autowired
).
autowire-candidate
屬性被設計為僅對基於型別的裝配有效. 對於通過name引用的顯式引用無效, 即使指定的bean沒有被標記為候選也會被解析. 結果就是當名字匹配時, 通過name自動裝配仍然會注入bean.
你可以通過基於bean名稱的模式匹配去限制bean的自動裝配. 根級別元素<beans/>
通過屬性default-autowire-candidate
接收一個或多個模式.例如:限制名稱以Repository
結尾的bean的候選狀態, 可以使用模式*Repository
. 可以通過逗號分隔多個模式. bean元素上的autowire-candidate
屬性的值true
或false
總是有優先權. 對於這些bean, 模式規則不生效.
這些技術對於從沒想要通過自動裝配注入的bean是有用的. 這並不意味著被排除的bean自己不能通過自動裝配所配置, 而是它本身將不會作為bean的候選裝配給其他bean.
1.4.6 方法注入
大多數應用場景中, 容器中的很多bean都是單例的. 當一個單例的bean需要與另一個單例bean協作, 或者一個非單例bean需要與另一個非單例bean協作時, 一般需要通過將一個bean作為另一個bean的屬性來處理依賴關係. 當bean的生命週期不同時將會發生問題. 假設一個單例bean A需要一個非單例(原型)bean B, 也許每個方法都有呼叫. 容器只建立A一次, 因此只有一次機會設定它的屬性. 一旦有用到, 容器不能總是使用B的新例項提供給A.
一種解決方案就是拋棄依賴注入. 你可以使一個Bean A 通過實現介面ApplicationContextAware
被容器所感知, 並且a通過getBean("B")
請求容器每次都獲得到b的新例項.下面程式碼演示了這種方法:
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
上面程式碼並不令人滿意, 因為業務程式碼與Spring框架耦合在一起了. 方法注入, 是一種Spring容器更為高階的特性, 能夠讓你更聰明滴處理此類問題.
你可以從這篇部落格中獲取到更多方法注入的動機.
查詢方法注入
查詢方法注入是重寫容器管理的bean並查詢另一個容器中的命名bean將其返回的能力. 查詢一般是一個原型bean, 就像在前面章節討論的. Spring框架通過CGLib庫的二進位制程式碼動態生成子類重寫方法.
為了能讓動態的子類工作, 需要為其生成子類的bean不能是
final
, 同時需要覆蓋的方法也不能是final
.進行單元測試時, 如果有
abstract
方法的類需要你自己去定義子類並且提供abstract
方法的樁實現.具體方法也是需要能元件掃描的, 這就需要獲取具體類.
另一個關鍵的限制查詢方法和工廠方法,特別是與配置類中的
@bean
註解方法不相容. 這種情況下, 容器不再控制建立例項因此也就不能在執行時建立子類.
在前面的CommandManager
類的程式碼片段中, Spring容器動態覆蓋實現了createCommand()
方法. 類CommandManager
沒有任何Spring依賴, 如下所示;
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
在客戶端程式碼中包含了需要注入的方法, 這個被注入的方法需要如下的格式:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是abstract
修飾的, 子類將實現這個方法, 否則動態生成的子類將重寫定義在源類中的實際方法. 如下所示:
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
id定義為commandManager
的bean呼叫其createCommand()
方法, 在其需要myCommand
例項的時候. 你必須小心地部署beanmyCommand
為原型bean. 如果它是一個單例, 則每次返回的都是相同的例項.
也可以使用元件註解的方式, 你可以通過@Lookup
註解在方法上, 如下所示:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
或者, 更規範地, 你可以信任目標bean通過返回的型別解析得到目標bean.
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
注意通常你需要用一個樁實現來宣告這種帶註釋的查詢方法, 這樣他們才能與Spring元件掃描相容, 預設情況下, 抽象類會被掃描忽略. 這個限制不適於顯式註冊或顯式匯入bean類.
另一個訪問不同作用域的方法是使用
ObjectFactory
/Provider
連線點. 參看: 不同作用域的bean依賴.
你可以發現
ServiceLocatorFactoryBean
(在包org.springframework.beans.factory.config
)是很有用的.
任意方法替換
一種比查詢方法注入不那麼有用的形式是在一個管理bean中使用另一個方法實現去替換方法. 你可以跳過這節, 直到你需要這種機制再回來看.
使用XML配置的後設資料時, 你可以使用replaced-method
元素替換一個已經存在的方法. 請看下面的類定義, 它有個方法computeValue
需要被重寫:
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
實現介面org.springframework.beans.factory.support.MethodReplacer
提供了一個新的方法定義,如下所示:
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
需要部署的源bean定義和需要覆蓋的方法應該按如下方式組合:
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
你可以為元素<replace-method/>
需要覆蓋的方法簽名指定一個或多個<arg-type/>
元素. 只有類中的方法被過載並且有多個的時候才需要引數簽名. 為了方便, String型別的引數可以只是一個縮寫, 例如,下面的寫法都匹配java.lang.String
java.lang.String
String
Str
因為引數的個數經常能區分出可能的選擇, 因此這種縮寫能節省大量輸入時間, 通過一個短字串來匹配一個引數型別.
1.5 Bean 作用域(scope)
當建立bean定義的時候, 實際上你就有個通過bean定義去建立類真實例項的配方. bean定義是配方的想法是非常重要的, 因為它意味著,你可以通過一個配方去建立多個例項.
通過bean的定義, 你不僅可以控制插入到物件的依賴和配置值, 還可以控制通過bean定義的物件的作用域. 這種方式是強大而靈活, 因為你可以選擇通過配置生成的物件的作用域, 而不是必須在class級別操作物件的作用域. bean可以定義為幾個作用域其中的一個. Spring框架支援六種作用域, 四種僅可以用在web型別的ApplicationContext
中. 你也可以自定義scope.
下面表格描述了支援的作用域:
表 3. Bean 作用域
Scope | Description |
---|---|
singleton | (預設)對於每個IoC容器來說, 處理每個bean定義的物件例項僅有一個 |
prototype | 處理一個bean定義可以有多個物件例項 |
request | 處理對於每個HTTP請求僅有一個例項. 也就是對於每個bean定義, 每個HTTP請求都有它自己的例項. 僅僅在Web型別的Spring ApplicationContext 中是可用的. |
session | 處理在一個HTTPSession 範圍內一個bean的定義. 僅僅在Web型別的Spring ApplicationContext 中是可用的. |
application | 處理在ServletContext 級別的bean的定義. 僅僅在Web型別的Spring ApplicationContext 中是可用的. |
websocket | 處理在WebSocket 級別的bean的定義, 僅僅在Web型別的Spring ApplicationContext 中是可用的. |
從Spring3.0開始, 執行緒級別的作用域是可用的, 但不是預設註冊的. 關於更多請參看
SimpleThreadScope
的文件. 關於如何註冊這個作用域或者其他自定義作用域的指導, 請參看自定義作用域.
1.5.1 單例作用域
容器內僅僅有一個共享的bean例項, 並且所有通過bean定義中的id或者id列表僅能匹配出唯一的特定bean的例項.
換個說法, 當你定義了一個bean,並且將其設定為singleton作用域時, Spring IoC容器建立了bean定義的唯一例項. 這個唯一例項是儲存在此類單例bean的快取中的, 並且所有子請求和引用都會返回快取的bean. 下面的圖展示了單例bean如何工作:
Spring單例bean的概念不同於GOF模式書中的單例模式. GoF單例硬編碼了物件的作用域, 所以對於每個ClassLoader,有且僅有一個bean例項被建立. Spring的單例bean作用域對於每個容器每個bean來說有且僅有一個. 這意味著, 如果你在每個容器中定義一個指定的bean, 容器將為每個bean的定義生成一個且僅有一個bean的例項. 單例作用域是Spring預設的. 要用XML定義一個單例bean, 你可以參看下面定義:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
1.5.2 原型作用域
非單例的原型作用域bean將在每次請求的時候建立一個新的例項. 也就是這個bean被注入另一個bean或者通過容器的getBean()
方法呼叫獲取. 原則上, 使用需要保持狀態的bean時使用原型作用域, 使用狀態無關的bean時使用單例bean.
下圖說明了Spring的單例作用域:
(DAO物件不是典型的原型作用域, 因為一個DAO不保持任何會話狀態. 我們重用單例的說明圖是很容易的)
下面例子定義了XML格式的原型bean
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
同其他作用域相比, Spring不會管理原型bean的完整生命週期. 容器除了將其例項化,配置,另外還有組裝和將其提供給client外, 不會有更多的原型例項的任何記錄了. 因此, 雖然初始化回撥方法由所有作用域的物件都會呼叫, 但在原型模式來說, 配置的銷燬回撥方法不會被呼叫. 客戶端程式碼必須清理原型物件並釋放被其佔用的任何昂貴的資源. 為了使Spring容器能獲取到原型bean佔用的資源, 嘗試使用自定義的post-處理器, 這個處理器維護這需要被清理的bean引用.
在某些方面, Spring容器對於原型bean的作用是對new操作符的代替. 過去所有的生命週期管理都是客戶端來維護. (關於Spring中bean生命週期的更多資訊, 參看:生命週期回撥)
1.5.3 擁有原型bean依賴的單例bean
當使用有原型bean做為依賴的單例bean時, 記住依賴是在例項化的時候解析的. 因此,如果你將一個原型bean注入單例bean, 那這個原型bean是作為新的bean被初始化了, 並且注入到了單例bean中. 這個原型bean是單例bean的獨佔例項.
儘管如此, 假設你想要在執行期間為單例bean多次重複獲取原型bean. 你不能講原型bean注入到單例bean中, 因為注入只發生了一次, 就在容器初始化單例bean並解析他的依賴的時候. 如果你需要一個執行時的原型bean, 參看:方法注入.
1.5.4 Request,Session,Application, 還有webSocket
request
,session
,application
,websocket
作用域只有在web型別的Spring ApplicationContext
(比如XmlWebApplicationContext
)實現中可用.如果在標準的Spring容器中使用這些作用域, 比如ClassPathXmlApplicationContext
, 則IllegalStateException
將會因未知作用域而丟擲.
初始化Web配置
為了支援bean的這幾個作用域(request
,session
,application
,websocket
(web作用域bean)), 在定義bean時還需要做少量的配置. (對於標準的作用域singleton
和prototype
,初始化設定是不需要的)
如何完成初始化設定取決於你特定的Servlet環境.
如果你使用Spring web MVC訪問作用域bean, 實際上,請求是由Spring的DispatcherServlet
處理的, 不需要其他的設定. DispatcherServlet
已經暴露了所有的相關狀態.
如果你使用Servlet 2.5 的Web容器, 當不使用DispatcherServlet
處理時(比如使用JSF或Struts), 你需要註冊org.springframework.web.context.request.RequestContextListener ServletRequestListener
. 對於Servlet 3.0+, 可以通過介面WebApplicationInitializer
程式設計完成. 或者作為替換,包括使用舊的容器的話, 在web.xml
檔案中新增如下宣告:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
或者, 如果監聽器設定有異常, 考慮使用Spring的RequestContextFilter
. filter對映依賴於包含其的web程式配置, 所以你必須合理修改下. 下面列出web程式的部分配置:
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
DispatcherServlet
,RequestContextListener
,還有RequestContextFilter
都做了同樣的事情, 即繫結Http請求物件到服務請求的Thread
. 這使得bean在呼叫鏈中可以使用request-和session-作用域.
Request 作用域
參考如下XML配置bean:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring容器使用loginAction
定義為每個HTTP請求建立bean LoginAction
的例項. 也就是loginAction
bean在HTTP請求級別作用域. 你可以隨意修改例項的內部狀態,因為被loginAction
bean定義例項化出來的其他物件看不到這些修改. 他們是特定於單獨的request的. 當請求完成處理後, request作用域的bean就被拋棄了.
當使用註解驅動的元件或java程式碼配置時, @RequestScope
註解能用來分配給一個request
作用域的元件. 下面例子展示瞭如何使用:
@RequestScope
@Component
public class LoginAction {
// ...
}
Session 作用域
參看下面XML配置的bean定義:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring容器通過userPreferences
bean的定義為每個HTTP Session
生成UserPreferences
bean的例項. 換句話說, userPreferences
bean 在 HTTP Session
作用域級別. 跟request作用域bean一樣, 你可以隨意修改例項的內部狀態, 而其他由同一個userPreferences
bean定義生成的 HTTP Session
的例項不會看到這些變化, 因為他們特定於單獨的 HTTP Session
. 當 HTTP Session
最終不再使用時, 對應的Session作用域的bean也就不再使用了.
當使用註解驅動元件或java程式碼配置時, 你可以使用@SessionScope
註解到session
作用域的相關元件上.
Application 作用域
考慮下面XML的bean定義:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring容器使用appPreferences
bean的定義建立了AppPreferences
bean的例項. 也就是appPreferences
bean是在ServletContext
級別的, 並且作為一個標準的ServletContext
屬性儲存. 這類似於Spring單例bean, 但有兩點重要的不同: 它是在每個Servlet
上的單例, 不是在每個Spring'ApplcationContext'上的單例(可能在任何web程式中有多個),並且實際上它是暴露的並且因此是作為ServletContext
屬性可見的.
當使用註解驅動或者java程式碼配置時, 你可以使用@ApplicationScope
註解到application
元件上. 如下所示:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
作用域bean作為依賴
Spring容器不僅管理物件(beans)的初始化, 也包含其組裝協作者(依賴項). (舉例來說)如果你想要將一個HTTP request作用域的bean注入到另一個更長生命週期的bean中, 你可以選擇注入一個AOP代理物件代替bean. 也就是說, 你需要使用暴露了與作用域物件相同介面的代理物件注入, 這個代理物件能夠從相關作用域中獲取到真實物件(例如一個HTTP請求)並且委託呼叫真實物件上的方法.
在單例bean之間你也可以使用
<aop:scoped-proxy/>
, 通過一箇中間代理引用, 這個代理是可序列化的,因此能在反序列化時重新獲取目標單例bean.
當在一個
prototype
作用域的bean上宣告<aop:scoped-proxy/>
時 , 每個請求共享代理物件的方法將會將其引導到新建例項上並呼叫.
同時, 在作用域安全的方式下, 從一個短作用域訪問bean, 作用域代理並不是唯一的方法. 你也可以用
ObjectFactory<MyTargetBean>
宣告你的注入點(也就是,構造器或setter引數或者自動裝配域), 允許通過getObject()
在需要物件時獲取當前例項-- 而不是持有其例項或單獨儲存之.
作為擴充套件的變體, 你可以宣告
ObjectProvider<MyTargetBean>
, 這個物件有幾個訪問方式的變體, 包含getIfAvailable
和getIfUnique
.
JSR-330 中這被叫做
Provider
並且用來與Provider<MyTargetBean>
一起使用, 並且用get()
方法獲取. 關於JSR-330更多請參考這裡.
下面例子中只有一行, 但理解其在背後的原因比怎麼做更重要:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/> <!--這裡定義了代理-->
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.something.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
為了建立代理, 需要插入一個子元素<aop:scoped-proxy/>
到作用域bean的定義中. (參看建立代理的型別選擇和XML配置方式). 為什麼定義在request
,session
或者自定義作用域上的bean需要<aop:scoped-proxy/>
? 考慮下面單例bean的定義並對比上面表述的作用域(注意下面userPreferences
bean定義是不完整的):
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
上面例子中, 單例bean(userManager
)注入了HTTP Session
-作用域的bean中(userPreferences
). 這裡的突出點是userManager
是單例的: 它是每個容器唯一的, 並且它的依賴(在本例中是userPreferences
bean)僅僅注入了一次. 這意味著userManager
bean都是在相同的userPreferences
物件上進行操作的(也就是最初被注入的那個).
這不是你想要的結果, 當將一個短生命週期的bean注入到長生命週期的bean中時(例如, 注入一個HTTP Session
作用域的協作bean到單例bean中). 相反, 你需要一個單例的userManager
物件, 並且對於HTTPsession
生命週期, 你需要userPreferences
物件並將其指定為HTTP Session
. 因此, 容器建立了一個暴露了與UserPreferences
類相同公共介面的物件.(理想情況下這個物件是UserPreferences
例項), 這個物件能夠從作用域機制中(HTTP request,session等)獲取到真實的物件. 容器將代理物件注入到userManager
bean, 它並不知道UserPreferences
引用是個代理. 本例中, 當UserManager
例項呼叫注入物件UserPreferences
的方法時, 實際上它是呼叫代理的方法. 代理然後獲取從HTTP Session
作用域(本例中)的真實UserPreferences
物件, 並呼叫真實物件上的方法.
因此, 你需要下面的配置(完整而正確), 當你注入request-
和session-
作用域的bean時, 如下:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
選擇建立的代理型別
預設情況下, 當Spring容器用標記為<aop:scoped-proxy/>
元素的bean建立代理時, 一個CGLIB類代理物件就被建立了.
CGLIB代理僅攔截公共呼叫! 不呼叫代理上的非公共方法. 他們不會被代理到目標作用域物件上.
或者, 你可以使用標準JDK基於介面的代理為作用域bean配置容器. 通過將元素<aop:scoped-proxy>
的屬性proxy-target-class
設定為false
. 使用JDK基於介面的代理意味著你不需要在classpath下引用多餘的庫. 儘管如此, 這也意味著作用域bean必須實現至少一個介面並且所有注入的的作用域bean必須通過介面引用. 下例展示瞭如何使用介面代理:
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.stuff.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
關於選擇基於類的還是基於介面的代理, 請參看: 代理機制.
1.5.5 自定義作用域
bean的作用域機制是可擴充套件的. 你可以自定義或者重新定義已經存在的作用域, 雖然稍後會知道這不是最佳實踐而且你不能重寫內建的singleton
和prototype
作用域.
建立自定義作用域
為了整合自定義的作用域到容器中, 你需要實現介面org.springframework.beans.factory.config.Scope
, 這個介面將會在本節描述. 對於如何實現自定義作用域的觀念, 參看Spring框架實現的Scope
實現還有Scope
java文件, 裡面解釋了更多需要實現的方法.
Scope
介面有四個方法, 用來從作用域獲取,移除,銷燬物件.
以session作用域的實現為例, 返回session作用域bean(如果不存在,方法在這個例項將在繫結到session後以備將來引用, 然後返回一個新例項).下面的方法從潛在作用域返回物件:
Object get(String name, ObjectFactory objectFactory)
以session作用域的實現為例, 從潛在的session作用域刪除bean. 物件應該被返回, 但如果指定名稱的bean找不到, 你可以返回null. 下面的方法將從潛在作用域刪除物件:
Object remove(String name)
下面的方法為作用域註冊了回撥, 將在它被銷燬或者當作用域內的指定物件銷燬時執行:
void registerDestructionCallback(String name, Runnable destructionCallback)
關於銷燬回撥, 可以參看java文件或者Spring作用域實現的程式碼.
下面方法包含了從潛在作用域獲取會話id:
String getConversationId()
這個識別符號在每個作用域都不同, 對於session作用域實現, 這個識別符號可以是session識別符號.
使用自定義作用域
在你編碼並測試一個或多個自定義作用域實現後, 你需要使Spring容器能夠感知到你的作用域. 下面方法是為Spring容器註冊一個新作用域的核心方法:
void registerScope(String scopeName, Scope scope);
這個方法是在介面ConfigurableBeanFactory
宣告的, 在Spring的大多數對於介面ApplicationContext
的實現中, 通過BeanFactory
屬性可以獲取到.
registerScope(..)
方法的第一個引數是相關作用域的唯一名稱. Spring內建的此類名稱是singleton
和prototype
. 方法的第二個引數是自定義Scope
實現的例項, 這個例項就是你想註冊和使用的.
假設你寫好了自定義的Scope
實現, 並且像下一個例子一樣註冊到了容器中.
下一個例子使用
SimpleThreadScope
, 其包含在Spring中但沒有預設註冊. 這個教程與你自定義的scope
實現是相同的.
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
你可以建立bean的定義, 並將自定義的scope實現依附其中. 如下:
<bean id="..." class="..." scope="thread">
自定義的Scope
實現不僅限於使用程式設計方式註冊, 你也可以使用宣告方式註冊, 通過CustomScopeConfigurer
類進行註冊, 如下所示:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="thing2" class="x.y.Thing2" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="thing1" class="x.y.Thing1">
<property name="thing2" ref="thing2"/>
</bean>
</beans>
當你在
FactoryBean
實現中使用<aop:scoped-proxy/>
時,FactoryBean
自身是作用域下的bean, 不是從getObject()
方法返回的物件.
1.6 自定義Bean的特性
Spring框架提供了一些介面供你自定義bean的特性. 本節按下面方式組織:
- 生命週期回撥
ApplicationContextAware
和BeanNameAware
- 其他
Aware
介面
1.6.1 生命週期回撥
為了與容器管理的bean的生命週期互動, 你可以實現Spring InitializingBean
和 DisposableBean
介面. 容器會為前者呼叫afterPropertiesSet()
,為後者呼叫destroy()
以使bean執行在bean初始化和銷燬時應該做的操作.
在現代Spring程式中,JSR-250 的
@PostConstruct
和@PreDestroy
註解被認為是獲取生命週期回撥的最佳實踐. 使用這些註解意味著你的bean不會耦合到Spring特定的介面中. 更多資訊請參看:使用@PostConstruct
和@PreDestroy
.
如果你不想用JSR-250註解但仍然想去除耦合, 考慮後設資料中使用
init-method
和destroy-method
.
在內部, Spring框架使用介面BeanPostProcessor
的實現來處理他發現的任何回撥介面並且呼叫合適的方法. 如果你需要自定義特性或者修改Spring沒有預設提供的其他生命週期行為, 你可以實現BeanPostPocessor
. 更多資訊請參看:容器擴充套件點.
除了初始化和銷燬回撥外, Spring管理的物件也可以實現Lifecycle
介面, 以便那些物件能參與容器自身生命週期驅動的啟動和停止過程.
生命週期回撥介面將在本節描述.
初始化回撥
org.springframework.beans.factory.InitializingBean
介面能夠在容器設定了bean的所有必要屬性後執行初始化工作. InitializingBean
介面只有一個方法:
void afterPropertiesSet() throws Exception;
我們強烈建議不要使用InitializingBean
介面,因為它不必要地耦合到了Spring中. 相反, 我們建議使用@PostConstruct
註解或者指定一個POJO 初始化方法. 如果使用XML配置, 你可以使用init-method
屬性指定一個沒有引數的方法名稱. Java程式碼配置的話, 你可以使用@Bean
上的 initMethod
屬性. 參看:獲取生命週期回撥. 參考下例:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {
public void init() {
// do some initialization work
}
}
上面的例子和下面的例子(由兩個清單組成)有相同的效果:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {
public void afterPropertiesSet() {
// do some initialization work
}
}
這樣,上面的頭兩個例子程式碼沒有與Spring耦合.
銷燬回撥
實現org.springframework.beans.factory.DisposableBean
介面使得一個bean能在包含它的容器銷燬時回撥. DisposableBean
介面只有一個方法:
void destroy() throws Exception;
我們建議你不要使用DisposableBean
回撥介面, 因為程式碼不必要的耦合到Spring了. 相反, 我們建議使用@PreDestroy
註解, 或者在bean定義中指定一個普通方法. 使用XML配置的話你可以使用元素<bean/>
上的destroy-method
屬性. 使用java程式碼配置的話, 你可以使用@Bean
註解上的destroyMethod
屬性. 參看獲取生命週期回撥. 示例如下:
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
上面的定義等同於下面的定義:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
上面的兩個定義與Spring程式碼沒有耦合.
你可以為
<bean>
元素分配給屬性destroy-method
一個專門的值(inferred
(推斷)), 這個值指示Spring自動去探測特定bean上的公共close
或shutdown
方法. (任何實現了java.lang.AutoCloseable
或java.oi.Closeable
的類將會匹配到) 你也可以為<beans>
元素分配給屬性default-destroy-method
一個特定值, 用來在所有bean上應用動作(參看預設初始化和銷燬方法). 注意, 這個java程式碼配置的預設行為.
預設初始化和銷燬方法
當你不用Spring特定的InitializingBean
和 DisposableBean
介面回撥的初始化和銷燬方法時, 你一般編寫方法的名稱類似init()
,initialize()
,dispose()
等. 理論上, 這些生命週期回撥方法是標準化的貫穿於整個專案, 這樣開發人員就能使用相同的方法並保持一慣性.
你可以配置Spring容器查詢每個bean上的命名初始化和銷燬方法. 這意味著你作為開發人員能編寫程式類並使用名為init()
的初始化方法回撥, 而並不需要為每個bean定義上指定init-method="init"
. Spring容器在bean被建立時呼叫這個方法(並遵循前面描述的標準生命週期回撥). 這個特性也需要強制執行一貫的初始化和銷燬方法回撥的命名約定.
假設你的初始化回撥方法叫init()
並且你銷燬回撥方法名叫destroy()
. 你的類將按下面例子阻止:
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// this is (unsurprisingly) the initialization callback method
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}
你可以像下面這樣使用那個類:
<beans default-init-method="init">
<bean id="blogService" class="com.something.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>
頂級元素<beans/>
上的屬性default-init-method
促使Spring容器去識別在bean上的init()
方法作為初始化回撥函式. 當bean被建立和組裝後, 如果bean有這麼個方法, 它就將在恰當的時候執行.
你也可以在頂級元素<beans/>
上使用default-destroy-method
, 類似地去配置(XML)銷燬回撥函式.
在已經存在的bean上, 已經有按約定命名的回撥方法, 你可以使用<bean/>
元素上指定init-method
和destroy-method
重寫預設的方法.
Spring容器擔保在bean的依賴全部被提供後能夠立即呼叫配置的初始化回撥. 因此,初始化回撥發生在原始bean引用上, 這意味著AOP注入還沒有應用到bean上. 一個完整的目標bean是首先被建立然後一個AOP代理注入鏈被引用. 如果目標bean和代理被單獨定義, 你的程式碼就能夠繞開代理與目標bean互動. 因此, 在init
方法上應用注入是不合邏輯的, 因為這樣將耦合目標bean的生命週期到它的代理或注入器, 並且當你的程式碼與原始的目標bean直接互動時留下奇怪的語義.
組合生命週期機制
從Spring2.5開始, 控制bean生命週期行為有三種選擇:
- 回撥介面
InitializingBean
和DisposableBean
- 自定義的
init()
和destroy()
方法 - 註解
@PostConstruct
和@PreDestroy
. 你可以組合這些機制去控制bean.
如果給一個bean配置了多種生命週期機制,並且每個機制都使用了不同的方法名, 那麼每個配置方法將會按本提示後面給出的順序執行. 儘管如此, 如果為多個機制配置了相同的方法名-- 例如, 初始化方法的
init()
-- 那該方法將只執行一次, 就如上面所闡述的.
為同一個bean配置多個生命週期機制, 初始化方法將按下面順序執行:
- 使用註解
@PostConstruct
的方法 - 通過回撥介面
InitializingBean
定義的afterPropertiesSet()
方法 - 自定義的
init()
方法
銷燬方法按相同的順序執行:
- 使用註解
@PreDestroy
的方法 - 通過回撥介面
DisposableBean
定義的destroy()
- 自定義的
destroy()
啟動和停止回撥
對於擁有自己生命週期需要的任何物件, 介面Lifecycle
定義了必不可少的方法(例如啟動和停止某些後臺程式).
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
任何Spring管理的物件都可以實現介面Lifecycle
. 然後, 當ApplicationContext
自身收到開始和停止訊號時(例如,執行時的停止/重啟場景), 它將級聯呼叫上下文定義的所有Lifecycle
實現. 這是通過委託給LifecycleProcessor
完成的, 如下所示:
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
注意, LifecycleProcessor
本身是介面Lifecycle
的擴充套件. 它新增了兩個方法在上下文重新整理和關閉時互動.
注意, 標準的
org.springframework.context.Lifecycle
介面是一個明確的規約, 是為了啟動和停止通知使用, 並沒有在上下文重新整理時應用自動啟動. 對於特定bean的自動啟動更細粒度的控制(包括啟動階段), 考慮實現介面org.springframework.context.SmartLifecycle
代替.
同時請注意, 停止通知不能保證一定在銷燬時會發生. 在標準的停止過程下,所有的
Lifecycle
bean在一般的銷燬回撥被傳播前首先接收一個停止通知. 儘管這樣, 在上下文生命週期熱重新整理時或者在拋棄重新整理嘗試時, 只有銷燬方法會被呼叫.
啟動和停止呼叫的順序可能是比較重要的. 如果兩個物件間存在依賴關係, 依賴方將在被依賴方之後啟動, 並且在被依賴方之後停止. 不過有時候直接依賴是不知道的. 你可能僅僅知道一些型別的物件的優先於另一些型別的物件啟動. 這種情況下, SmartLIfecycle
介面定義了另一種選項, 在其父類介面Phased
上指定getPhase()
方法. 下面的程式碼展示了Phased
介面:
public interface Phased {
int getPhase();
}
下面展示了SmartLifecycle
介面:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
當啟動時, 底層階段的物件優先啟動. 當停止時, 順序相反. 因此, 一個實現了介面SmartLifecycle
並且其方法getPhase()
返回Integer.MIN_VALUE
將會在第一個啟動的, 並是最後一個停止的. 在此範圍的另一端,也就是Integer.MAX_VALUE
將表明物件是最後啟動的並且是最先停止的(可能因為它依賴於要執行的其他程式). 當提到階段值的時候, 重要的一點是任何"正常"Lifecycle
物件如果沒有實現SmartLifecycle
介面的話,這個階段值的預設為0. 因此, 任何負值的物件將在那些標準元件之前啟動(在其後停止). 如果正值則相反.
通過SmartLifecycle
接收一個停止方法. 任何實現必須在物件的停止過程完成後呼叫回撥的run()
方法. 這就使程式擁有了非同步停止能力, 因為介面LifecycleProcessor
的預設實現DefaultLifecycleProcessor
在每個階段中等待該組物件的超時值以呼叫該回撥. 每個階段的超時時間是30秒. 你可以通過定義一個名為lifecycleProcessor
的bean來重寫預設的生命週期處理器. 如果你想僅僅是修改超時時間, 定義如下:
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- timeout value in milliseconds -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
就像前面提到的, LifecycleProcessor
介面也定義了上下文重新整理和關閉的回撥方法. 後者驅動關閉過程,就好像stop()
方法被顯式呼叫, 但它發生在上下文關閉時. 另一方面, 重新整理回撥賦予了SmartLifecycle
bean的另一種特性. 當上下文被重新整理(在所有物件建立和初始化後), 回撥就被呼叫了. 此時, 預設的生命週期處理器檢查每個SmartLifecycle
物件的isAutoStartup()
方法返回的bool值. 如果是true
, 物件將立刻啟動而不是等到上下文或者其自身的start()
方法顯式呼叫(與上下文重新整理不同,上下文啟動並不是標準上下文實現的標準實現). phase
值和任何依賴關係決定了前面描述的啟動順序.
在非WEB程式中優雅地停止Spring容器
這節內容僅僅應用於非web程式. Spring 基於web的
ApplicationContext
實現已經提供了當web程式關閉時優雅地關閉容器的程式碼.
如果你在一個非web程式中(例如, 一個胖客戶端桌面環境)使用Spring容器, 註冊一個JVM的停止鉤子. 這樣做可以保證你優雅地關閉, 並呼叫單例bean上關聯的銷燬方法使所有資源能夠釋放. 你必須依然要正確配置和實現那些銷燬回撥.
為了註冊停止的鉤子, 呼叫介面ConfigurableApplicationContext
宣告的方法registerShutdownHook()
, 如下所示:
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
public static void main(final String[] args) throws Exception {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// add a shutdown hook for the above context...
ctx.registerShutdownHook();
// app runs here...
// main method exits, hook is called prior to the app shutting down...
}
}
1.6.2 ApplicationContextAware
和 BeanNameAware
當ApplicationContext
建立了一個實現了org.springframework.context.ApplicationContextAware
介面的物件例項時, 這個例項就提供了對ApplicationContext
的引用. 下面程式碼是介面ApplicationContextAware
的定義:
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
因此, bean能夠通過程式設計方式操作建立了他們的ApplicationContext
, 通過ApplicationContext
介面或通過能轉化為這個介面子型別的物件(例如: ConfigurableApplicationContext
,這個介面暴露了更多的功能). 其中一個用途是對其他bean進行程式設計檢索. 有時候這種能力是有用的. 但總體上你應該避免使用, 因為它會耦合你的程式碼到Spring並且不能遵循IoC風格, 在那裡協作bean作為bean的屬性. ApplicationContext
的其他方法提供了訪問檔案,釋出程式事件,還有訪問MessageSource
等功能. 這些附加特性參見ApplicationContext
的附加功能.
自從Spring2.5開始, 自動裝配是另一種能獲取ApplicationContext
引用的替代方式. "傳統的"constructor
和byType
裝配模型(如同在裝配協作者一節描述的)能分別為構造器引數或setter方法引數提供ApplicationContext
型別依賴. 更多的靈活性, 包括自動裝配欄位和多引數方法, 使用新的基於註解的裝配特性. 如果你這麼做了, 帶有@Autowired
註解的域, 建構函式或方法, 那ApplicationContext
就自動裝配到一個期望ApplicationContext
型別的屬性域,建構函式引數, 或者方法引數中去了. 更多資訊請參見使用@Autowired
.
當ApplicationContext
建立了實現org.springframework.beans.factory.BeanNameAware
介面的類例項後, 這個類就擁有了在定義時為其指定的名字引用. 下面程式碼展示了介面BeanNameAware的定義:
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}
回撥在bean的屬性被佈局後但是在InitializingBean
, afterPropertiesSet
或自定義初始化方法之前被呼叫.
1.6.3 其他Aware
介面
除了(之前討論的)ApplicationContextAware
和BeanNameAware
外, Spring提供了一系列Aware
介面以使bean向容器表明他們需要特定的基礎設施依賴. 作為普遍法則, 這個名稱表明了依賴型別. 下面表格簡要列出最重要的Aware
介面:
表4 Aware介面
名稱 | 注入的依賴 | 解釋文件索引 |
---|---|---|
ApplicationContextAware | 宣告ApplicationContext | ApplicationContextAware and BeanNameAware |
ApplicationEventPublisherAware | 封裝的ApplicationContext的事件釋出 | ApplicationContext的附加功能 |
BeanClassLoaderAware | 用來載入bean的類載入器 | 例項化bean |
BeanFactoryAware | 宣告BeanFactory | ApplicationContextAware and BeanNameAware |
BeanNameAware | 宣告的bean的名稱 | ApplicationContextAware and BeanNameAware |
BootstrapContextAware | 容器執行其中的資源介面卡BootstrapContext, 一般僅JCA可用的ApplicationContext中可用 | |
LoadTimeWeaverAware | 載入時為處理類定義的載入器 | Spring框架中使用AspectJ載入時織入 |
MessageSourceAware | 配置的解析訊息的資源策略(支援引數化和國際化) | ApplicationContext的附加功能 |
NotificationPublisherAware | Spring JMX 提醒釋出者 | 提醒 |
ResourceLoaderAware | 底層訪問資源的配置載入器 | Resources |
ServletConfigAware | 容器執行其中的當前ServletConfig ,僅在web-aware的Spring ApplicationContext 中可用 |
Spring MVC |
ServletContextAware | 容器執行其中的當前ServletContext ,僅在web-aware的Spring ApplicationContext 中可用 |
Spring MVC |
請再次注意, 使用這些介面將使你的程式碼耦合到Spring API, 並且也不遵循IoC風格. 因此我們建議他們當需要訪問容器時使用基礎設施bean.
1.7 Bean定義繼承
bean的定義包含很多配置資訊, 包含建構函式引數, 屬性值, 還有特定於容器的資訊, 例如初始化方法,靜態工廠方法等. 一個bean的子類定義繼承了父定義中的配置資料. 子類定義可以覆蓋或者按需新增資料. 使用父子定義可以節省很多編碼. 事實上, 這是一種模型.
如果你使用程式設計方式使用ApplicationContext
介面, 子類bean的定義是通過ChildBeanDefinition
類來表現的. 許多使用者不在這個層面使用它. 相反, 他們通過在諸如ClassPathXmlApplicationContext
的類中生命配置bean. 當你使用基於XML的配置後設資料時, 你可以通過parent
屬性指明子類bean定義, 其值是父bean的定義名稱. 下例展示瞭如何這麼做:
<bean id="inheritedTestBean" abstract="true"
class="org.springframework.beans.TestBean">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<!--注意`parent`屬性-->
<bean id="inheritsWithDifferentClass"
class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBean" init-method="initialize">
<property name="name" value="override"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>
如果沒有指定,子類bean定義將使用父類定義的bean類,但依然可以覆蓋它. 後一種情況下, 子類必須相容父類(也就是必須接受父類的屬性值).
子類繼承了父bean的作用域,建構函式引數值, 屬性值還有方法重寫, 同時可以新增新的值給它. 任何你指定的作用域,初始化方法,銷燬方法或者static
工廠方法配置都將覆蓋掉父類的配置.
剩下的配置總是從子類定義獲取: 依賴,裝配模式,依賴檢查,單例, 懶載入.
上面例子中父類定義使用abstract
屬性顯式標記父bean是抽象的. 如果父bean不指定型別, 顯式標記父bean為abstract
是必須的, 如下所示:
<bean id="inheritedTestBeanWithoutClass" abstract="true">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBeanWithoutClass" init-method="initialize">
<property name="name" value="override"/>
<!-- age will inherit the value of 1 from the parent bean definition-->
</bean>
父bean不能被例項化自己, 因為它不完整, 且顯式標記為abstract
了. 當一個定義是abstract
的時, 它僅僅是為子定義提供的一個純淨的模板bean定義. 檢視嘗試單獨使用這個抽象bean, 或者將它引用為另一個bean的ref
屬性, 或者使用父bean的id執行getBean()
方法將返回錯誤. 相似的, 容器內部的preInstantiateSingletons()
方法將忽略定義為抽象的bean.
ApplicationContext
預設預初始化所有的單例. 因此, 重要的一點是(至少對於單例bean)如果你有個(父)bean定義, 並且僅僅想把它作為模板使用, 並且給他指定了類名, 你就必須保證設定了abstract屬性為true, 否則應用程式上下文將(嘗試)預例項化這個abstract
bean.
1.8 容器擴充套件點
通常, 攻城獅不需要ApplicationContext
的子類實現. 相反, Spring容器可以通過實現特定介面的方式擴充套件. 以下幾節描述了這些介面.
1.8.1 使用BeanPostProcessor
自定義bean
BeanPostProcessor
介面定義了一些回撥方法, 你可以實現提供你自己的(或者覆蓋預設的)初始化邏輯, 依賴解析邏輯等. 如果你想要在容器完成初始化, 配置和例項化bean之後實現一些自己的邏輯, 你可以插入一個或多個BeanPostProcessor
的實現.
你可以配置多個BeanPostProcessor
例項, 並能夠通過order
屬性的值來控制這些例項執行的順序.如果BeanPostProcessor
實現了Ordered
介面, 你就可以設定這個屬性了. 如果你寫了自己的BeanPostProcessor
, 你就也應該考慮實現Ordered
介面. 更多細節可參看javadoc關於BeanPostProcessor
和Ordered
介面的部分. 也可以參看: 程式設計註冊BeanPostProcessor
的例項.
BeanPostProcessor
例項作用在bean或物件上. 也就是Spring容器例項化一個bean例項後BeanPostProcessor
例項開始做他們的工作.
BeanPostProcessor
是容器級別的作用域. 僅僅是你使用容器層次結構的時候才是有意義的. 如果你在容器中定義了一個BeanPostProcessor
,它將僅僅處理這個容器中的bean. 換句話說, 定義在一個容器中的bean不能被其他容器中的BeanPostProcessor
處理, 就算這倆容器都是相同等級層次架構的一部分.
為了改變bean的定義(也就是定義bean的藍圖), 你需要使用
BeanFactoryPostProcessor
, 這在使用BeanPostProcessor
來自定義配置原資料中有描述.
org.springframework.beans.factory.config.BeanPostProcessor
介面包含兩個回撥方法. 當一個post-processor的bean被註冊到容器後, 對於每個本容器例項化出的bean, post-processor都將從容器獲取到回撥方法, 是在容器初始化方法之前(例如InitializingBean.afterPropertiesSet()
或者任何宣告為init
的方法)將被呼叫, 且在bean例項化回撥方法之後. post-processor可以做任何事情, 包括完全忽略回撥方法. 一個bean post-processor通常檢查回撥介面, 或者使用代理來包裝bean. 一些Spring AOP 基礎設施類就是實現為post-processor, 為了提供代理包裝邏輯.
ApplicationContext
自動探測在配置後設資料中任何實現了BeanPostProcessor
介面的bean. ApplicationContext
將作為post-processor註冊了這些bean, 以便晚些時候在bean建立的時候呼叫. bean post-processor能夠以和其他bean相同的方式部署到容器中.
注意當在配置類上使用@Bean
註解的工廠方法宣告BeanPostProcessor
時, 工廠方法的返回型別必須是實現型別本身, 或者至少是介面org.springframework.beans.factory.config.BeanPostProcessor
, 明確表示bean的post-processor屬性. 否則, ApplicationContext
不能在完全建立它時通過型別自動探測. 因為BeanPostProcessor
需要被早點例項化, 從而被用於其他bean的初始化過程中, 所以這種早期探測是至關重要的.
程式設計的方式註冊
BeanPostProcessor
例項
建議註冊
BeanPostProcessor
的方式就是前面提到的,通過ApplicationContext
自動檢測, 同時你也可以用程式設計的方式使用ConfigurableBeanFactory
的方法addBeanPostProcessor()
進行註冊. 當你需要評估註冊前的條件邏輯, 或者想要在一個層次結構中跨上下文拷貝post-processor時, 程式設計的方式就很有用了. 注意, 程式設計新增的BeanPostProcessor
例項並不關心Ordered
介面. 這裡, 是註冊的順序決定了執行的順序. 同時注意, 程式設計註冊的BeanPostProcessor
介面總是在那些自動檢測註冊的介面之前處理, 就算顯式指定了順序也是如此.
BeanPostProcessor
例項和AOP自動代理
實現了
BeanPostProcessor
介面的類是特殊的並且被容器區別對待. 所有BeanPostProcessor
例項和他們直接飲用的bean都是在啟動時例項化的, 作為ApplicationContext
啟動階段的一個特殊部分. 接下來, 所有BeanPostProcessor
例項將被以一種風格註冊並應用到所有容器的bean上. 因為AOP自動代理是作為BeanPostProcessor
自身的一個實現. 所以BeanpostProcessor例項和它們直接引用的bean都不符合自動代理的條件,因此,它們中沒有織入的方面。
對於這樣的bean, 你可能看到如下日誌資訊: Bean someBean 不符合所有
BeanPostProcessor
介面處理的條件(例如:不符合自動代理).
如果你有通過自動裝配或
@Resource
註解(可能會回到自動裝配)方式裝配bean到BeanPostProcessor
中, Spring在查詢型別匹配的依賴候選時, 可能會訪問到不期望的bean, 使得他們不符合自動代理或者其他bean post-processor處理. 例如, 如果你有個@Resource
的依賴項, 其中欄位或setter名稱並不直接對應到bean的宣告名稱, 並且不使用name屬性, 那麼Spring將按型別訪問其他bean來匹配它們.
下面的例子展示了在ApplicationContext
中如何編碼, 註冊和使用BeanPostProcessor
.
示例: BeanPostProcessor
-風格的Hello World程式
第一個例子是基本用法. 展示了一個自定義的BeanPostProcessor
實現呼叫每個容器建立的bean的toString()
方法並將結果列印到控制檯.
下面是程式碼:
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // we could potentially return any object reference here...
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
下面beans
元素使用了InstantiationTracingBeanPostProcessor
:
<?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:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">
<lang:groovy id="messenger"
script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
</lang:groovy>
<!--
when the above bean (messenger) is instantiated, this custom
BeanPostProcessor implementation will output the fact to the system console
-->
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>
</beans>
注意InstantiationTracingBeanPostProcessor
僅僅是宣告瞭. 它沒有名字, 並且應該它是個bean, 它能夠依賴注入給任何其他bean. (上面配置定義了個bean, 是使用Groovy指令碼的形式,Spring動態語言支援的細節請看:動態語言支援)
下面java程式執行上面的程式碼和配置:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = (Messenger) ctx.getBean("messenger");
System.out.println(messenger);
}
}
輸出如下:
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
示例: RequiredAnnotationBeanPostProcessor
使用回撥介面或註解與自定義BeanPostProcessor
實現協力是擴充套件Spring容器的常見做法. 一個例子是Spring的RequiredAnnotationBeanPostProcessor
, 一個BeanPostProcessor
的實現, 它是隨Spring發行附帶的, 能確保標記為任意註釋的beans上的javabean屬性是實際上配置為依賴注入的一個值.
1.8.2 使用BeanFactoryPostProcessor
自定義配置後設資料
下一個擴充套件點我們看一下org.springframework.beans.factory.config.BeanFactoryPostProcessor
. 這個介面的語法類似於其他的BeanPostProcessor
, 但有個主要不同: BeanFactoryPostProcessor
操作bean的後設資料. 也就是, Spring容器使用BeanFactoryPostProcessor
讀取配置後設資料並可能在容器例項化任何除BeanFactoryPostProcessor
之外的bean之前改變它.
你可以配置多個BeanFactoryPostProcessor
例項, 並可以通過order
屬性控制這些例項的執行順序. 雖然,你僅僅需要設定實現了Ordered
介面的BeanFactoryPostProcessor
的這個屬性. 如果你編寫了自己的BeanFactoryPostProcessor
, 你就應該考慮實現Ordered
介面. 更多細節參看:BeanFactoryPostProcessor
和Ordered
的java文件.
如果你想修改實際的bean例項(也就是通過配置後設資料建立的物件), 那你就需要使用
BeanPostProcessor
(之前章節討論的通過BeanPostProcessor
自定義bean). 雖然技術上可以使用BeanFactoryPostProcessor
與bean例項協作(例如, 通過使用BeanFactory.getBean()
), 但這樣做將導致早產的bean例項, 侵犯了標準的Spring容器生命週期.這樣容易導致副作用, 例如繞開bean處理.
同時
BeanFactoryPostProcessor
例項是容器級別的作用域. 這僅僅在使用容器層次結構的時候是有意義的. 如果你容器中定義了一個BeanFactoryPostProcessor
, 那他就只在這個容器定義的bean起作用.在一個容器中的bean定義不能被另一個容器中的BeanFactoryPostProcessor
處理, 就算兩個容器都是相同層次結構的一部分.
一個bean工廠的post-processor, 當它在ApplicationContext
中宣告時自動執行, 為了給定義在容器中的後設資料做出修改. Spring包含很多預定義的工廠post-processor, 例如PropertyOverrideConfigurer
和PropertySourcesPlaceholderConfigurer
. 你也可以使用自定義的BeanFactoryPostProcessor
--例如, 註冊自動以的屬性編輯器.
ApplicationContext
自動檢測那些部署到其中的, 實現介面BeanFactoryPostProcessor
的bean. 在恰當的時候, 它使用這些bean為bean工廠的post-processor. 你可以象其他bean一樣部署這些post-processor bean.
與使用
BeanPostProcessor
一樣, 你通常不想要配置BeanFactoryPostProcessor
為延遲載入. 如果沒有其他bean引用一個Bean(Factory)PostProcessor
, 那post-processor將不會被初始化. 因此, 將其標記為延遲初始化是會被忽略的, 並且Bean(Factory)PostProcessor
將會被更早的初始化, 就算你設定了<beans/>
元素的default-lazy-init=true
.
例子: 類的名字替換PropertySourcesPlaceholderConfigurer
通過使用標準的java Properties
格式, 你可以使用PropertySourcesPlaceholderConfigurer
來擴充套件獨立在檔案中的bean定義中的屬性值. 這麼做能使人們部署程式去自定義的特定環境的屬性, 例如資料庫URLs和密碼, 而不需要複雜的有風險的去修改主要的XML定義檔案.
請看下面的XML格式的配置後設資料片段, 裡面有個使用佔位符的DataSource
值被定義:
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>
<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
示例展示了從一個外部Properties
檔案中配置的屬性. 在執行時, PropertySourcesPlaceholderConfigurer
被用來替換資料來源中的一些屬性值. 需要替換的屬性是以一種${properties-name}
的格式指定, 遵循Ant和log4j以及JSP EL風格.
真正的值來自另一個標準的Properties
檔案:
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
因此, 字串${jdbc.username}
在執行時被'sa'替換, 並且其他的與鍵匹配的佔位符值都給替換了. PropertySourcesPlaceholderConfigurer
檢查properties佔位符和bean的定義屬性. 而且, 你可以自定義佔位符字首和字尾.
隨著Spring2.5版本的context
名稱空間, 你可以使用專門的配置元素去配置屬性佔位符. 你可以為location
屬性提供一個或多個用逗號分隔的值列表. 如下:
<context:property-placeholder location="classpath:com/something/jdbc.properties"/>
PropertySourcesPlaceholderConfigurer
不僅查詢指定的properties檔案. 預設情況下, 如果它不能從指定的屬性檔案中找到, 它會繼續查詢SpringEnvironment
屬性和標準的System
屬性:
你可以使用
PropertySourcesPlaceholderConfigurer
去替換類的名稱, 這有時候是有用的, 當你不得不在執行時選擇一個特定的類實現的時候. 下面展示了這種情況:
<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/something/strategy.properties</value>
</property>
<property name="properties">
<value>custom.strategy.class=com.something.DefaultStrategy</value>
</property>
</bean>
<bean id="serviceStrategy" class="${custom.strategy.class}"/>
如果這個類不能在執行時被解析為一個有效的類, 解析失敗發生在當它將要被建立時, 對於一個非延遲載入的bean, 也就是在
ApplicationContext
的preInstantiateSingletons()
階段.
示例: PropertyOverrideConfigurer
PropertyOverrideConfigurer
是另一個bean工廠post-processor, 類似於PropertySourcesPlaceholderConfigurer
, 但又不同於後者, 對於bean的properties, 最初的定義可以沒有預設值或者壓根就沒值. 如果一個覆蓋的Properties
檔案沒有bean的properties合適的入口, 那麼預設的上下文定義將被使用.
注意, bean的定義並不知道被覆蓋, 所以覆蓋配置被使用的xml定義檔案並不是立即就可見. 萬一有多個PropertyOverrideConfigurer
例項為同一屬性定義了多個, 那依據覆蓋機制最後一個將獲勝.
properties檔案配置的行格式如下:
beanName.property=value
下面是這種格式的示例:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb
示例檔案能被定義了一個名字為dataSource
的bean使用, 其中包含了driver
和url
屬性.
組合屬性名稱也是受支援的, 只要路徑path的每個被複寫的部分都是非空, 最終屬性除外(假設是使用構造器初始化的). 在接下來的例子中, beantom
的fred
屬性的bob
屬性的sammy
屬性被設定為123
.
tom.fred.bob.sammy=123
指定的覆寫的值總是字面量. 他們不會被解析為bean的引用. 當xml bean定義中指定一個bean的引用時,這個約定同樣被使用.
自從Spring 2.5的context
名稱空間開始, 就可以使用一個明確的配置元素去配置屬性了. 如下所示:
<context:property-override location="classpath:override.properties"/>
1.8.3 使用FactoryBean
自定義例項化邏輯
你可以為本身是工廠的類實現介面org.springframework.beans.factory.FactoryBean
.
FactoryBean
介面是可插入Spring容器例項化邏輯的一個點. 如果你有比較複雜的程式碼, 這種程式碼使用java比使用一對xml檔案更好的表達, 你就可以自己去建立自己的FactoryBean
, 將複雜的例項化程式碼寫進這個類, 並將這個類插入到容器中.
FactoryBean
介面提供了額三個方法:
Object getObject()
: 返回工廠建立的例項物件. 這個例項可能是共享的, 取決於工廠返回的是單例還是原型.boolean isSingleton()
: 如果這個FactoryBean
返回單例就是true
,否則是false
.Class getObjectType()
: 返回物件的型別, 這個物件是getObject()
方法返回的. 或者返回null
, 如果型別不能預知.
FactoryBean
的觀念和介面在Spring框架中多處使用. Spring自身提供的就總有50多個實現.
當你需要問容器請求介面FactoryBean
的真實例項本身, 而不是它生產的bean時, 在呼叫ApplicationContext
的方法getBean()
方法將返回在bean的id
前面冠以&
. 所以, 對於一個id為myBean
的FactoryBean
, 呼叫getBean("myBean")
將返回FactoryBean
的產品, 而呼叫getBean("&myBean")
返回FactoryBean
例項本身.
1.9 基於註解的容器配置
Spring中是否註解配置要優於XML的配置
基於註解的配置是否優於XML, 關於註解的介紹引出了這個問題. 簡短的答案是"看情況". 長一點的答案是:每種方式都有其優缺點, 經常地, 這取決於開發人員決定哪種方式更適合他們. 基於他們定義的方式, 註解提供了許多上下文, 這導致了更短而更閉聯的配置. 而XML更精通於不接觸程式碼或無需重新編譯而組裝元件.一些開發人員更喜歡將裝配接近原始碼的方式, 而其他人則認為註解的類不再是POJO了, 而且配置變得零散而難以控制.
不管何種選擇, Spring都能相容兩種方式甚至混用它們. 值得指出,通過java配置選項, Spring使得註解在一個非侵入方式下使用, 不需要觸碰目標元件的原始碼, 至於工具, 所有的配置方式都被Spring Tool Suite 支援.
一種替換XML配置的方式是基於註解的配置, 後者依賴於二進位制位元組碼原資料裝配元件而不是使用尖括號的宣告. 不同於XML描述裝配, 開發人員將配置轉移到元件類內部,通過註解在類上, 方法上或域欄位上. 就像在示例:RequiredAnnotationBeanPostProcessor
中一樣, 使用BeanPostProcessor
與註解協作是一種擴充套件Spring 容器的普遍方法. 例如, Spring2.0賦予了使用@Required
註解強制必填屬性的能力. Spring2.5使得使用相同的一般模式來驅動Spring DI 成為可能. 本質上, @Autowired
註解提供了在自動裝配一節的描述相同的能力, 但擁有更細粒度的控制和更廣的適用性. Spring2.5同樣新增了JSR-250註解的支援,例如@PostConstruct
和@PreDestroy
. Spring 3.0 新增了對JSR-330(java的依賴注入)註解支援, 包含在javax.inject
包下如@Inject
和@Named
. 這些註解的細節可以在相關章節找到.
註解注入在XML注入之前執行. 因此, XML配置將覆蓋了通過兩種方式裝配的註解.
通常, 你可以像分離的bean定義一樣註冊他們, 但你也可以隱式的通過包含在XML配置中的下面標籤來註冊. (注意將context
名稱空間包含進去):
<?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:annotation-config/>
</beans>
(隱式註冊的post-processor包含AutowiredAnnotationBeanPostProcessor
, CommonAnnotationBeanPostProcessor
, PersistenceAnnotationBeanPostProcessor
, 還有前面提到的RequiredAnnotationBeanPostProcessor
.)
<context:annotation-config/>
僅僅查詢相同應用上下文定義的bean上的註解. 這意味著如果你為一個DispatcherServlet
的WebApplicationContext
上新增標籤<context:annotation-config/>
, 它僅僅在你的Controller上檢查@Autowired
, 而不會在service上. 可參看:DispatcherServlet獲取更多資訊.
1.9.1 @Required
@Required
註解引用在bean的setter方法上, 如下:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
這個註解表明, 受影響的bean的屬性必須在配置時就確定, 不管是通過bean定義中的顯式的屬性值還是自動裝配. 如果沒有找到值,容器將丟擲異常. 這允許早期和顯式失敗, 避免了NullPointerException
等諸如此類的例項. 我們依然建議你將斷言放置在bean類內部(例如, 放在初始化方法裡面). 這樣就強制了必填屬性的引用和值, 就算你在容器外使用類也是如此.
@Required
註解在Spring framework 5.1開始起被否定了, 更傾向於必填設定通過建構函式(或使用隨bean屬性的setter方法InitializingBean.afterPropertiesSet()
的自定義實現) .
1.9.2 使用@Autowired
JSR 330 的註解
@Inject
能夠替換在本節例子中使用的Spring註解@Autowired
. 這裡(bean的標準註解)有更多細節.
你可以將@Autowired
註解應用到建構函式, 如下所示:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
從Spring 框架4.3 開始, 如果目標bean只定義了一個建構函式, 那麼
@Autowired
註解就不是必須的. 但如果有多個建構函式, 那至少需要註解其中的一個來告訴容器使用哪個.
你也可以將@Autowired
註解應用到傳統的setter方法上, 如下:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
你也可以將註解應用到具有任意名稱和多個引數的方法, 如下:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
也可以將@Autowired
應用到域並且可以和構造方法混用, 如下
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
private MovieCatalog movieCatalog;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
確保你的目標元件(如
MovieCatalog
或CustomerPreferenceDao
)和使用@Autowired
註解的地方是型別一致的. 否則,注入可能會因為執行時沒有匹配到型別而失敗.
對於XML定義的bean或元件類通過classpath掃描, 容器通常可以預知型別. 但對於
@Bean
工廠方法, 你需要確保返回型別要足夠明確. 對於那些實現了多個介面的元件, 或隱式的引用到具體實現型別的元件, 考慮在工廠方法返回值將其宣告為特定型別(至少要指定為與注入點引用的bean需要的型別).
你也可以為需要一個陣列的域或方法提供一組特定型別的bean, 這些bean從ApplicationContext
中獲取. 如下:
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;
// ...
}
同樣可以引用於集合類, 如下:
public class MovieRecommender {
private Set<MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
如果你想指定集合陣列中的元素順序, 你可以給目標bean實現
org.springframework.core.Ordered
介面或者使用@Order
或者標準的@Priority
註釋. 否則他們的順序遵循容器中定義的相關目標bean註冊的順序.
你可以在目標類級別上宣告
@Order
註解, 以及在@Bean
方法上, 這可能是單獨的bean定義(在使用同一個bean類的多個定義的情況下).@Order
值可能影響注入點的優先順序, 但要知道, 它不影響單例的啟動順序, 而這是由依賴關係和@DependsOn
宣告決定的正交關係.
注意標準的
javax.annotation.Priority
註解不能在@Bean
級別上使用, 因為它不能宣告在方法上. 它的語義可以通過@Order
值與每個型別的單一bean上使用@Primary
相結合來建模.
甚至型別化的Map
例項也是可以自動包裝的, 只要期待的key值是String
型別. Map的值包含了所有期望型別的例項, 而key值包含了相關bean的名稱, 如下所示:
public class MovieRecommender {
private Map<String, MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
預設情況下, 如果沒有候選bean, 自動裝配將會失敗(也就是預設必填). 預設的行為是將註解方法, 建構函式和域看做所需的依賴. 你也可以像如下所示的那樣去改變其行為:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired(required = false)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
每個類僅有一個建構函式可以被標記為必填, 但可以有多個註解的建構函式. 那種情況下, 每個都將被作為候選, 並且Spring使用最大引數的建構函式, 這個建構函式可以滿足依賴.
建議將
@Autowired
所需的必填屬性置於@Required
註解之上. 必填屬性表明屬性不是為了自動裝配必須要的. 屬性將被忽略如果它不能被自動裝配. 而@Required
, 另一方面它更強, 它可以以容器支援的任何方式設定屬性. 如果沒有值注入, 相關的異常將會丟擲.
作為替換, 你可以使用java8的java.util.Optional
來表達一個非必填屬性依賴. 如下所示:
public class SimpleMovieLister {
@Autowired
public void setMovieFinder(Optional<MovieFinder> movieFinder) {
...
}
}
從Spring 5.0 開始, 你也可以使用@Nullable
註解了(任何包中任何型別的註釋, 例如JSR-305的:javax.annotation.Nullable
) :
public class SimpleMovieLister {
@Autowired
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
...
}
}
您也可以為眾所周知的依賴項介面使用@Autowired
: BeanFactory
, ApplicationContext
, Environment
, ResourceLoader
, ApplicationEventPublisher
,還有MessageSource
. 這些介面和他們的擴充套件介面, 例如:ConfigurableApplicationContext
或者ResourcePatternResolver
, 會被自動解析, 不需要特殊的設定步驟. 下面例子展示了自動裝配一個ApplicationContext
物件:
public class MovieRecommender {
@Autowired
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
@Autowired
,@Inject
,@Resource
, 和@Value
是被Spring的BeanPostProcessor
實現所處理. 這意味著你不能將這些註釋應用到你自己的BeanPostProcessor
或者BeanFactoryPostProcessor
型別. 這些型別必須顯式的使用XML或Spring@bean
方法裝配.
1.9.3 使用@Primary
註釋進行微調
因為按型別的自動裝配可能會有多個候選, 所以有必要在選擇過程中有更多控制. 一種達成此目標的方式是使用@Primary
註解. @Primary
表明當多個bean候選都符合單例依賴時, 這個特定的bean應當被自動裝配給引用. 如果確實有一個主要的bean, 那它就成為了自動裝配的值.
參看下面的配置, 定義了firstMovieCatalog
作為主要的MovieCatalog
:
@Configuration
public class MovieConfiguration {
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }
@Bean
public MovieCatalog secondMovieCatalog() { ... }
// ...
}
使用上面的配置, 下面的MovieRecommender
被自動裝配了firstMovieCatalog
:
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog;
// ...
}
相關的bean定義如下:
<?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:annotation-config/>
<bean class="example.SimpleMovieCatalog" primary="true">
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
1.9.4 使用Qualifiers微調
當使用按型別自動封裝時, 如果有幾個不同的例項而只有一個主要的例項被確定, @Primary
是一種有效的方法.當你需要在選擇過程中有更多控制時, 可以使用Spring的@Qualifier
註解. 你可以使用它的幾個相關引數來縮小型別匹配範圍, 這樣對於任一引數選擇其特定的bean. 在最簡單的示例中, 它可以是一個普通的描述值, 如下:
public class MovieRecommender {
@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
// ...
}
你也可以指定@Qualifier
註解到單獨的建構函式引數上, 如下:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(@Qualifier("main")MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
下面示例展示了相關的bean定義:
<?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:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier value="main"/> <!--用main指定的值被具有相同名稱的建構函式引數自動裝配-->
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier value="action"/> <!--用action指定的值被具有相同名稱的建構函式引數自動裝配-->
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
作為一種回退匹配, bean的名稱被認為是預設的指定值. 因此,你可以定義一個
id
為main
的bean替代內部的指定元素, 結果是一樣的. 雖然你可以使用bean名稱引用特定的bean,@Autowired
根本上是使用可選語義的修飾語進行型別驅動注入.這就意味著限定的值,即便有名稱後備, 總是會限定在匹配型別範圍內. 他們語義上不會表達為對一個beanid
的引用. 好的限定值是main
,MEMA
或persistent
, 表達獨立於bean的id
外的一種特定角色, 這個bean的id可能是自動生成的匿名定義, 就像前面例子中一樣.
限定還可被用到集合, 就像前面討論的一樣, 如:
Set<MovieCatalog>
. 這種情況下, 所有匹配的bean, 根據宣告的限定, 是作為集合注入. 這暗示了限定符不需要是唯一的. 相反他們構成了過濾標準. 例如, 你可以使用相同的限定值"action"定義多個MovieCatalog
bean, 所有他們都被注入到使用@Qualifier("action")
註解的集合Set<MovieCatalog>
中.
在型別匹配的候選時, 讓限定值依賴bean名稱選擇, 不需要在注入點新增@Qualifier
註解. 如果沒有其他解析指令(如限定符或primary標記), 對於非單一依賴的情況, Spring通過目標bean的名稱並選擇相同名稱的候選來匹配注入點名稱(也就是,域名稱或引數名稱).
也就是說, 如果你想通過名稱來表達註解驅動的注入, 就不需要使用@Autowired
, 就算他能在型別匹配的候選中進行選擇. 相反, 使用JSR-250 的 @Resource
註解, 這個註解語義上定義了特定目標bean的唯一名稱, 宣告的型別和匹配過程無關. @Autowired
有不同的語義: 在通過型別選擇bean後, 特定的String
限定的值被認為僅僅是侯選中的(例如, 匹配account
限定將依賴於相同的限定標籤).
對於本身定義為集合的bean, 如Map
或陣列型別, @Resource
是好的解決方法, 通過名字引用特定的集合或陣列bean. 也就是說, 自從4.3版本開始, 你也可以使用@Autowired
型別匹配演算法來匹配Map
和陣列型別, 只要元素型別資訊儲存在@Bean
的返回簽名型別或集合繼承中. 在這種情況下, 如上一段所述, 你可以使用限定值在相同型別中進行選擇.
從4.3開始, 對於注入, @Autowired
也被認為是自引用的(也就是引用返回到當前被注入的bean上). 注意,自我注入是個回退. 一般的對其他元件的依賴往往具有優先權. 從這個意義上說, 自我引用不在一般候選範圍因此從來不是主要的. 相反,他們一般是最後的選擇. 實際上你應該使用自我引用作為最後的手段(例如通過bean的事務代理呼叫同一例項的其他方法). 在這種場景下, 考慮重構受影響的方法到單獨的代理bean中. 想法, 你可以使用@Resource
, 這可以通過其唯一名稱獲取到當前bean的代理.
@Autowired
被用到域,建構函式,還有多個引數的方法上, 允許通過限定註解在引數級別縮小範圍. 相比之下, @Resource
僅被支援在域和bean屬性的setter方法上使用. 因此, 你應該在你注入目標是個建構函式或多引數方法上使用限定.
你也可以建立自定義的限定註解. 定義了一個註解, 並給其定義上新增了@Qualifier
, 如下:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {
String value();
}
接下來你就可以將自定義限定新增到域或引數上, 如下:
public class MovieRecommender {
@Autowired
@Genre("Action")
private MovieCatalog actionCatalog;
private MovieCatalog comedyCatalog;
@Autowired
public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
this.comedyCatalog = comedyCatalog;
}
// ...
}
接下來, 你可以為候選bean定義提供資訊了. 你可以新增<qualifier/>
標籤作為<bean/>
標籤的子元素, 然後為其指定type
和value
來匹配你自定義的註解. 型別通過註解類的全限定名匹配. 最為替換, 如果沒有名字衝突, 你可以使用短名稱. 下面例子展示了全部兩種方式:
<?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:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier type="Genre" value="Action"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier type="example.Genre" value="Comedy"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
在類路徑掃描和元件管理中, 你可以看到XML定義的基於註解的限定後設資料. 特別的, 請參看使用註解提供限定後設資料.
一些情況下, 使用沒有值的註解就足夠了. 當註解服務於更普遍的目標並且能應用於多個不同型別的依賴時更為有用. 例如, 你可能需要提供一個離線目錄供搜尋, 當沒有網路接入時. 首先, 定義簡單的註解, 如下:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}
然後, 新增註解到域或屬性上, 如下:
public class MovieRecommender {
@Autowired
@Offline //此處新增了註解@Offline
private MovieCatalog offlineCatalog;
// ...
}
現在bean定義僅僅需要一個限定type
, 如下所示:
<bean class="example.SimpleMovieCatalog">
<qualifier type="Offline"/> <!--這個元素指定了限定-->
<!-- inject any dependencies required by this bean -->
</bean>
你也可以定義自定義限定註解, 其可以接收更多的命名屬性或替換簡單的value
屬性. 如果多個屬性值被指定到一個域或引數上, 自動包裝時, bean的定義必須匹配所有這些屬性值才能作為候選. 參看如下所示的定義:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {
String genre();
Format format();
}
這個例子中Format
是列舉, 定義如下:
public enum Format {
VHS, DVD, BLURAY
}
自動包裝的域是用自定義限定註解的, 其包含兩個屬性:genre
和format
, 如下所示:
public class MovieRecommender {
@Autowired
@MovieQualifier(format=Format.VHS, genre="Action")
private MovieCatalog actionVhsCatalog;
@Autowired
@MovieQualifier(format=Format.VHS, genre="Comedy")
private MovieCatalog comedyVhsCatalog;
@Autowired
@MovieQualifier(format=Format.DVD, genre="Action")
private MovieCatalog actionDvdCatalog;
@Autowired
@MovieQualifier(format=Format.BLURAY, genre="Comedy")
private MovieCatalog comedyBluRayCatalog;
// ...
}
最終, bean定義應該包含匹配的限定值. 這個示例也展示了你可以使用bean的後設資料屬性來代替<qualifier/>
元素. 如果可用, <qualifier/>
和他的屬性優先, 但自動封裝機制回退則使用<meta/>
標籤的值, 如果沒有限定存在的話. 如下所示定義中後面兩個:
<?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:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Action"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Comedy"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<meta key="format" value="DVD"/>
<meta key="genre" value="Action"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<meta key="format" value="BLURAY"/>
<meta key="genre" value="Comedy"/>
<!-- inject any dependencies required by this bean -->
</bean>
</beans>
1.9.5 使用泛型作為自動包裝限定符
另外, 對於@Qualifier
註解, 你可以使用java泛型型別作為限定的隱式格式. 例如, 你有如下配置:
@Configuration
public class MyConfiguration {
@Bean
public StringStore stringStore() {
return new StringStore();
}
@Bean
public IntegerStore integerStore() {
return new IntegerStore();
}
}
假設前面的bean實現了一個泛型介面(也就是Store<String>
和Store<Integer>
), 你可以@Autowire
這個Store
介面, 並且泛型被用來作為限定, 如下:
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean
@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
泛型限定也用來自動裝配集合列表,Map
介面和陣列. 下面例子自動裝配了泛型List
:
// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;
1.9.6 使用CustomAutowireConfigurer
CustomAutowireConfigurer
是一個BeanFactoryPostProcessor
, 允許你註冊你自己自定義的限定註解型別, 就算它們不是被Spring的@Qualifier
註解的. 下例展示瞭如何使用CustomAutowireConfigurer
:
<bean id="customAutowireConfigurer"
class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
<property name="customQualifierTypes">
<set>
<value>example.CustomQualifier</value>
</set>
</property>
</bean>
AutowireCandidateResolver
通過下面的方式決定自動裝配候選:
- 每個bean定義的
autowire-candidate
值 - 任何
<beans/>
元素上可用的default-autowire-candidates
模型 - 使用了
@Qualifier
的註解以及通過CustomAutowireConfigurer
註冊的自定義註解
當多個bean被限定為自動裝配的候選, 首要項的決策遵循: 如果候選中存在primary
屬性且值為true
, 則它被選中.
1.9.7 使用@Resource
注入
Spring支援使用JSR-250規範的@Resource
註解進行注入, 其(javax.annotation.Resource
)能夠用在域或者setter方法上. 這是JavaEE的一種普遍模式: 例如, 在JSF-管理的bean和JAX-WS端點中. Spring為Spring管理的bean也支援這種模式.
@Resource
獲取name屬性. 預設Spring將這個值作為bean名稱注入. 換句話說, 它遵循按名稱語義, 如下所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource(name="myMovieFinder") // 這裡注入了一個@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
如果沒有明確指定名稱, 預設名稱就是從域的名字或者setter方法匯出. 在域情況下, 它使用域名稱, 在setter方法情況下, 它獲取bean屬性名. 下面例子展示了名為movieFinder
的bean注入到setter方法:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
註解提供的名稱將會被接收
CommonAnnotationBeanPostProcessor
通知的ApplicationContext
解析為bean的名稱. 名稱可以通過JNDI解析, 如果你顯式配置了Spring的SimpleJndiBeanFactory
. 儘管可以, 但我們建議你依賴預設的行為並使用Spring的JNDI查詢來保證間接級別.
在沒有顯式的名稱指定的@Resource
使用場景下, 類似於@Autowired
,@Resource
查詢主型別匹配而不是命名bean, 並且也可以解析熟知的依賴:BeanFactory
, ApplicationContext
, ResourceLoader
, ApplicationEventPublisher
, 和 MessageSource
介面.
因此, 下例中,customerPreferenceDao
域首先查詢一個名為"customerPreferenceDao"的bean 並且回退到主型別匹配的型別CustomerPreferenceDao
:
public class MovieRecommender {
@Resource
private CustomerPreferenceDao customerPreferenceDao;
@Resource
private ApplicationContext context; // `context`域是注入了熟知的依賴型別`ApplicationContext`
public MovieRecommender() {
}
// ...
}
1.9.8 使用@PostConstruct
和@PreDestroy
CommonAnnotationBeanPostProcessor
不僅識別@Resource
註解, 也可以識別JSR-250生命週期註解:javax.annotation.PostConstruct
和javax.annotation.PreDestroy
. Spring 2.5 引用的, 對於這些註解的支援提供了一種相應的生命週期回撥機制, 如之前的初始化回撥和銷燬回撥章節有描述的. 假設ApplicationContext
註冊了CommonAnnotationBeanPostProcessor
, 一個新增了這些註解的方法將會被呼叫, 呼叫點與相應的Spring生命週期介面方法或顯式宣告回撥方法相同. 下面例子中, 快取通過初始化方法預熱並通過銷燬方法清除:
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
關於組合使用不同的生命週期機制, 可以參看組合生命週期機制.
如同
@Resource
,@PostConstruct
和@PreDestroy
註解是從JDK6到8成為標準的java類庫的一部分. 儘管這樣, 整個javax.annotation
包從JDK9的核心java模組中獨立了, 並最終在JDK11中被刪除.如果要用, 則需要從Maven中心引用javax.annotation-api
架構, 簡單的將其新增到類路徑下.
1.10 類路徑掃描和託管元件
本章中大部分例子都將使用XML來指定用於Spring容器生產BeanDefinition
的配置後設資料. 前面章節(基於註解的容器配置)闡述如何通過程式碼級別的註解來生成大量配置後設資料. 就算在那些示例中, 最基礎的bean定義依然是顯式定義在XML檔案中的, 而註解僅僅是驅動了DI. 本節描述一種隱式的通過掃描類路徑方法探測候選元件的方式. 候選元件是一組按一定過濾規則匹配的並被容器註冊的相關bean定義. 這就移除了使用XML來完成bean註冊的方式. 相反, 你可以使用註解(例如:@Component
), AspectJ型別表示式, 或者你自定義的過濾規則來篩選哪些類定義能註冊到容器中.
Spring 3.0 開始. 通過Spring的Java配置專案提供的一些特性成為了Spring核心的一部分. 這就允許你使用Java而不是傳統的XML來定義bean. 可以參看
@Configuration
,@Bean
,@Import
, 和@DependsOn
註解示例來了解如何使用這些特性.
1.10.1 @Component
和更多註解
@Repository
註解是為很多履行一個倉儲角色(等同於資料訪問物件或DAO)的諸多類上的一個標記. 使用這個標記的一個用法就是自動解析異常, 如同在異常解析一節描述的.
Spring 提供了更多註解: @Component
, @Service
, 和 @Controller
. @Component
是一般的使用在Spring管理的元件上的註解. @Repository
, @Service
, 和 @Controller
是比@Component
更具針對性的用例(各自分別在持久化,服務和表現層使用). 因此, 你可以在你的元件上使用@Component
, 但使用@Repository
, @Service
, 和 @Controller
替代將使你的類在工具處理時或聯合使用方面時更合理. 例如, 這些註解提供了理想的目標切入點. @Repository
, @Service
, 和 @Controller
也能在將來Spring版本中提供更多的語義.因此, 如果你在你的服務層中選擇使用@Component
或 @Service
, @Service
是更明確的更好的選項. 類似的, 如前所述, @Repository
在持久層已經得到異常解析方面的支援.
1.10.2 使用元註解和組合註解
許多Spring提供的註解可以作為元註解用到你的程式碼中. 元註解是可以應用到另一個註解上的註解. 例如, 前面提到的@Service
註解就是使用@Component
元註解的, 如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // Component 將導致 @Service 與 @Component 一樣被對待
public @interface Service {
// ....
}
你也可以組合使用元註解用來建立組合註解. 例如, Spring MVC 的 @RestController
是由@Controller
和@ResponseBody
組合的.
更多的, 組合註解能夠選擇重新定義元註解的屬性來自定義. 如果你想要僅暴露元註解中的一些屬性的話這就是有用的. 例如, Spring的@SessionScope
註解硬編碼了scope的名稱為session
, 但仍然允許自定義proxyMode
. 如下是SessionScope
的定義:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
然後你可以不宣告proxyMode
來使用@SessionScope
:
@Service
@SessionScope
public class SessionScopedService {
// ...
}
你也可以重寫proxyMode
的值, 如下所示:
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}
更多細節請看wiki頁面的Spring註解程式設計模型
1.10.3 自動探測類和註冊bean定義
Spring能自動探測更多的類並使用ApplicationContext
註冊相關的BeanDefinition
例項.例如下面的兩個類對於這樣的自動探測是合適的:
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
為了自動探測並註冊相關的bean, 你需要新增@ConponentScan
到@Configuration
類上, basePackages
屬性時兩個類通用的包(如果多個包的話,你可以使用逗號或分號或空格分割的列表,將每個類的父包包含其中).
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig{
...
}
為了簡潔, 上面例子可以使用註解的值屬性(也就是
@ComponentScan("org.example")
).
下面是對應的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="org.example"/>
</beans>
使用
<context:component-scan>
隱式的啟用了<context:annotation-config>
的功能. 當使用<context:component-scan>
時就不需要在包含<context:annotation-config>
元素了.
類路徑包的掃描需要相關的目錄包含在類路徑下. 當你使用Ant打包JARs是, 確保沒有為JAR任務開啟"僅檔案"開關. 同時, 類路徑目錄可能會在一些環境下由於安全策略而沒有暴露--例如, JDK1.7.0-45的獨立app或更高(需要信任類庫設定在你的清單中, 參看http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources).
在JDK9的模組路徑(Jigsaw)下, Spring類路徑掃描通常按預期執行. 儘管如此, 請確保你的元件類匯出在
module-info
下. 如果你期望Spring呼叫你類中的非公有變數, 請確保他們是'opened'(也就是,在module-info
描述符下,他們使用opens
宣告替換exports
宣告).
更多的, 當你使用component-scan元素時, AutowiredAnnotationBeanPostProcessor
和CommonAnnotationBeanPostProcessor
兩者都是隱式包含的. 這意味著這兩個元件被自動探測並一起包裝了--都沒有任何XML提供的bean配置後設資料.
你可以通過包含
annotation-config
屬性並設定為false
來關閉AutowiredAnnotationBeanPostProcessor
和CommonAnnotationBeanPostProcessor
.
1.10.4 使用過濾器來自定義掃描
預設情況下, 用@Component
, @Repository
, @Service
, @Controller
標記的類, 或者使用@Component
元註解的自定義註解標記的類是探測的候選元件. 儘管這樣, 你也可以通過應用過濾器修改和擴充套件行為. 為註解@ComponentScan
新增includeFilters
或excludeFilters
引數(或者component-scan
元素的include-filter
或 exclude-filter
子元素). 每個元素都需要type
和expression
屬性, 下面的表格描述了過濾選項:
表5. 過濾型別
過濾型別 | 示例表示式 | 描述 |
---|---|---|
annotation (default) | org.example.SomeAnnotation | 目標元件上, 在類級別上出現的註釋 |
assignable | org.example.SomeClass | 目標元件要被其擴充套件或實現的類或介面 |
aspectj | org.example..*Service+ | 與目標元件匹配的一個AspectJ表示式 |
regex | org.example.Default.* | 與目標元件類名稱匹配的正規表示式 |
custom | org.example.MyTypeFilter | org.springframework.core.type.TypeFilter 介面的自定義實現 |
下面例子展示了忽略所有@Repository
註解並使用"stub"倉儲代替的配置:
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
...
}
下面例子是等效的XML配置:
<beans>
<context:component-scan base-package="org.example">
<context:include-filter type="regex"
expression=".*Stub.*Repository"/>
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
</beans>
你也可以通過在註解上設定
useDefaultFilters=false
或者給元素<component-scan/>
設定use-default-filters="false"
來使預設過濾器失效. 這實際上也使得使用@Component
,@Repository
,@Service
,@Controller
, 或@Configuration
註解的類的自動探測失效了.
1.10.5 在元件中定義後設資料
Spring元件也能為容器貢獻bean定義後設資料. 你可以通過使用@Configuration
註解的類裡面的@Bean
註解來定義bean後設資料. 下面例子展示了這種方式:
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
上面的類是Spring的一個元件,work()
方法中它有應用特定的程式碼. 它也貢獻了一個bean的定義,有個引用方法publicInstance()
的工廠方法. @Bean
註解標識了工廠方法和其他bean定義的屬性, 如通過@Qualifier
指定的限定值. 其他方法級別的註解可能是特定的@Scope
,@Lazy
以及自定義限定註解.
除了元件初始化角色, 你也可以將
@Lazy
註解放在由@Autowired
或@Inject
標記的注入點上. 在這個上下文中, 它最終注入了一個延遲解析代理.
自動裝配域和方法是受支援的, 如前所述, 同時對@Bean
方法有更多的自動封裝的支援. 如下所述:
@Component
public class FactoryMethodComponent {
private static int i;
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
// use of a custom qualifier and autowiring of method parameters
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}
@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}
@Bean
@RequestScope
public TestBean requestScopedInstance() {
return new TestBean("requestScopedInstance", 3);
}
}
示例自動為型別為String
的引數country
自動裝配了另一個名為privateInstance
的bean的age屬性值. Spring表示式語言元素通過註解#{<expression>}
定義了值. 對於@Value
註解, 表示式解析器在解析表示式文字的時候查詢bean的名稱.
從Spring4.3開始, 你可能需要宣告一個引數型別為InjectionPoint
的工廠方法(或者它的特定的子類如DependencyDescriptor
), 來訪問請求的注入點來觸發當前bean的建立. 注意這種僅適用於真實的bean例項建立, 對於已存在例項的注入不適用. 作為其結果, 這個特性對於原型bean更有意義. 對於其他作用域, 工廠方法僅看到scope內觸發建立新bean例項的注入點(例如, 觸發建立延遲載入單例bean的依賴項).這種情況下你可以謹慎的使用注入點後設資料. 下面例子展示瞭如何使用InjectionPoint
:
@Component
public class FactoryMethodComponent {
@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}
普通Spring元件中的@Bean
方法與其他在Spring@Configuration
類中的其他同輩們處理是不同的. 不同之處在於, @Component
類沒有用CGLIB增強用來切入方法和域的呼叫. CGLIB代理是一種呼叫@Configuration
類中@Bean
註解的方法和域來建立引用到相關物件的bean後設資料的方法.此類方法不會被普通的java語義呼叫, 而是為了提供非正常的生命週期管理和Spring的Bean代理而貫穿容器, 即使是通過程式設計方法呼叫@Bean
方法引用其他bean時. 相反, 呼叫@Component
類中的@Bean
方法或域時使用標準java語義, 沒有其他特定CGLIB的處理或應用其他限制.
你可能宣告
@Bean
為static
, 允許呼叫它們,而無需將其包含的配置類作為例項建立。這在定義post-processor的bean時有特殊的意義(例如, 型別BeanFactoryPostProcessor
或BeanPostProcessor
), 因為這樣的bean會被容器生命週期中更早的初始化, 並且在這個點上應該避免觸發其他配置部分.
呼叫靜態的
@Bean
方法從來不會被容器切入, 就算是@Configuration
類(如前所述), 由於技術限制: CGLIB 子類僅僅能覆蓋非靜態方法. 因此, 對另一個@Bean
方法的直接呼叫具有java語義, 結果一個獨立的例項直接由工廠方法返回.
@Bean
方法的Java語言的可見性不會立即對Spring容器中的bean定義產生影響. 你可以自由的在非@Configuration
類中宣告工廠方法, 或者在任何地方宣告靜態方法. 儘管可以,@Configuration
類中普通的@Bean
方法需要是可覆蓋的-- 也就是他們不能被宣告為private
或者final
.
在提供的元件或配置類中
@Bean
方法可以被發現, 同樣在java8的由元件和配置類實現的介面中的預設實現方法也可以. 從Spring4.2開始, 這就允許在組合複雜配置時有更大的靈活性, 甚至通過Java8的預設辦法使多繼承成為可能.
最終,一個類中可能持有相同bean的多個方法, 作為多工廠方法的安排, 這些取決於於執行時可用依賴. 這與選擇最佳建構函式或配置中的工廠方法是相同的演算法: 在構造時會選擇具有最大數量的依賴項的變數,即容器如何在多個
@Autowired
建構函式之間進行選擇。
1.10.6 命名的自動探測元件
當一個元件被作為掃描過程的一部分探測到時, 它的bean名稱通過掃描者知道的BeanNameGenerator
策略生成. 預設情況下,任何Spring的註解(@Component
, @Repository
, @Service
, 和 @Controller
)包含一個名字value
因此提供了相應bean定義的名稱.
如果註解不包含value
或被其他元件探測(例如自定義過濾器), 預設的bean名稱生成器會返回首字母小寫的不合格類名. 例如, 付過下面的元件類被探測到, 他們的名字應該是myMovieLister
和movieFinderImpl
.
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
如果你不想依賴預設的bean命名策略, 你可以提供自定義的bean命名策略. 首先,實現
BeanNameGenerator
介面, 並且保證包含了無參建構函式. 接著, 當配置掃描者的時候提供全限定類名, 如下例子中的註解和bean定義展示的:
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
...
}
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>
作為通用規定, 考慮使用註解指定名稱, 當其他元件顯式引用到它時. 另一方面,當容器負責裝配時自動生成的名稱就足夠了.
1.10.7 為自動探測元件提供作用域
Spring託管元件一樣, 預設的和大多數通用的scope是singleton
. 雖然如此, 有時候你需要通過@Scope
指定一個不同的作用域. 你可以為其指定名字, 如下所示:
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
@Scope
註解僅在具體的bean類上(註解的元件)或工廠方法上(@Bean
方法)是自省的. 相比於XMLbean定義, 沒有bean定義繼承的概念, 並且類級別的繼承與後設資料不相干.
在Spring上下文中更多的關於web特定的作用域如"request","session"等,請參看Request,Session,Application和WebSocket作用域. 這些作用域有預置的註解, 你也可以使用Spring元註解的方式組合你自己的作用域註解: 例如, 使用@Scope("prototype")
自定義註解,可能需要宣告一個自定義的scope-proxy模式.
為作用域解析提供一個自定義策略而不是依賴註解的方式, 你可以實現介面
ScopeMetadataResolver
. 確保包含一個無參建構函式. 接著你在配置掃描的時候提供一個全限定名, 如下例子中的註解和bean定義所示:
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
...
}
<beans>
<context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>
當使用非單例作用域時, 也許必要為作用域的物件生成代理. 原因在作用域bean作為依賴項中有描述. 為了達到此目的, component-scan元素的一個scoped-proxy屬性時可用的. 三種可能的值分別是: no
,interfaces
,和targetClass
. 例如, 下面配置將生成標準的JDK動態代理:
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
...
}
<beans>
<context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
1.10.8 Qualifier後設資料註解
在使用修飾符來微調基於註解的裝配中討論了@Qualifier
註解. 那一節中的例子展示了在解析自動裝配的候選的時候, @Qualifier
註解和自定義修飾註解提供的更好的控制. 因為這些例子基於XML bean定義, 修飾後設資料是通過在作為候選的bean
元素中使用qualifier
或 meta
子元素來實現的. 當依賴於類路徑掃描的自動探測元件時, 你可以在候選類上新增限定後設資料註解. 下面三個例子展示了這種技術:
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
對大多數基於註解的變體, 記住註解後設資料是繫結到類定義自身的, 使用XML允許給相同型別的多個bean提供不同的限定後設資料, 因為後設資料是提供給每個例項,而不是每個型別.
1.10.9 候選元件生成索引
雖然類路徑掃描是很快的, 對於大型程式, 也可以通過在編譯時建立一個靜態候選列表在提供啟動效能. 在這種模式下,自動掃描的所有模組必須必須使用這種機制.
@ComponentScan
或<context:component-scan>
指令必須保持不變, 以請求上下文掃描特定包中的候選. 當ApplicationContext
探測到這麼個索引, 則自動使用它而不是掃描類路徑.
生成索引, 要給每個包含被掃描的目標元件的模組新增一個額外的依賴. 下例展示了Maven中的配置:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.2.0.BUILD-SNAPSHOT</version>
<optional>true</optional>
</dependency>
</dependencies>
對於Gradle4.5或更早版本, 依賴需要宣告在compileOnly
配置中, 如下例子所示:
dependencies {
compileOnly "org.springframework:spring-context-indexer:5.2.0.BUILD-SNAPSHOT"
}
對於Gradle4.6及以後版本, 依賴需要宣告在
annotationProcessor
配置中, 如下例子所示:
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:5.2.0.BUILD-SNAPSHOT"
}
處理過程將生成一個META-INF/spring.components
檔案, 包含在jar檔案.
當在你的IDE中使用此模式的時候,
spring-context-indexer
必須作為註解處理器註冊, 來確保當候選元件更新時索引頁同步更新.
當
META-INF/spring.components
存在在類路徑下時, 索引自動生效. 如果索引是特定對某些庫(或用例)可用但不是為整個程式構建時, 你可以通過spring.index.ignore
設定為true
來回退到標準的類路徑排列(好像根本不存在索引), 無論是作為系統屬性或設定在類路徑下的spring.properties
檔案.
1.11 使用JSR330標準註解
Spring3.0 開始, Spring提供了對JSR-330標準註解(依賴注入)的支援. 這些註解與Spring註解以同樣的方式被掃描. 使用它們你需要在類路徑下引入相關的jar.
如果你使用Maven,
javax.inject
在標準的Maven倉儲下是可用的(http://repo1.maven.org/maven2/javax/inject/javax.inject/1/). 你可以新增下面依賴到pom.xml:
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
1.11.1 使用@Inject
和@Named
依賴注入
代替@Autowired
註解, 也可以使用@javax.inject.Inject
如下:
import javax.inject.Inject;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.findMovies(...);
...
}
}
如同@Autowired
, 你可以將@Inject
使用在域級別,方法級別和構造引數級別. 而且你可能宣告你的注入點為一個Provider
, 允許按需訪問較短作用域的bean或者通過Provider.get()
呼叫延遲訪問其他bean. 下例提供了上面例子中的一個變體:
import javax.inject.Inject;
import javax.inject.Provider;
public class SimpleMovieLister {
private Provider<MovieFinder> movieFinder;
@Inject
public void setMovieFinder(Provider<MovieFinder> movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
...
}
}
如果你想為注入的依賴項使用限定名稱, 可以使用@Named
註解, 如下例:
import javax.inject.Inject;
import javax.inject.Named;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
如同@Autowired
,@Inject
也可以與java.util.Optional
或@Nullable
一同使用. 在這裡這是更合適的, 因為@Inject
沒有required
屬性. 下面兩個例子展示瞭如何使用@Inject
和@Nullable
:
public class SimpleMovieLister {
@Inject
public void setMovieFinder(Optional<MovieFinder> movieFinder) {
...
}
}
public class SimpleMovieLister {
@Inject
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
...
}
}
1.11.2 @Named
和@ManagedBean
: 標準可替換@Component
的註解
替換@Component
, 也可以使用@javax.inject.Named
或javax.annotation.ManagedBean
, 如下所示:
import javax.inject.Inject;
import javax.inject.Named;
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
使用沒有指定名稱的@Component
是很普遍的. @Named
可以用同樣的風格, 如下:
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
當使用@Named
或 @ManagedBean
時, 你可以像使用Spring註解一樣使用元件掃描, 如下所示:
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}
不同於
@Component
, JSR-330 的@Named
和 JSR-250 的ManagedBean
註解不能組合使用. 你應該使用Spring場景模型或者建立自定義註解.
1.11.3 JSR-330 標準註解的限制
使用標準註解, 你應該知道一些顯著的特性是不可用的, 如下表所示:
表6 Spring元件模型元素與JSR-330變體的區別
Spring | Javax.inject.* | javax.inject限制/說明 |
---|---|---|
@Autowired | @Inject | @Inject 沒有'required'屬性. 可以用Java8的Optional 替代 |
@Component | @Named/@ManagedBean | JSR-330不提供組合模型,僅僅是一種標識命名模型的方式 |
@Scope("singleton") | @Singleton | JSR-330的預設作用域類似Spring的prototype . 所以為了保持與Spring預設的一致性, JSR-330 的bean宣告在容器中預設是singleton 的. 為了使用非singleton 的其他作用域, 你需要使用Spring的@Scope 註解. javax.inject 也提供了@Scope註解.不過這個僅僅用來建立你自己的註解時使用. |
@Qualifier | @Qualifier / @Named | javax.inject.Qualifier 僅是為了建立自定義限定的元註解.具體的string 型別的限定符(如同Spring的@Qualifier 帶有一個值)可以是通過javax.inject.Named 關聯 |
@Value | - | 無等效的 |
@Required | - | 無等效的 |
@Lazy | - | 無等效的 |
ObjectFactory | Provider | javax.inject.Provider 是Spring的ObjectFactory 的替代, 但僅僅有一個短的方法get() . 它也可以用在與Spring@Autowired 或無註解建構函式以及setter方法上組合使用 |
1.12 基於Java的容器配置
這節內容涵蓋了如何在java程式碼中使用註解配置容器. 包含下面主題:
- 基本概念:
@Bean
和@Configuration
- 使用
AnnotationConfigApplicationContext
初始化Spring容器 - 使用
@Bean
註解 - 使用
@Configuration
註解 - 組合基於java的配置
- Bean定義檔案
PropertySource
抽象- 語句中的佔位符解析
1.12.1 基本概念: @Bean
和@Configuration
Spring中的基於Java的配置支援和核心是@Configuration
註解類和@Bean
註解的方法.
@Bean
註解用來表明一個方法例項化,配置,並初始化出有Spring容器管理的新物件.對於熟悉Spring的<beans/>
XML配置, @Bean
註解扮演了與<bean/>
元素相同的角色. 你可以與Spring@Component
一起使用@Bean
註解方法. 即使如此, 他們總是跟@Configuration
bean一起使用的.
@Configuration
註解的類表明它的主要目的是作為bean定義的源. 而且@Configuration
允許通過呼叫同一個類中的其他@bean
方法來定義bean間的依賴關係. 可能最簡單的@Configuration
類如下:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
上面的AppConfig
類等同於下面的<beans/>
XML配置;
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
全@Configuration vs 輕@Bean模式
當
@Bean
方法在沒有被@Configuration
註解的類中宣告時, 它們會作為輕型模式被處理. 在@Component
中的bean方法或在普通舊的類中的bean方法會被認為是輕型的, 與包含的類有不同的主要目的, 並且@Bean
方法是種額外的好處.例如, 服務元件可能通過各合適的元件類附加的@Bean
方法暴露管理檢視給容器. 此情景下,@Bean
方法是通用的工廠方法機制.
不同於全
@Configuration
, 輕量@Bean
方法不能宣告內部bean依賴. 相反, 他們操作包含他們元件的內部狀態, 以及可選擇的它們宣告的引數. 這樣的@Bean
方法因此不能呼叫其他@Bean
方法. 每個這個的方法僅僅是對特定bean的引用的工廠方法, 不需要任何執行時的特定語義. 積極的一個影響是執行時沒有CGLIB子類被應用, 因此在類設計方面沒有任何限制(也就是,包含類可能是final
的等等).
普遍場景下,
@Bean
方法在@Configuration
類內部宣告, 要保證full模式總是被使用並跨方法引用因此直接關聯到容器生命週期管理. 這就阻止了相同的@Bean
方法通過普通Java呼叫, 從而幫助減少lite模式下難以追蹤的bug發生.
@Bean
和@Configuration
註解在接下來的章節中會更深的討論. 首先, 我們掃描了通過使用Java配置的方式建立容器的方式.
1.12.2 使用AnnotationConfigApplicationContext
初始化Spring容器
接下來的章節記錄了Spring的AnnotationConfigApplicationContext
, Spring3.0引入的. 多功能的ApplicationContext
實現能接收不僅僅是Configuration
類作為輸入, 還有@Component
類以及JSR-330後設資料註解的類.
當@Configuration
類作為輸入時, @Configuration
類本身被作為bean定義註冊並將其內部所有的@Bean
方法也被註冊為bean定義.
當@Component
和JSR-330類作為輸入時, 他們被註冊為bean定義, 並且假定如@Autowired
或@Inject
的DI後設資料在這些類的內部按需使用.
簡單的結構
與Spring的XML檔案用來被初始化ClassPathXmlApplicationContext
時的輸入一樣, 你可以使用@Configuration
類作為初始化AnnotationConfigApplicationContext
時的輸入. 這允許完全不適用XML的方式, 如下所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
如前面提到的, AnnotationConfigApplicationContext
不僅限於@Configuration
類. 任何@Component
或者JSR-330註解類都可以作為構造的引數, 如下所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
前面的例項假設了MyServiceImpl
,Dependency1
和 Dependency2
使用了諸如@Autowired
的Spring DI註解.
通過register(Class<?> ... )
程式設計構建容器
你可以通過無參構造例項化AnnotationConfigApplicationContext
,然後通過register()
方法配置它. 這種方法在程式設計構建AnnotationConfigApplicationContext
是特別有用. 如下所示:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
使用scan(String…)
開啟元件掃描
開啟元件掃描, 你可以在@Configuration
類上註解如下:
@Configuration
@ComponentScan(basePackages = "com.acme") // 這個註解啟動了元件掃描
public class AppConfig {
...
}
有經驗的Spring使用者可能更熟悉XML宣告, 它是從Spring的context:
名稱空間, 如下例子所示:
<beans>
<context:component-scan base-package="com.acme"/>
</beans>
在前面例子中, com.acme
包被掃描以查詢任何@Component
註解的類, 還有那些註冊在Spring容器內的bean定義.AnnotationConfigApplicationContext
暴露的scan(String…)
方法擁有同樣的元件掃描能力, 如下所示:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
記住
@Configuration
類是使用@Component
元註解的, 所以他們是掃描的候選. 在前面的例子中, 假設AppConfig
宣告在com.acme
包內部(包括更深層次的包內), 在呼叫scan()
期間它被選中. 依賴refresh()
, 它所有的@Bean
方法被處理並註冊在容器內部.
使用AnnotationConfigWebApplicationContext
支援WEB程式
一種WebApplicationContext
的AnnotationConfigApplicationContext
變體是AnnotationConfigWebApplicationContext
. 當配置ContextLoaderListener
Sevlet監聽器, Spring MVC DispatcherServlet
等時可以使用這個實現. 下面的web.xml
片段配置一個典型的Spring MVC Web程式(注意contextClass
context-param 和 init-param 的使用).
<web-app>
<!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<!-- Configuration locations must consist of one or more comma- or space-delimited
fully-qualified @Configuration classes. Fully-qualified packages may also be
specified for component-scanning -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.AppConfig</param-value>
</context-param>
<!-- Bootstrap the root application context as usual using ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Declare a Spring MVC DispatcherServlet as usual -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<!-- Again, config locations must consist of one or more comma- or space-delimited
and fully-qualified @Configuration classes -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.web.MvcConfig</param-value>
</init-param>
</servlet>
<!-- map all requests for /app/* to the dispatcher servlet -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
1.12.3 使用@Bean
註解
@Bean
是方法級別的註解, 並且與XML 的 <bean/>
元素同源. 這個註解支援一些<bean/>
屬性,如* init-method * destroy-method * autowiring * name
.
在@Configuration
註解的或者在@Component
註解的類中都可以使用@Bean
註解.
宣告一個Bean
要宣告bean, 你可以在一個方法上新增@Bean
註解. 在ApplicationContext
內部, 這個方法將被它的返回值型別註冊到容器的bean定義. 預設bean的名字和方法名稱是一樣的. 下面示例展示了@Bean
的宣告:
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
上面的配置是等同於下面XML的配置:
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
兩種方式都使得名為transferService
的bean在ApplicationContext
可用, 繫結到一個型別為TransferServiceImpl
的例項, 如下所示:
transferService -> com.acme.TransferServiceImpl
你也可以使用介面(或基類)宣告@Bean
方法返回型別, 如下所示;
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
然則, 這限制了預測型別的可見性為介面型別(TransferService
). 然後, 容器僅知道一次完整型別(TransferServiceImpl
),受影響的bean單例被建立了. 非延遲載入的單例bean例項化根據他們宣告的順序初始化, 所以,依賴於當其他元件嘗試匹配一個沒有宣告的型別時, 你可能看到不同型別的匹配結果(如@Autowired TransferServiceImpl
, 僅在transferService
被例項化時解析).
如果你持續通過宣告的服務介面引用, 你的
@Bean
返回型別可能安全地參與到設計決策中. 然而, 對於實現了多個介面或對於潛在引用它們實現型別的元件, 更安全的是儘可能的指定返回類(至少指定引用到bean的注入點時需要的型別)
Bean依賴
@Bean
註解方法可能還有隨意數量的描述建立bean所需依賴的引數. 例如, 如果我們的TransferService
需要一個AccountRepository
, 我們可以具象出使用帶有一個引數的方法, 如下:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
這種解析機制比較其建構函式依賴而言相當, 更多細節參看相關章節.
獲取生命週期回撥
任何用@Bean
註解的類定義都支援一般的生命週期回撥, 並且可以使用JSR-250@PostConstruct
和@PreDestroy
註解. 參看JSR-250註解獲取更多細節.
一般的Spring的生命週期回撥也完全支援. 如果bean實現了InitializingBean
,DisposableBean
,或者Lifecycle
, 他們各自的方法就會被容器呼叫.
標準的*Aware
介面(如BeanFactoryAware,BeanNameAware,MessageSourceAware,ApplicationContextAware等)也是完全支援的.
@Bean
註解支援指定任意的初始化和銷燬回撥方法, 如同XML中在<bean/>
元素上的init-method
和destroy-method
屬性, 如下:
public class BeanOne {
public void init() {
// initialization logic
}
}
public class BeanTwo {
public void cleanup() {
// destruction logic
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}
@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
預設Java配置定義的bean有個公有的
close
或shutdown
方法自動參與銷燬回撥. 如果你定義了相關的方法又不想在容器關閉時呼叫,你可以新增@Bean(destroyMethod="")
到bean的定義來關閉預設的(inferred
)推斷模式.
你可能想在獲取JNDI資源時預設這麼做, 這種資源的生命週期是在程式外管理的. 特別的, 確保在操作
DataSource
時總是這麼做, 因為在Java EE 程式伺服器上已知是有問題的.
下面示例展示瞭如何阻斷自動的銷燬回撥:
@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
return (DataSource) jndiTemplate.lookup("MyDS");
}
此外, 使用
@Bean
方法, 通常使用程式設計方式使用JNDI查詢, 也使用Spring的JndiTemplate
或JndiLocatorDelegate
幫助類或者直接的JNDIInitialContext
, 但不是JndiObjectFactoryBean
變數(這將迫使你宣告返回型別為FactoryBean
而不是真實的目標型別, 增加其他@Bean
方法中跨引用呼叫的難度, 這些方法本來要引用提供的資源).
在上面的BeanOne
示例程式碼中, 構造時呼叫init()
方法是等效的, 如下所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}
直接與java操作時, 你可以做任何你想要做的事情, 而不需依賴容器生命週期.
指定Bean作用域
Spring包含@Scope
註解所以你可以指定bean的作用域.
使用@Scope
註解
你可以為@Bean
註解的bean定義指定作用域. 可以使用在bean作用域中指定的標準作用域.
預設作用域是singleton
, 但可以覆蓋, 如下:
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
@Scope
和 scoped-proxy
通過作用域代理, Spring提供了一種方便的方法與作用域依賴協作. 最簡單的方式是在XML配置<aop:scoped-proxy/>
新增建立的代理.在Java中配置bean使用@Scope
註解的proxyMode
屬性提供了等效的支援. 預設沒有代理(ScopedProxyMode.NO
), 但可以指定ScopedProxyMode.TARGET_CLASS
或ScopedProxyMode.INTERFACES
.
如果將XML中的引用文件(參看作用域代理)移植到Java的@Bean
, 它看起來如下:
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
return new UserPreferences();
}
@Bean
public Service userService() {
UserService service = new SimpleUserService();
// a reference to the proxied userPreferences bean
service.setUserPreferences(userPreferences());
return service;
}
自定義Bean命名
預設配置的類使用@Bean
方法名稱作為結果Bean的名稱. 這個功能可以使用name
屬性覆蓋, 如下:
@Configuration
public class AppConfig {
@Bean(name = "myThing")
public Thing thing() {
return new Thing();
}
}
Bean別名
如在命名Bean中討論的, 有時候迫切需要給bean指定多個名稱, 也就是別名. @Bean
的name
屬性可以接收一個String型別的陣列來達到這個目的. 下面例子展示了一個bean中的多個別名:
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
Bean描述
有時候為bean提供更多細節的描述是有用的. 特別是在bean暴露給監控的時候(也許通過JMX).
為@Bean
新增描述, 你可以使用@Description
註解, 如下:
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}
1.12.4 使用@Configuration
註解
@Configuration
是一個表明物件是bean定義源的類級註解. @Configuration
通過公有的@Bean
註解方法宣告類. 呼叫@Configuration
類內的@Bean
方法也可以用來定義內部bean依賴. 參看基本概念:@Bean
和@Configuration
的基本介紹.
注入內部bean依賴
當bean有對其他bean的依賴時, 表達這種依賴如同一個呼叫另一個bean方法這樣簡單, 如下所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
前面的例子中, beanOne
通過建構函式接收一個beanTwo
.
這種內部bean依賴宣告的方法僅適用於在
@Configuration
內部定義的@Bean
. 適用尋常的@Component
類不能宣告內部bean依賴的情況.
查詢方法注入
如前面提到的, 查詢方法注入是應該少用的高階特性. 在一個單例bean擁有原型bean依賴的場景它是有用的. 使用java配置為實現這種模式提供了自然的意義. 如下展示瞭如何使用查詢方法注入:
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
通過使用java配置, 你可以建立CommandManager
的子類, 其createCommand()
方法被覆蓋, 這個方法用來查詢一個原型指令物件. 如下所示:
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// inject dependencies here as required
return command;
}
@Bean
public CommandManager commandManager() {
// return new anonymous implementation of CommandManager with createCommand()
// overridden to return a new prototype Command object
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}
關於基於Java配置內部工作的更多資訊
下面的兩個例子, 此例中一個@Bean
註解方法被呼叫兩次.
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
clientDao()
在clientService1()
和clientService2()
分別呼叫一次. 因為這個方法生成一個新的ClientDaoImpl
並返回, 你可能預測會有兩個例項(每個服務一個). 這種概念是有問題的: Spring中, bean預設是singleton
作用域. 這就是魔法時刻: 所有@Configuration
類都在啟動時由CGLIB
生成子類了. 子類中, 在它呼叫父類方法並建立例項前, 子方法檢查任何快取(作用域內)的bean.
根據你bean作用域不同, 這種行為可能不同. 我們這裡只講singleton.
從Spring3.2開始, 不需要新增CGLIB到類路徑了, 因為CGLIB類已經被打包到
org.springframework.cglib
並直接包含在Spring核心JAR中了.
由於在啟動時CGLIB動態新增特性, 有一些限制. 特別的, 配置Configuration類必須不是final的. 雖然, 從4.3開始, 配置類中任何建構函式都是被允許的, 包含使用
@Autowired
或單個的預設建構函式.
如果你想避免CGLIB的限制, 考慮將你的
@Bean
方法宣告在非@Configuration
類中(例如使用@Component
類代替).@Bean
之間的跨方法呼叫就不再被攔截了, 所以你必須在建構函式和方法級別完全依賴DI.
1.12.5 組合基於Java的配置
Spring基於Java的配置特性使得可以組合註釋, 這能減少配置的複雜性.
使用@Import
註解
很像XML配置中<import/>
元素被用來輔助模組化配置, @Import
註解允許從其他配置類載入@Bean
定義, 如下:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
現在, 在初始化上下文時, 不需要指定ConfigA.class
和ConfigB.class
, 僅僅ConfigB
需要顯示提供, 如下所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
這種方法簡化了容器初始化, 因為僅僅一個類需要處理, 而你不需要記住構造時潛在的大量@Configuration
類.
從Spring4.2開始,
@Import
也支援普通的元件類, 類似於AnnotationConfigApplicationContext.register
方法. 如果想避免元件掃描這就是有用的, 通過使用少量配置類作為入口顯式定義你的所有元件.
在匯入的@Bean
定義中注入依賴
前面的例子可以執行但是過分簡化. 在大多數實際場景中, bean依賴會跨Configuration類. 當使用XML時, 這不是問題, 因為沒有編譯介入, 你可以宣告ref="someBean"
並信任Spring會在初始化時處理好它. 當使用@Configuration
類時, java編譯器在配置模式下設定了約束, 引用其他bean必須符合java語義.
幸運的是, 解決這個問題很簡單. 我們已經討論的, @Bean
方法可以有任意的描述依賴的引數. 考慮下面的真實場景, 它有幾個@Configuration
類, 每個依賴描述在其他配置中:
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
有其他方法可以得到相同的結果. 記住@Configuration
類僅僅是容器中的另一個bean: 這意味著他們可以得到跟其他bean一樣的好處, 可以使用如@Autowired
或@Value
注入等特性.
確保以這種方式注入的依賴關係只屬於最簡單的型別。
@Configuration
類在上下文初始化過程中處理得非常早,通過這種方式強制注入依賴項可能會導致意外的早期初始化。儘可能使用基於引數的注入,如前面的示例所示。
同時請特別小心的使用
BeanPostProcessor
和BeanFactoryPostProcessor
定義. 這些應該總是宣告為static @Bean
方法, 不觸發容器配置類的初始化. 否則,@Autowired
或@Value
不會在配置類上工作, 因為它過早的作為bean被建立了.
下面展示一個bean如何被另一個bean自動裝配:
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource dataSource;
@Autowired
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
在
@Configuration
類上進行構造引數注入是從Spring4.3開始支援的. 注意如果僅有一個構造引數就不需要指定@Autowired
在目標bean定義上.在前面例子中,@Autowired
無需在RepositoryConfig
上指定.
全限定匯入bean,便於導航
前面的場景中, 使用@Autowired
工作的很好並且提供了急需的模組化, 但明確的決定包裝bean在哪裡依然是模糊的. 例如, 開發人員查詢ServiceConfig
, 你怎麼指定@Autowired AccountRepository
定義在哪裡? 程式碼裡面沒有明確, 並且這還好. 記住Spring Tool Suite 提供了工具化能呈現包裝的圖示, 這可能是你需要的全部. 你的Java IDE也能簡單的找到所有的宣告和AccountRepository
類的使用以及很快展示@Bean
方法和返回值.
萬一這種模糊性不可接受並且你想從IDE直接從@Configuration
類導航到另一個, 就要考慮包裝配置類本身了, 如下:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
前面提到的這種情況, AccountRepository
是完全顯式定義的. 雖然, ServiceConfig
現在被緊密耦合到RepositoryConfig
了. 這是折中. 通過使用基於介面或抽象類的@Configuration
類使用, 緊耦合能或多或少得到緩解. 如下:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
現在ServiceConfig
鬆耦合到子類DefaultRepositoryConfig
, 並且IDE的內建功能依然可用: 你可以輕易找到RepositoryConfig
的實現. 用這種方法, 導航@Configuration
類和他們的依賴於通常的導航基於介面的程式碼沒什麼差別.
如果你想對特定bean的啟動建立產生影響, 考慮宣告他們為
@Lazy
的(對於首次訪問而不是啟動)或者@DependsOn
的(確保其他bean在當前bean前建立, 超出後者直接依賴的含義).
條件包含@Configuration
類或@Bean
方法
根據隨機的系統狀態, 條件化的啟動和禁止整個@Configuration
類或單獨的@Bean
方法是有用的. 一個普遍的例子就是,當profile在Spring的Environment
來使用@Profile
註解來啟用bean(參看Bean定義Profiles獲取更多).
@Profile
註解實際上是使用更靈活的@Conditional
註解來實現的. @Conditional
註解表明特定的org.springframework.context.annotation.Condition
實現應該在@Bean
被註冊前諮詢到.
介面Condition
的實現提供了一個matches(...)
方法, 返回true
,false
. 例如, 下面程式碼展示了@Profile
使用到的Condition
實現:
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
// Read the @Profile annotation attributes
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
}
return true;
}
檢視@Conditional
java文件獲取更多細節.
組合Java和XML配置
Spring的@Configuration
支援並不是為了100%達到替換XML配置的目的. 一些設施, 例如Spring的XML名稱空間, 保留了配置容器更理想的方式. 萬一XML配置更方便或更需要, 你有一個選擇: 要麼容器是XML為中心的方式,例如ClassPathXmlApplicationContext
, 或者是一個使用AnnotationConfigApplicationContext
和@ImportResource
註解匯入XML的以Java配置為主的方式.
使用@Configuration
類的XML為主方式
可能用XML去啟動Spring容器並用ad-hoc鉤子方式包含@Configuration
類更好. 例如, 在大型的現有的程式碼中使用XML, 按需建立@Configuration
類並從已有的XML檔案中包含進來更為容易. 本節的後面, 我們會講述在XML為主的程式使用@Configuration
的這種選擇.
宣告@Configuration
類為普通的<bean/>
元素
記住@Configuration
類是容器中的最終的bean定義. 在這個系列的例子中, 我們建立了名為AppConfig
的@Configuration
類並作為<bean/>
定義包含在system-test-config.xml
中. 因為<context:annotation-config/>
是開著的, 容器識別出@Configuration
註解併合理處理了定義在AppConfig
中的@Bean
方法.
下面例子展示了Java中的一般配置:
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
下面例子展示了system-test-config.xml
檔案的一部分:
<beans>
<!-- enable processing of annotations such as @Autowired and @Configuration -->
<context:annotation-config/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="com.acme.AppConfig"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
下面是jdbc.properties
檔案的可能的內容:
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
在
system-test-config.xml
檔案中,AppConfig
<bean/>
沒有宣告id
屬性. 這樣是可接受的, 它不是必要的, 沒有其他bean引用它, 並且也不可能顯式通過名字被容器獲取. 相似的,DataSource
bean僅僅是按型別裝配, 所有顯式的id
不是嚴格需要的.
使用@Configuration
類
因為@Configuration
是用@Component
作為元註解的, 基於@Configuration
的類自動作為元件掃描的候選. 使用前面描述的例子中的場景, 我們可以重新定義system-test-config.xml
來獲取元件掃描的好處. 注意,在本例中, 我們不需要顯式宣告<context:annotation-config/>
因為<context:component-scan/>
啟動了相同的功能.
下面例子展示了修改後的system-test-config.xml
檔案:
<beans>
<!-- picks up and registers AppConfig as a bean definition -->
<context:component-scan base-package="com.acme"/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
使用@ImportResource
匯入XML的@Configuration
類為主的方式
程式中用了@Configuration
類作為主要的配置容器的機制, 但仍然可能需要使用少量XML. 這種場景下, 你可以使用@ImportResource
並僅定義你需要的XML. 這樣就可以用Java為中心的方式配置容器並將XML保持到最小. 下面例子(包含一個配置類,一個定義了bean的xml檔案, 一個properties檔案,以及主類main
)展示瞭如何使用@ImportResource
註解完成Java配置並按需使用XML:
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
properties-config.xml
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
1.13 環境抽象
Environment
介面是整合到容器的一個抽象, 抽象出兩個關鍵的程式環境方面: profiles 和 properties.
profile是一個命名的, bean定義的邏輯組,這些bean只有給定的profile啟用才註冊到容器中的. 不管是在XML還是通過註解定義, bean可能會被分配給一個profile. 關聯到profile的Environment
物件角色是決定哪些profile當前是啟用的, 還有哪些profile應該是預設啟用的.
在大多數程式中, properties扮演了一個重要的角色, 並且源於一些列初始來源: properties檔案, JVM系統屬性,系統環境變數, JNDI,servlet上下文引數,ad-hoc屬性物件, Map
物件等. 關聯到properties的Environment
物件提供給使用者一種便捷的服務介面, 來配置屬性源和解析他們.
1.13.1 Bean定義Profile
Bean定義profile提供了一種核心容器的機制, 允許不同環境中不同bean的註冊. "environment"這個單詞對不同的使用者有不同的意義, 並且在很多用例下有用, 包含:
開發時在記憶體資料庫進行, QA或生產則查詢JNDI中相同的資料庫
在效能環境部署程式時, 註冊監控元件
對於客戶A和客戶B部署不同的自定義實現.
考慮第一種用例, 在特定程式中需要需要一個DataSource
. 在測試環境, 配置可能如下組織:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
現在考慮在QA或生產環境部署程式, 假設程式的生產資料來源註冊在伺服器的JNDI目錄. 我們的dataSource
bean現在如下:
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
問題是如何根據當前環境轉換這兩個變數. 隨著時間的過去, Spring的使用者想出很多方法來實現, 通常依賴於系統環境變數和XML包含了${placeholder}
語句的<import/>
來解決, 根據環境變數的值不同解析出正確的檔案路徑. Bean定義profile提供瞭解決此類問題的一種核心特性.
如果我們總結上面的用例, 我們以需要在不同上下文中註冊不同的bean定義結束. 你可能會說你想在狀況A時註冊一個profile,而狀況B時註冊另一個profile. 我們修改你的配置以達到這一需求.
使用@Profile
@Profile
註解能表明一個元件對於一種或多種啟用的profile是有效的. 使用前面的例子, 我們重寫dataSource
配置如下:
@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
如早前提到的,
@Bean
方法,你通常通過使用Spring的JndiTemplate/JndiLocatorDelegate
或直接是JNDI的InitialContext
來程式設計實現JNDI查詢, 但不使用JndiObjectFactoryBean
, 因為這個變數會強制你宣告返回值為FactoryBean
型別.
profile的字串可能包含一個簡單的profile名稱(如production
)或者profile表示式. profile表示式允許更復雜的邏輯(如product & us-east
). 下面操作符支援profile表示式:
!
: 表示非&
: 表示並|
: 表示或
不適用圓括號的話你不能混用
&
和|
. 例如production & us-east | eu-central
不是有效的表示式. 必須使用production & (us-east | eu-central)
.
你可以使用@Profile
作為元註解來建立組合註解. 下面例子定義了一個@Production
註解, 可以用來替代@Profile("production")
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
如果一個
@Configuration
類標記為@Profile
, 那所有關聯到這個類的@Bean
方法和@Import
都會被繞開, 除非一個或多個特定的profile被啟用. 如果@Component
或@Configuration
類被@Profile({"p1", "p2"})
標記, 那這些類將不會被處理,除非'p1'或'p2'被啟用. 如果profile被非操作符!
作為字首, 那註解的元素會在profile非啟用狀態下注冊. 例如, 給定@Profile({"p1", "!p2"})
,註冊將在profile'p1'啟用或者profile'p2'非啟用狀態下發生.
@Profile
也能在方法級別宣告用來包含配置類中一個特定的bean(例如, 特定bean的替換變數), 如下:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") // `standaloneDataSource`方法僅在`development`啟用狀態可用
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production") // `jndiDataSource`方法僅在`production`啟用狀態可用
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
使用@Bean
方法上的@Profile
, 一種特殊場景可能會應用: 對於具有相同方法名稱過載的@Bean
方法(類似建構函式過載), @Profile
條件需要在所有過載方法上宣告. 如果條件不一致, 則僅在過載方法中第一個宣告處起作用. 因此@Profile
不能被用在選擇具有引數簽名的方法. 同一bean的所有工廠方法之間的解析在建立時遵循Spring的建構函式解析演算法。
如果你想用不同的profile條件定義不同的bean, 請使用不同的java方法名稱, 這些名稱通過使用@Bean
的name屬性指向同一個bean名稱, 如前面的例子所示. 如果引數簽名都一樣(例如, 所有的變數都有無參構造方法), 這是在有效的java類中表示這種安排的唯一方法(因為只能有一個特定名稱和引數簽名的方法).
XML bean定義 profile
XML相對應的是<beans>
元素的profile
屬性. 我們上面的例子可以被重寫為XML檔案, 如下:
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
也可能為了避免分開定義在不同的<beans/>
元素, 所以定義在同一個檔案中, 如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="development">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
spring-bean.xsd
被限制僅僅作為檔案的最後元素出現. 這可能有助於XML檔案中避免混亂.
對應的XML配置不支援前面提到的profile表示式. 雖然, 可以通過使用
!
操作符. 也可能通過巢狀的profile用"and"來完成邏輯與, 如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="production">
<beans profile="us-east">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
</beans>
上面例子中,
dataSource
bean在production
和us-east
都被啟用情況下暴露出來.
啟用profile
現在我們升級了配置, 我們依然需要通知Spring哪個profile被啟用. 如果我們啟動樣例程式, 我們會看到一個NoSuchBeanDefinitionException
異常丟擲, 因為容器找不到名叫dataSource
的bean.
可以有幾種方式啟用profile, 但最直接的是通過使用ApplicationContext
的Environment
API . 如下展示瞭如何這麼做:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
更多的, 你也可以通過屬性spring.profiles.active
啟用profile, 這是通過系統變數, JVM系統屬性, web.xml
中定義的servlet引數, 甚至是JNDI中的配置(檢視PropertySource
抽象).在整合測試中, 啟用profile可以通過使用在spring-test
模組上的@ActiveProfiles
註解啟用(參看使用環境profile配置上下文).
注意profile不是一個"是...或者..."的命題. 你可以一次啟動多個profile. 程式設計方式來說, 你可以提供多個profile名稱給setActiveProfiles()
方法, 接收一個String...
變數, 如下啟用了多個profile:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
spring.profiles.active
也可以接受一個逗號分隔的profile名稱, 如下:
-Dspring.profiles.active="profile1,profile2"
預設profile
預設profile是預設情況下表現出的profile. 考慮下面的例子:
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
如果沒有profile啟用, dataSource
被建立. 你可以看到這是一種對於一個或多個bean定義的預設方式. 如果任意profile啟用了, 預設profile就不會被啟用.
你可以通過使用Environment
上的setDefaultProfiles()
修改預設profile的名稱. 或者,通過使用屬性spring.profiles.default
宣告.
PropertySource
抽象
Environment
抽象提供了從property源通過配置架構查詢操作的能力. 如下:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
上面的程式碼片段中, 我們看到從高層次的方式來查詢Spring的my-property
是否在當前環境中定義. 為回答這個問題, Environment
物件從一系列PropertySource
物件中查詢. PropertySource
是一個對key-value對的簡單的抽象. Spring的StandardEnvironment
使用兩個PropertySource 物件進行配置--一個是JVM系統屬性(System.getProperties())還有一個是系統環境變數(System.getenv()).
這些預設的屬性源是為
StandardEnvironment
存在, 在獨立程式中,StandardServletEnvironment
是由更多的包含servlet配置和servlet上下文引數的屬性源生成. 它可選地啟動JndiPropertySource
.參看java文件.
具體的, 當你使用StandardEnvironment
時, env.containsProperty("my-property")
將返回true, 如果執行時有my-property
系統屬性或my-property
環境變數存在的話.
查詢是按層次的. 預設系統屬性優先於環境變數. 因此, 如果
my-property
碰巧同時出現, 在呼叫env.getProperty("my-property")
, 系統屬性勝出並返回. 注意屬性值不是合併, 而是完全被優先的值覆蓋.
對於普通
StandardServletEnvironment
, 全部層級如下, 最上面事最高優先順序的:
- ServletConfig 引數 (如果恰當, 例如, 對於
DispatcherServlet
上下文) - ServletContext 引數(web.xml 上下文引數)
- JNDI環境變數(
java:comp/env/
入口) - JVM 系統屬性(
-D
命令列引數) - JVM 系統環境變數(作業系統環境變數)
最重要的, 整個機制是可配置的. 也許你有自定義的propertis源想要整合到這裡. 這麼做的話, 繼承和例項化你自己的PropertySource
併為當前Environment
新增PropertySources
. 如下:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
前面程式碼中, MyPropertySource
被以最大優先順序新增. 如果它包含my-property
屬性, 這個屬性將被找到並返回, 優先於任何其他PropertySource
中的my-property
. MutablePropertySources
API暴露了很多允許精確操作property源的方法.
1.13.3 使用@PropertySource
@PropertySource
註解提供了方便的新增PropertySource
到Spring的Environment
的機制.
給定的app.properties
檔案包含了鍵值對testbean.name=myTestBean
, 緊接著的@Configuration
類使用@PropertySource
呼叫testBean.getName()
返回myTestBean
:
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
任何在@PropertySource
源路徑下的${…}
都將解析為一組已經註冊到環境中的屬性源, 如下:
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
假設my.placeholder
在其他的屬性源中定義並已經註冊(例如, 系統屬性或環境變數), 那這個佔位符將解析為相關的值. 如果沒有, 那麼default/path
將會被使用, 如果沒有預設屬性指定和解析, 那IllegalArgumentException
將丟擲.
@PropertySource
註解是可重複的, 根據Java8的約定. 雖然可以, 但所有@PropertySource
註解需要在相同層級宣告, 直接在配置類或者作為元註解自定義註解. 混用元註解和指令註解不建議, 因為指令註解實際上覆蓋了原註解.
1.13.4 語句中的佔位符解析
歷史上, 元素中的佔位符只能依據JVM系統屬性或環境變數解析. 這已經不是問題了. 因為Environment
抽象通過容器整合, 通過它解析佔位符是簡單的途徑. 這意味著你可以用你喜歡的方式配置解析方式. 你也可以修改查詢系統屬性和環境變數的優先順序, 或者移除他們. 適當的話你還可以新增你自己的源.
具體的, 下面的語句不管哪裡定義了customer
, 只要他在Environment
中有效就能工作:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>
1.14 註冊LoadTimeWeaver
LoadTimeWeaver
被Spring用來在載入到JVM時動態轉化類.
啟用載入時織入, 你可以新增@EnableLoadTimeWeaving
到一個@Configuration
類上. 如下:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
對應的XML配置,可以使用context:load-time-weaver
元素.
<beans>
<context:load-time-weaver/>
</beans>
一旦為ApplicationContext
配置了任何在ApplicationContext
內部都可以實現LoadTimeWeaverAware
的bean,從而在收到在載入時織入的例項. 組合使用Spring的JPA支援時這一點很有用, 對於JPA類轉化時在載入時織入的地方可能是必要的. 訪問LocalContainerEntityManagerFactoryBean
的文件有更詳細介紹. 對AspectJ 載入時織入, 參看使用AspectJ載入時織入.
1.15 ApplicationContext
的更多功能
在本章的描述中, org.springframework.beans.factory
包提供了管理和操作bean的基本功能, 包括以程式設計的方式. org.springframework.context
包新增了介面ApplicationContext
, 這個介面擴充套件了BeanFactory
, 另外擴充套件了其他介面的功能,用來在一個更應用程式向的風格中提供附加功能. 許多人以一種完全宣告的方式使用ApplicationContext
, 並不是程式設計的建立它, 而是依賴支援類如ContextLoader
來自動例項化ApplicationContext
,將其作為Java EE Web程式啟動的一個過程.
在一個傾向框架的風格中加強BeanFactory
功能, context包也提供瞭如下功能:
- 使用介面
MessageSource
以i18n風格訪問訊息 - 通過
ResourceLoader
介面訪問資源,如URLs和檔案 - 通過使用
ApplicationEventPublisher
介面實現事件釋出, 實現了ApplicationListener
介面的命名bean. - 多(層次)上下文載入, 通過
HierarchicalBeanFactory
介面, 使得每個聚焦於特定的層, 如web層
1.15.1 使用MessageSource
國際化
ApplicationContext
介面擴充套件了叫MessageSource
的介面,因而提供了國際化("i18n")功能. Spring也提供了HierarchicalMessageSource
介面, 這個介面能層次性解析訊息. 總之這些介面提供的功能依賴於Spring有效處理訊息解析的能力. 這些定義在介面的方法包含:
String getMessage(String code, Object[] args, String default, Locale loc)
: 用來從MessageSource
獲取訊息的基本方法. 當指定區域沒有訊息時, 預設的訊息將被採用. 使用標準類庫MessageFormat
提供的功能, 任何傳入的引數將替換值.String getMessage(String code, Object[] args, Locale loc)
: 本質上跟上面方法一樣但有個區別: 沒有預設訊息指定. 如果沒有訊息發現,NoSuchMessageException
將被丟擲.String getMessage(MessageSourceResolvable resolvable, Locale locale)
: 上面方法中使用的屬性被包裝到一個叫MessageSourceResolvable
的類裡面, 這個你可用在本方法中.
當ApplicationContext
被載入, 它就自動載入上下文中定義的MessageSource
bean. 這個類必須有名字messageSource
. 如果找到這個bean, 所有前面的方法的呼叫將委託給訊息源. 如果沒有訊息源被找到, ApplicationContext
嘗試找到包含相同名稱的父容器. 如果找到了, 這個bean就作為MessageSource
使用. 如果ApplicationContext
不能找到任何訊息源, 則空的DelegatingMessageSource
被初始化用來接受上面定義的方法的呼叫.
Spring提供了兩個MessageSource
實現, ResourceBundleMessageSource
和StaticMessageSource
. 二者都實現了HierarchicalMessageSource
以便能處理巢狀訊息. StaticMessageSource
很少使用但提供了程式設計的方式新增訊息到源的方法. 下例展示了ResourceBundleMessageSource
:
<beans>
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
<value>windows</value>
</list>
</property>
</bean>
</beans>
這個示例假設你有三個源叫format
,exception
和windows
在你的類路徑下. 任何解析訊息的請求都是以通過物件ResourceBundle
解析訊息的標準JDK方式處理的. 本例的目的,假設上面的兩個訊息檔案如下:
# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.
下面例子展示了程式設計執行MessageSource
功能. 記住所有的ApplicationContext
實現也是MessageSource
實現, 所以可以被轉化為MessageSource
介面.
public static void main(String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("message", null, "Default", null);
System.out.println(message);
}
上面的程式執行結果如下:
Alligators rock!
總之, MessageSource
定義在一個名叫beans.xml
的檔案中, 這個檔案在你類路徑的根目錄下. messageSource
bean定義通過basenames
屬性引用到一系列資源bundles. 這三個檔案分別叫format.properties
, exceptions.properties
, 還有windows.properties
存在於你的類路徑下, 通過basenames
屬性傳遞.
下面例子展示了傳遞給訊息查詢的引數. 這些引數轉化為String
物件並插入查詢的佔位符中.
<beans>
<!-- this MessageSource is being used in a web application -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="exceptions"/>
</bean>
<!-- lets inject the above MessageSource into this POJO -->
<bean id="example" class="com.something.Example">
<property name="messages" ref="messageSource"/>
</bean>
</beans>
public class Example {
private MessageSource messages;
public void setMessages(MessageSource messages) {
this.messages = messages;
}
public void execute() {
String message = this.messages.getMessage("argument.required",
new Object [] {"userDao"}, "Required", null);
System.out.println(message);
}
}
execute()
方法呼叫的結果如下:
The userDao argument is required.
說起國際化("i18n"), Spring的變數MessageSource
實現遵循與標準JDK的ResourceBundle
相同的區域解析和回退規則. 總之, 繼續前面定義的messageSource
示例,如果您想解析針對英語訊息(en-GB
), 那麼你需要分別建立format_en_GB.properties
, exceptions_en_GB.properties
, 和 windows_en_GB.properties
.
一般的, 區域解析是受程式的周圍環境管理. 接下來的例子中, 依賴於(英國)的訊息將被手工的解析:
# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.
public static void main(final String[] args) {
MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
String message = resources.getMessage("argument.required",
new Object [] {"userDao"}, "Required", Locale.UK);
System.out.println(message);
}
執行結果如下:
Ebagum lad, the 'userDao' argument is required, I say, required.
你也可以使用MessageSourceAware
介面獲取任何定義的MessageSource
的引用. 任何在ApplicationContext
中定義的MessageSourceAware
介面實現將在程式上下文的bean建立和配置時使用MessageSource
注入.
作為
ResourceBundleMessageSource
的替換, Spring提供了ReloadableResourceBundleMessageSource
類. 這個變數支援檔案格式化但比起基於標準JDK的ResourceBundleMessageSource
實現更靈活. 特別的, 它允許從Spring資源路徑讀取檔案(不僅是類路徑)並且支援熱載入property檔案(將其有效快取). 檢視java文件ReloadableResourceBundleMessageSource
獲取更多資訊.
1.15.2 標準和自定義事件
通過ApplicationEvent
類和ApplicationListener
介面提供了ApplicationContext
的事件處理. 如果bean實現了ApplicationListener
介面並部署在上下文中, 每當ApplicationEvent
釋出到ApplicationContext
中時, 這個bean就得到通知了. 本質上, 這是標準的觀察者模式.
從Spring4.2開始, 事件相關基礎設施被明顯增強了, 並且提供了基於註解的模型,也可以釋出任意事件.(也就是說物件沒必要擴充套件自
ApplicationEvent
). 當這樣的物件釋出後, 我們為你包裝在一個事件中.
下面表格描述了Spring提供的標準事件:
表7 內建事件
Event | Explanation |
---|---|
ContextRefreshedEvent | 當ApplicationContext 被初始化或者重新整理後釋出(如通過使用ConfigurableApplicationContext 介面的refresh() 方法呼叫).這裡,"initialized"意味著所有的bean被載入,post-processor被探測並啟用, 單例被預初始化, ApplicationContext 物件準備就緒可用. 只要上下文沒有被關閉, 重新整理會被多次觸發, 所選的ApplicationContext 事實上支援熱重新整理. 例如XmlWebApplicationContext 支援熱重新整理,但GenericApplicationContext 不支援 |
ContextStartedEvent | 在ApplicationContext 啟動後, 使用介面ConfigurableApplicationContext 的方法start() 將釋出該事件.這裡, "啟動"意味著所有的Lifecycle bean接收到顯式的啟動訊號. 一般而言, 這個訊號在顯式停止後用來重啟bean, 但它也用來啟動那些沒有被配置為自動啟動的bean(例如, 沒有被初始化啟動的元件). |
ContextStoppedEvent | 當ConfigurableApplicationContext 介面的方法stop() 呼叫後ApplicationContext 被停止是釋出該事件. 這裡"停止"意味著所有Lifecycle bean接收一個顯式的停止訊號. 結束的上下文可能通過start() 呼叫重啟 |
ContextClosedEvent | 當ConfigurableApplicationContext 介面的方法close() 呼叫後,當ApplicationContext 關閉時釋出. 這裡, 關閉 意味著所有單例bean被銷燬. 關閉的上下文生命結束. 它不可能被重新整理或重啟 |
RequestHandledEvent | 一個WEB特定的事件,告訴所有HTTP請求被處理的bean. 這個事件在請求完成後被髮布. 這個事件僅僅對於使用了DispatcherServlet 的web程式有用. |
你也可以建立和釋出你自定義的事件. 下面的例子展示了擴充套件ApplicationEvent
基類的一個簡單類:
public class BlackListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlackListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
釋出自定義的ApplicationEvent
, 可以呼叫ApplicationEventPublisher
上的publishEvent()
方法. 一般這是通過建立一個實現ApplicationEventPublisherAware
的類並將其註冊為一個Spring bean. 如下所示:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blackList;
private ApplicationEventPublisher publisher;
public void setBlackList(List<String> blackList) {
this.blackList = blackList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blackList.contains(address)) {
publisher.publishEvent(new BlackListEvent(this, address, content));
return;
}
// send email...
}
}
在配置時, Spring探測到實現了ApplicationEventPublisherAware
介面的EmailService
, 並自動呼叫setApplicationEventPublisher()
. 實際上,傳入的引數是Spring容器自身. 你通過ApplicationEventPublisher
介面與之互動.
為了接收自定義的ApplicationEvent
, 你可以建立實現了ApplicationListener
介面的類並註冊為bean. 如下所示:
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
注意ApplicationListener
是使用自定義事件為泛型的引數的(前面例子中的BlackListEvent
).這意味著onApplicationEvent()
方法能保持型別安全, 防止任何型別轉換. 只要願意你可以註冊任意多的監聽器, 但要注意,預設情況下事件監聽器是同步接收事件的. 這意味著publishEvent()
方法會阻塞直到所有監聽器處理完事件. 一個同步和單執行緒的好處是,當監聽器獲取到一個事件, 它就會在釋出者的事務上下文可用時進行操作. 如果其他事件釋出策略時必要的, 可參看Spring介面ApplicationEventMulticaster
介面文件.
下面例子展示了用來註冊和配置上面所述每個類的bean定義:
<bean id="emailService" class="example.EmailService">
<property name="blackList">
<list>
<value>known.spammer@example.org</value>
<value>known.hacker@example.org</value>
<value>john.doe@example.org</value>
</list>
</property>
</bean>
<bean id="blackListNotifier" class="example.BlackListNotifier">
<property name="notificationAddress" value="blacklist@example.org"/>
</bean>
總體來看, 當emailService
的sendEmail()
方法被呼叫後, 如果有任何在黑名單郵件訊息, 自定義的事件BlackListEvent
就釋出了. blackListNotifier
bean作為ApplicationListener
被註冊, 並接收BlackListEvent
, 這裡可以通知到恰當的某些部分.
Spring的事件機制是為了在相同上下文內部bean之間的簡單通訊. 儘管如此, 對於大多數複雜的企業整合需求, 獨立維護的Spring整合專案提供了完整的對構建輕量,面向模式的, 事件驅動的架構的支援, 可以構建在熟知的Spring程式設計模型之上.
基於註解的事件監聽器
從Spring4.2開始, 你可以在任意的通過EventListener
註解的bean方法上註冊事件監聽器. BlackListNotifier
可以重寫如下:
public class BlackListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
這個方法簽名再次宣告瞭它監聽的事件型別, 但這次, 沒有實現特定的介面並有靈活的名稱. 這個事件型別也可以通過泛型縮小範圍. 只要在繼承架構內真實型別可以解析泛型引數即可.
如果你的方法應該監聽多個事件或者你想用無參來定義它, 這個事件型別也可以指定在註解上面. 如下所示:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
...
}
通過使用condition
屬性可以為執行時新增過濾, 可以定義一個SpEL
表示式, 用來匹配特定事件呼叫該方法.
下面例子展示了我們的notifier可以重寫, 僅在content
屬性等於my-enent
的時候來呼叫:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
每個SpEL
表示式依賴於專有上下文解析. 下表列出上下文可以用的專案, 這些你可以用來條件化處理事件:
表8 事件SpEL可用的後設資料
名稱 | 位置 | 描述 | 示例 |
---|---|---|---|
Event | root object | 實際上是ApplicationEvent |
#root.event |
Arguments array | root object | 用來呼叫目標的引數(陣列) | #root.args[0] |
Argument name | evaluation context | 方法引數名字. 如果因為某種原因,名稱不可用(例如因為debug資訊),引數名稱也可以基於#a<#arg> 使用, 其中#arg 代表引數索引(從0開始) |
#blEvent 或 #a0 (你也可以使用#p0 或#p<#arg> 記號作為別名) |
注意#root.event
提供了訪問底層事件的入口, 就算你的方法簽名實際指向釋出的任意物件.
如果你需要釋出另一個事件的結果作為事件, 你可以改變方法簽名, 使其返回要釋出的事件, 如下:
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
這個特性不支援非同步事件監聽.
這個新方法為每個上面方法處理的BlackListEvent
釋出了一個ListUpdateEvent
事件. 如果需要釋出若干事件, 則需要返回事件的Collection
.
非同步監聽器
如果你想要一個特定的監聽器去非同步處理事件, 你可以重用標準的@Async
支援. 如下:
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}
當使用非同步事件時有如下限制:
如果事件監聽器丟擲
Exception
, 它將不會傳遞給呼叫者, 檢視AsyncUncaughtExceptionHandler
有更多細節.此類監聽器不能傳送回覆. 如果你需要將處理結果作為事件釋出, 需要手工將
ApplicationEventPublisher
注入.
有序監聽器
如果你需要一個監聽器在另一個之前呼叫, 你可以新增@Order
註解到方法上, 如下:
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
泛型事件
你也可以使用泛型深化定義事件結構. 考慮使用EntityCreatedEvent<T>
其中T
是建立的真實實體. 例如, 你可以僅僅通過EntityCreatedEvent
為Person
建立事件監聽器:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
...
}
因為型別擦除, 只有在觸發的事件對事件偵聽器篩選的泛型引數進行轉述時, 此操作才有效(也就是, 形如class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }
).
在特定場景下, 如果所有事件允許相同的結構(如前面例子中的事件案例),這可能會變得相當乏味. 在類似示例中, 你可以實現ResolvableTypeProvider
來指導框架跨越執行時環境所能提供的. 如下:
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
}
}
不盡是
ApplicationEvent
, 可以是你傳送的任意物件事件.
1.15.3 便捷訪問底層資源
為了以最優來使用應用程式上下文的使用和理解,您應該使用Spring的Resource
抽象(如Resource一章中所描述的)重新調整.
程式上下文是一個ResourceLoader
,用來載入Resource
物件. Resource
本質上是個JDKjava.net.URL
類的特性增強版本. 實際上, Resource
的實現封裝了java.net.URL
例項. Resource
能以一種透明的方式獲取任何位置的底層資源, 包含類路徑, 系統路徑, 任何使用標準URL描述的地方, 還有其他的變數. 如果資源路徑字串是個沒有特殊字首的字串, 那麼這些資源的來源是特定的,並且適合於實際的應用程式上下文型別.
你可以配置一個實現了指定藉口ResourceLoaderAware
的bean部署到應用上下文, 用來自動在程式上下文初始化時作為ResourceLoader
傳入. 你也可以暴露型別為Resource
的屬性, 用來訪問靜態資源. 他們像其他屬性一樣被注入. 當bean被部署後, 你可以像簡單的String
路徑一樣指定這些Resource
屬性, 並依賴自動從字串轉化為真實的Resource
物件.
位置路徑或提供給ApplicationContext
引數的路徑實際上是資源字串, 並且簡單的形式下, 將根據特定的上下文實現被合適的對待. 如ClassPathXmlApplicationContext
將以類路徑對待一個簡單的路徑. 你可以使用特定的字首迫使定義從類路徑或URL載入, 不管真實的上下文是什麼.
1.15.4 Web程式便捷的ApplicationContext例項化
通過使用如ContextLoader
你可以建立ApplicationContext
例項. 當然, 你也可以使用ApplicationContext
的其中一個實現類程式設計方式建立ApplicationContext
.
你可以通過ContextLoaderListener
註冊ApplicationContext
, 如下:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
監聽器檢查contextConfigLocation
引數. 如果引數不存在, 預設使用/WEB-INF/applicationContext.xml
. 當這個引數存在時, 監聽器通過預定義的字元(逗號,分號,空格)分割字串並使用它們來作為應用上下文查詢的路徑. Ant風格的路徑模式也支援. 例如:/WEB-INF/*Context.xml
(對於包含在WEB-INF
目錄下的所有的以Context.xml
結尾的檔案)和/WEB-INF/**/*Context.xml
(對於所有WEB-INF
目錄下任何子目錄的檔案).
1.15.5 部署SpringApplicationContext
為JavaEE RAR 檔案
部署Spring的ApplicationContext
為JavaEE RAR 檔案是可能的, 將封裝上下文和所有需要的類和庫JAR檔案到一個JAVA EE RAR部署單元. 這等同於啟動了一個單獨的ApplicationContext
(僅在Java EE 環境宿主), 能夠訪問Java EE 伺服器設施. RAR部署是一種更自然的替代方式去部署一個沒有標頭檔案的WAR檔案, 實際上, 沒有任何HTTP入口的WAR檔案經常用在Java EE環境中, 用來啟動一個ApplicationContext
.
對於不需要HTTP入口但有訊息端點和定時任務的程式上下文, RAR部署是理想化的. 這類上下文中的bean能使用程式伺服器資源, 比如JTA事務管理和JNDI繫結JDBCDataSource
例項以及JMSConnectionFactory
例項, 並且也能註冊平臺的JMX伺服器--都是通過Spring標準事務管理和JNDI還有JMX支援設施的. 通過Spring的TaskExecutor
抽象, 應用元件也可以與應用伺服器JCAWorkManager
互動.
關於RAR部署的更多配置細節可以檢視文件SpringContextResourceAdapter
類.
對於一個簡單的將ApplicationContext部署為Java EE RAR 檔案來說:
打包所有程式類到RAR檔案(標準的JAR檔案, 只是字尾不同). 新增所需的庫JAR到RAR架構的根下. 新增
META-INF/ra.xml
部署描述符(見javadocSpringContextResourceAdapter
)還有相關的XML定義檔案(典型的如META-INF/applicationContext.xml
).將結果RAR檔案丟到應用程式伺服器的部署路徑下.
此種部署單元是自包含的. 他們沒有暴露給外部元件, 甚至沒暴露給相同程式的其他模組. 與基於RAR的
ApplicationContext
互動通常通過它與其他模組共享的JMS目標發生. 基於RAR的ApplicationContext
也可能定時作業或與檔案系統種的檔案互動. 如果它需要允許從外界同步訪問, 它可以(舉例來說)到處RMI端點,端點是用來在相同機器上被其他模組使用的.
1.16 BeanFactory
BeanFactory
API提供了基本的Spring IoC容器功能. 它的專門的約定幾乎是用來與Spring其他部分或相關三方框架整合, 在高階的容器GenericApplicationContext
種, 它的DefaultListableBeanFactory
實現是一個關鍵的代理.
BeanFactory
和相關的介面(如BeanFactoryAware
,InitializingBean
,DisposableBean
)是與其他元件重要的整合點. 不需要任何註解或反射, 他們允許有效的在容器和它的元件間互動. 應用級別的bean可能使用相同的回撥介面但一般更喜歡宣告式的DI, 不管通過註解或通過程式設計方式配置.
注意核心BeanFactory
API和它的DefaultListableBeanFactory
實現不會假設配置格式或任何使用的元件註解. 所有這些調劑通過擴充套件(如XmlBeanDefinitionReader
和AutowiredAnnotationBeanPostProcessor
)並在共享的作為後設資料呈現的BeanDefinition
物件上操作. 這是使得Spring容器靈活可擴充套件的關鍵.
1.16.1 BeanFactory
或者 ApplicationContext
?
本節解釋了BeanFactory
和ApplicationContext
容器級別的不同和啟動時的含義.
不管你是否有理由, 你應該使用ApplicationContext
, GenericApplicationContext
和它的子類AnnotationConfigApplicationContext
是為自定義啟動更一般的實現. 這些是Spring核心容器一般目的而言主要的入口點: 配置檔案載入, 觸發類路徑掃描, 程式設計註冊bean定義和註解類, 還有(從5.0開始)註冊函式式bean定義.
因為ApplicationContext
包含了BeanFactory
的所有功能, 它一般是更建議使用的, 除非有所有bean的處理控制的需求. 在ApplicationContext
(如GenericApplicationContext
實現)內, 按慣例幾種bean被探測(也就是, 通過名字或型別-特別的,post-processors), 而DefaultListableBeanFactory
並不知道任何特定的bean.
對許多容器擴充套件特性來說, 如註解處理和AOP代理, BeanPostProcessor
擴充套件點是關鍵. 如果你僅僅使用普通的DefaultListableBeanFactory
, post-processors預設不會被探查和啟用. 這種情況可能是疑惑的, 因為你的配置沒有任何錯誤. 當然此場景下 , 容器需要通過附加的設定完全啟動.
下面列出了通過BeanFactory
和ApplicationContext
介面和實現提供的特性.
表9 特性矩陣
Feature | BeanFactory |
ApplicationContext |
---|---|---|
bean例項化/裝配 | 是 | 是 |
整合生命週期管理 | 否 | 是 |
BeanPostProcessor 自動註冊 |
否 | 是 |
BeanFactoryPostProcessor 自動註冊 |
否 | 是 |
MessageSource 便捷訪問(對於內部) |
否 | 是 |
內建ApplicationEvent 釋出機制 |
否 | 是 |
顯式使用DefaultListableBeanFactory
註冊一個post-processor, 你需要程式設計方式呼叫addBeanPostProcessor
如下:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions
// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());
// now start using the factory
為一個普通DefaultListableBeanFactory
應用BeanFactoryPostProcessor
, 你需要呼叫它的postProcessBeanFactory
方法, 如下:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// now actually do the replacement
cfg.postProcessBeanFactory(factory);
兩個例子中, 顯式註冊步驟是不必要的, 這就是在Spring背景的程式中, ApplicationContext
變數比DefaultListableBeanFactory
更好用. 特別是當一個典型企業應用中依賴於BeanFactoryPostProcessor
和BeanPostProcessor
例項進行擴充套件時.
AnnotationConfigApplicationContext
有通用的post-processor註冊並可能在底層通過配置註解引入更多的processor, 如@EnableTransactionManagement
. 在Spring基於註解的抽象級別, bean postprocessor的概念變成了一個微小的內部容器細節.
2. 資源
本章涵蓋了Spring如何處理資源以及在Spring中你如何與資源打交道的內容. 包含下面議題:
- 簡介
- Resource 介面
- 內建 Resource 實現
ResourceLoader
ResourceLoaderAware
介面- 作為依賴項的Resource
- 應用上下文和資源路徑
2.1 簡介
Java的標準java.net.URL
類和用來處理多種URL字首的標準處理器, 很不幸對於底層資源的訪問不是足夠的. 例如, 沒有標準的URL
實現能用來訪問需要從類路徑下或者相關的ServletContext
下的資源. 雖然可以註冊為特定的URL
字首的處理器(類似已經存在的處理http:
字首的處理器), 但一般比較複雜, 並且URL
介面仍然缺乏一些急需的功能, 例如檢查指向資源是否存在的方法.
2.2 資源介面
Spring的Resource
介面意味著對於底層資源的抽象來說功能更豐富的介面. 下面列出了Resource
介面定義:
public interface Resource extends InputStreamSource {
boolean exists();
boolean isOpen();
URL getURL() throws IOException;
File getFile() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
如同Resource
介面定義的, 它擴充套件了InputStreamSource
介面, 如下列出這個介面的定義:
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
一些重要的從Resource
介面而來的方法是:
getInputStream()
: 定位和開啟資源, 返回從資源讀取的InputStream
. 它期待每個呼叫返回全新的InputStream
. 呼叫者有義務關閉流.exists()
: 返回boolean
指示這個資源實際上是否物理上存在.isOpen()
: 返回boolean
指示資源是否表現為處理開啟的流. 如果true
,InputStream
不能被多次讀取,而且進讀取一次然後關閉以避免資源洩漏. 返回false
則通常對一般資源實現, 同時可能返回異常InputStreamResource
.getDescription()
: 返回資源的描述, 當處理資源時用於錯誤的輸出. 通常時全限定檔名或資源的url.
其他方法允許你獲取實際的表示資源的URL
或File
物件(如果底層實現是相容的並支援此功能).
Spring自身使用Resource
抽象擴充套件作為許多方法簽名的引數型別. 其他相同API中(如多個ApplicationContext
實現)的方法使用String
或簡單格式來建立Resource
到上下文實現, 或者, 通過特殊的Stirng路徑上的字首, 來使呼叫者指定必須建立使用的特定Resource
.
Resource
介面用在很多Spring或與Spring協作, 它本身是很有用的工具類, 可用於你的程式碼中, 用來訪問資源, 就算你程式碼不知道或在意Spring的其他部分. 這使得你的程式碼耦合到Spring中, 但它真的僅僅耦合了其中一小部分工具類, 這些類提供了更有用的URL
的替換並能提供其他相同目的的三方庫.
Resource
抽象功能上不替換. 它做了封裝. 例如,UrlResource
封裝了RUL並使用封裝的URL
工作.
2.3 內建資源實現類
Spring包含如下Resource
實現:
UrlResource
ClassPathResource
FileSystemResource
ServletContextResource
InputStreamResource
ByteArrayResource
2.3.1 UrlResource
UrlResource
封裝了java.net.URL
並且可以用來訪問可以用URL
訪問的物件, 如檔案,HTTP目標,FTP目標等.所有URL都有一個標準的String
表現, 有標準化的字首可以用來表明URL型別. 包含file:
來訪問檔案路徑, http:
訪問通過HTTP協議的資源, ftp:
訪問FTP資源等.
UrlResource
通過顯式使用UrlResource
建構函式來建立, 但通常, 在你呼叫包含一個表示路徑的String
引數API方法時就隱式建立了. 對於後者, JavaBeans PropertyEditor
最終決定建立Resource
哪個型別.如果路徑包含熟知的字首(如:classpath:
), 他就為這個字首建立了一個特定的Resource
. 那如果沒有可識別的字首, 就假定它是一個標準的URL字串並建立UrlResource
.
2.3.2 ClassPathResource
這個類表示應該從類路徑包含的資源. 它使用執行緒上下文類載入器, 給定的類載入器,或者一個為載入資源的類.
如果類路徑資源在檔案系統, 不在包含在jar檔案並沒有被解壓(被servlet引擎或環境)到檔案系統中, 類路徑資源Resource
實現支援作為java.io.File
解析. 為處理這種情況, Resource
實現總是支援作為java.net.URL
解析.
ClassPathResource
過顯式使用UrlResource
建構函式來建立, 但通常, 在你呼叫包含一個表示路徑的String
引數API方法時就隱式建立了. 對於後者, JavaBeans PropertyEditor
識別出特定在字串路徑上的字首classpath:
,並建立一個ClassPathResource
.
2.3.3 FileSystemResource
這是一個對java.io.File
和java.nio.file.Path
處理的Resource
實現.支援作為File
或URL
解析.
2.3.4 ServletContextResource
這是一個對ServletContext
資源的Resource
實現, ServletContext
資源解析為在相關web程式根路徑內的相對路徑.
支援流訪問或URL訪問, 但僅在web程式結構解壓並且資源以及存在於物理檔案系統時允許java.io.File
訪問. 不管是否解壓到檔案系統或直接從Jar或其他的類似資料庫的地方(這是可能的)訪問, 都依賴於Servlet容器.
2.3.5 InputStreamResource
InputStreamResource
是對於一個給定InputStream
的Resource
實現. 僅用於沒有合適的Resource
實現時. 特別的, 更應該用ByteArrayResource
或其他基於檔案的Resource
實現.
相比其他的Resource
實現, 這是一個對以及開啟資源的描述. 因此, isOpen()
返回true
. 如果你需要保持資源描述符或者你需要多次讀取流就不要使用它.
2.3.6 ByteArrayResource
這是對給定位元組陣列的Resource
實現. 它為給定陣列建立一個ByteArrayInputStream
.
對於從任何給定的byte陣列而不需要從單獨使用InputStreamResource
時是很有用的.
2.4 ResourceLoader
ResourceLoader
介面被實現為能夠返回(載入)Resource
的物件. 下面列出ResourceLoader
介面定義:
public interface ResourceLoader {
Resource getResource(String location);
}
所有程式上下文均實現了ResourceLoader
介面. 所以, 所有程式上下文可以包含Resource
例項.
當你在特定的程式上下文呼叫getResource()
方法時, 並且指定的位置路徑沒有特殊的字首, 你就得到一個特定於上下文的Resource
型別. 例如, 假設下面程式碼依賴於ClassPathXmlApplicationContext
執行.
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
依賴於ClassPathXmlApplicationContext
, 程式碼返回一個ClassPathResource
. 如果相同的方法依據FileSystemXmlApplicationContext
例項執行, 他將返回FileSystemResource
. 對於一個WebApplicationContext
, 他將返回ServletContextResource
. 每種上下文對應每種特定返回的物件.
結果, 你可以使用一種恰當的特定上下文的風格載入資源.
另一方面, 你可能也需要強制使用ClassPathResource
, 不管上下文型別是什麼, 通過指定特定的classpath:
字首, 如下:
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
相似的, 你可以強制通過指定任意標準的java.net.URL
字首來使用UrlResource
. 下面一對例項使用file
和http
字首:
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt");
下面表格簡要列出從String
物件到Resource
物件的轉化策略:
表10 資源字串
字首 | 示例 | 解釋 |
---|---|---|
classpath: | classpath:com/myapp/config.xml | 從類路徑載入 |
file: | file:///data/config.xml | 以url格式從檔案系統載入. 檢視FileSystemResource 附加說明 |
http: | http://myserver/logo.png | 從url載入 |
(none) | /data/config.xml | 依賴於底層ApplicationContext 型別 |
2.5 ResourceLoaderAware
介面
ResourceLoaderAware
介面是特定的回撥介面, 用來標識期望有ResourceLoader
引用提供的元件. 下面展示了介面定義:
public interface ResourceLoaderAware {
void setResourceLoader(ResourceLoader resourceLoader);
}
當一個類實現ResourceLoaderAware
並且部署到應用上下文中(受Spring管理)時, 它就被上下文認作ResourceLoaderAware
. 引用上下文接著呼叫setResourceLoader(ResourceLoader)
, 並將自身作為引數傳入(記住, Spring所有應用上下文都實現了ResourceLoader
介面).
因為ApplicationContext
時一個ResourceLoader
, bean也可以實現ApplicationContextAware
並使用現有的上下文直接載入資源. 儘管如此, 一般而言,如果這就是你想達到的目的, 最好使用特定的ResourceLoader
介面(被認為是個工具介面). 程式碼將僅耦合到資源載入介面而不是整個SpringApplicationContext
介面.
在程式元件中, 你也可以依賴ResourceLoader
自動裝配, 作為ResourceLoaderAware
實現的替代. 傳統的constructor
和byType
自動裝配模式(如在自動裝配協作者中描述的)能為建構函式引數或setter方法提供ResourceLoader
. 問了更多的靈活性(包括自動裝配域和多引數方法), 考慮使用基於註解的自動裝配特性. 這種情況下,ResourceLoader
自動裝配給 使用@Autowired
註解的任何域,構造引數或方法引數. 更多資訊參看:使用@Autowired
.
2.6 Resource作為依賴
如果一個bean將要通過某種動態方式判定和提供資源路徑, 可能對於這個bean使用ResourceLoader
介面載入資源就是有意義的. 例如, 考慮載入某型別的模版, 這種模版資源需要依賴於使用者角色. 如果資源是固定的, 那就可以完全排除ResourceLoader
介面的使用, 暴露bean需要的的Resource
屬性, 並期望他們被注入.
然後注入這些屬性的簡單之處在於,所有應用程式上下文都註冊並使用一個特殊的JavaBeans PropertyEditor
,它可以將字串路徑轉換為資源物件. 所以, 如果myBean
有個型別為Resource
的模版屬性, 它可以被配置為一個簡單的字串, 如下:
<bean id="myBean" class="...">
<property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>
注意資源路徑沒有字首. 所以, 因為程式上下文將會被作為ResourceLoader
, 資源將通過ClassPathResource
,FileSystemResource
,ServletContextResource
, 依賴於具體的上下文型別.
如果你需要強制使用特定的Resource
型別,你可以使用字首. 下面兩個例子展示瞭如何強制一個ClassPathResource
和UrlResource
(後者被用來訪問檔案系統檔案):
<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>
2.7 應用上下文和資源路徑
本節涵蓋了如何建立有資源的上下文, 包含使用XML的捷徑, 如何使用萬用字元, 和其他細節.
2.7.1 構建應用上下文
一個程式上下文構造器(對於一個特定的應用上下文型別)一般用一個字串或字串陣列作為資源的位置路徑, 如組成上下文的定義的XML檔案.
當這個位置路徑不含有字首時, 特定的Resource
型別從這個路徑構建並載入bean定義依賴,而且時特定於上下文的. 例如, 考慮下面的例子, 建立了一個ClassPathXmlApplicationContext
:
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
bean的定義從類路徑載入, 因為使用了ClassPathResource
. 再考慮下面的例子, 建立了FileSystemXmlApplicationContext
:
ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");
現在bean定義從檔案系統路徑載入(本例中, 相對於當前工作目錄).
注意特定的類路徑字首或URL字首覆蓋了預設的載入bean定義的Resource
型別, 考慮下面例子:
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
使用FileSystemXmlApplicationContext
從類路徑載入定義. 雖然如此, 仍然是一個FileSystemXmlApplicationContext
. 如果因此作為ResourceLoader
來使用, 任何沒有字首的路徑都將被作為檔案系統路徑對待.
ClassPathXmlApplicationContext
構建例項--捷徑
ClassPathXmlApplicationContext
類暴露了很多建構函式來方便地初始化. 基本概念是, 你可以僅僅提供一個包含XML檔名本身的字串陣列(沒有引導路徑資訊),或者提供一個Class
. ClassPathXmlApplicationContext
接著從給定class獲取路徑資訊.
有如下目錄結構:
com/
foo/
services.xml
daos.xml
MessengerService.class
下面例子展示了ClassPathXmlApplicationContext
例項組合從名字為services.xml
和 daos.xml
的檔案中(在類路徑下)整合bean的定義初始化.
ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}, MessengerService.class);
檢視ClassPathXmlApplicationContext
文件獲取更多建構函式的明細.
2.7.2 程式上下文建構函式資源路徑中的萬用字元
程式上下文建構函式中資源路徑可以是簡單的路徑(如前面的例子), 每一個都一對一對映到目標Resource
, 或者相反, 可能存在指定的"classpath*:"字首, 或者內部Ant風格的正規表示式(通過使用SpringPathMatcher
工具匹配). 後面兩個都是有效的萬用字元.
這種機制的一種用處就是, 當你需要在一個組合風格的應用集中使用. 所有元件都能'publish'應用上下文定義片段到熟知的路徑中, 並且當最終上下文使用相同的路徑字首classpath*:
建立時, 所有組合的片段都自動被獲取.
注意, 建構函式中的萬用字元是特定於資源路徑並在構造時被解析. 它跟Resource
型別本身沒有關係, 你不能使用classpath*:
字首去構建實際的Resource
, 一個資源在某一時刻僅對應一個資源.
Ant風格的模式
路徑可以包含Ant風格的模式, 如下:
/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml
如果路徑包含Ant風格的模式, 解析器遵循更為複雜的處理過程去試圖解析萬用字元. 它為路徑生成一個Resource
資源到最近的非萬用字元片段幷包含了一個URL. 如果URL不是jar:
URL或者容器特定的變體(如WebLogic中的zip:
, WebSphere中的wsjar
等), 一個java.io.File
包含進來並用於通過檔案系統遍歷解析萬用字元. 如果是jar URL, 解析器也可以獲得到一個java.net.JarURLConnection
並解析這個jar URL,然後遍歷jar檔案的內容.
可移植的含義
如果特定的路徑已經是一個檔案URL(因基類ResourceLoader
是檔案系統而隱式的或顯式的), 萬用字元確保在一個完全可移植的風格下工作.
如果指定的是一個類路徑, 解析器必須包含最近的非萬用字元路徑片段URL, 這是通過呼叫Classloader.getResource()
完成的. 因為這僅僅是一個路徑的節點(不是最終檔案), 它實際上是沒有明確定義的一種URL. 實踐中, java.io.File
通常表示目錄(類路徑資源解析為檔案路徑資源)或某jar URL(類路徑解析為jar路徑). 同樣, 其中有種可移植的概念.
如果jar URL包含最近的非通配片段, 解析器必須能獲取到java.net.JarURLConnection
或者手工轉化為jar URL, 從而能訪問jar的內容並解析萬用字元. 這在一些環境下會失敗, 我們強烈建議特定環境下的jar資源萬用字元解析在特定環境下測試後再最終依賴它.
classpath*:
字首
當構建一個XML的應用上下文時, 位置字串可能使用classpath*:
字首, 如下:
ApplicationContext ctx =
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");
特定的字首指定了所有匹配到的類路徑資源必須被包含進來(內部, 這通過呼叫ClassLoader.getResources(…)
實現), 並最終合併到上下文定義中.
萬用字元類路徑依賴於底層類載入器的
getResource()
方法. 現在很多程式伺服器提供了他們自身的載入器實現, 所以這種行為可能有所差別, 特別是處理jar檔案時.classpath*
是否工作的一個簡單測試是使用載入器去載入類路徑下的一個jar檔案:getClass().getClassLoader().getResources("<someFileInsideTheJar>")
. 嘗試這類測試, 當檔案有相同的名稱但處於不同路徑下時. 萬一返回的結果不對, 檢查伺服器文件的設定, 這些設定可能會影響類載入器行為.
你也可以在路徑其他部分中使用PathMatcher
模式來組合classpath*:
(例如, classpath*:META-INF/*-beans.xml
). 這種情況下, 解析策略相當簡單: ClassLoader.getResources()
用來解析最後的非通配路徑片段來獲取所有類載入器繼承中匹配的所有資源, 然後對於每種資源外, 對萬用字元子路徑使用前面描述的路徑匹配器解析策略。
萬用字元相關的其他注意事項
注意classpath*:
當組合使用Ant格式模式時, 只有在模式開始之前,至少一個根目錄才能可靠地工作,除非實際的目標檔案駐留在檔案系統中。這意味著類似classpath*:*.xml
之類的模式可能不會從JAR檔案的根檢索檔案,而可能只從擴充套件目錄的根檢索檔案。
Spring檢索類路徑條目的能力源於JDK的ClassLoader.getResources()
方法,該方法只返回空字串的檔案系統位置(指示搜尋的潛在根路徑)。Spring也在JAR檔案中評估 URLClassLoader
載入器執行時配置和java.class.path
清單,但這並不一定會導致可移植行為。
對類路徑包的掃描要求在類路徑中存在相應的目錄條目。當您用Ant構建JAR時,不要啟用JAR任務的僅檔案開關。此外,類路徑目錄在某些環境(例如,JDK1.7.0_45及更高版本上的獨立應用程式)中可能不會根據安全策略公開(這需要在清單中設定“受信任的庫”)。參見http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。
在JDK 9的模組路徑(jigsaw)上,Spring的類路徑掃描一般按預期工作。將資源放在專用目錄中也是非常值得推薦的,避免了搜尋JAR檔案根級別的上述可移植性問題。
具有classpath:
的Ant模式:如果要搜尋的根包在多個類路徑位置中可用,則不能保證資源找到匹配的資源。考慮以下資源位置示例:
com/mycompany/package1/service-context.xml
現在考慮可能有人想要Ant模式中找到檔案:
classpath:com/mycompany/**/service-context.xml
這樣的資源可能只位於一個位置,但是當使用前面的示例這樣的路徑試圖解析它時,解析器將關閉getResource("com/mycompany");
返回的(第一個)url;。如果此基本包節點存在於多個類載入器位置,則可能不存在實際的結束資源。因此,在這種情況下,您應該更喜歡使用具有相同Ant樣式的ClassPath*:
模式,它搜尋包含根包的所有類路徑位置。
FileSystemResource
附加說明
FileSystemResource
沒有附加到FileSystemApplicationContext
(也就是,當FileSystemApplicationContext
不是真正的ResourceLoader
)時, 它對待絕對路徑和相對路徑能按預期處理. 相對路徑相對於當前工作目錄,而絕對路徑相對於檔案系統的根。
當FileSystemApplicationContext
是ResourceLoader
時, 因向後相容而有所變化. FileSystemApplicationContext
強制所有依附的FileSystemResource
例項以相對路徑方式對待路徑, 不管他們是否以字首"/"開頭. 實踐中, 下面兩種方式是等同的:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx =
new FileSystemXmlApplicationContext("/conf/context.xml");
下面兩種方式也是等同的(就算區別開他們有意義, 因為一個是相對的一個是絕對的):
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");
實踐中, 如果你需要絕對路徑, 你應該避免使用FileSystemResource
或FileSystemXmlApplicationContext
. 並通過file:
字首強制使用UrlResource
. 如下展示瞭如何這麼做:
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
new FileSystemXmlApplicationContext("file:///conf/context.xml");