Spring系列(零) Spring Framework 文件中文翻譯

罪惡斯巴克發表於2019-03-09

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).


第一部分 總覽

內容:

  1. 我們為什麼以"Spring"命名
  2. Spring及Spring框架的歷史
  3. 設計哲學
  4. 反饋和貢獻
  5. 入門

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生態中整合了獨立的規範:

  1. Servlet API (JSR 340)

  2. WebSocket API (JSR 356)

  3. Concurrency Utilities (JSR 236)

  4. JSON Binding API (JSR 367)

  5. Bean Validation (JSR 303)

  6. JPA (JSR 338)

  7. JMS (JSR 914)

  8. 如果需要的話,還有 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.beansorg.springframework.context 這兩個包是IoC容器的基礎. BeanFactory 介面提供了能夠管理任何物件型別的高階配置機制. ApplicationContextBeanFactory 的一個子類介面. 增加以下功能:

  • 易於與Spring的AOP特性整合.
  • 訊息資源處理(國際化)
  • 事件釋出
  • 應用程式層次的特定上下文,例如:在Web程式中的WebApplicationContext.

簡言之, BeanFactory 提供了配置框架和基本的功能, ApplicationContext 增加了諸多企業特性功能. ApplicationContextBeanFactory 的一個完整超集, 在本章中專門用於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 介面的幾個實現. 在獨立應用中, 通常會建立一個ClassPathXmlApplicationContextFileSystemXmlApplicationContext的例項.XML是傳統的定義配置的格式, 你也可以通過一小段XML配置來啟用這些支援的格式, 指定容器使用Java註解或者程式碼格式配置.

在很多的應用場景下, 並不需要顯式的例項化一個或多個Spring的IoC容器. 例如, 在Web應用中,web.xml檔案中大概八行類似的樣板化的XML描述就足夠了(參看Web程式中便捷的ApplicationContext例項). 如果你使用Spring Tool Suite(一種Eclipse增強開發環境), 能夠很輕鬆地用幾次點選滑鼠和幾個按鍵生成這樣的樣板配置.

下圖從較高層次展示了Spring如何工作. 你的程式類和配置後設資料時結合在一起的, 因此,當ApplicationContext建立並例項化後, 你就有了一個可執行系統或程式.
The Spring Ioc Container

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類和兩個資料讀取物件JpaAccountDaoJpaItemDao(根據JPA物件關係對映標準). name指類中的屬性,表示bean的名稱, ref元素指向另一個bean定義. 在idref元素之間的聯絡表明了物件間的協作依賴關係. 關於物件依賴配置的更多細節, 參看"依賴項".

結合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.xmlclasspath:/config/services.xml. 但這樣就不靈活地將路徑耦合到程式配置中了.一般的做法是可以使用一種間接方式-例如佔位符"${...}", 這樣系統JVM可以在執行時解析到正確路徑上

名稱空間本身提供了匯入指令特性. 比純bean定義更高階的配置特性Spring也有提供. 例如contextutil名稱空間.

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配置中,idname屬性用來做識別符號. id用來精確指定一個id. 按照習慣, 這些名稱是字母數字組成的('myBean', 'someService', etc.), 但他們也可以包含特殊字元. 如果你想給bean指定別名,你可以將他們賦值給name屬性, 用逗號,分號或者空格分割. 在Spring3.1之前, id是定義為一個xsd:ID型別, 只能是字母. 自從3.1開始將其定義為xsd:string型別. 注意id的唯一性依然是容器強制的, 而不是XML解析器.

給beannameid屬性不是必須的. 如果沒有指定, 容器會為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) {
        // ...
    }
}

假設ThingTwoThingThree沒有繼承關係, 沒有潛在的二義性. 因此, 下面的配置能很好的起作用, 在<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標記, 可以使用@ConstructorPropertiesJDK註解顯式新增到建構函式的引數上. 看起來如同下面例子:

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名為clienttargetName屬性的值不會被校驗. 僅僅是在client被實際例項化的時候會發生型別轉化(大多數情況下是嚴重錯誤). 如果clientbean是個原型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 localref 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被容器解析和初始化後, 最終的例項將有一個adminEmailsProperties集合, 包含了合併父集合與子集合中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實現型別之上的無序集合型別.

集合合併的限制

不能合併不同的集合型別(如MapList進行合併). 如果這樣做, 會丟擲相應異常. 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>

當beansomethingaccount屬性準備注入時, 它的泛型型別資訊被反射獲取到. 因此, 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"/>建立了從beanjohnjane的引用, 第二個定義則使用了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>

somethingbean有個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處理時, 其啟動時lazybean不會被立即初始化, 而not.lazybean將立即被初始化.

儘管如此, 當一個延遲初始化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, 致命錯誤將發生

通過byTypeconstructor裝配模式, 你可以裝配陣列和泛型集合. 這種情況下, 容器中所有型別匹配的候選物件都將提供以滿足依賴. 對於Map,如果key的型別是String,你就可以自動裝配. 一個自動裝配的Map例項的值是由所有匹配型別的bean組成的, 這個例項的key包含對應bean的名稱.

自動裝配的限制和不足

自動裝配使用在貫穿整個專案的過程中能工作得很好. 如果不是普遍使用, 而只是使用到一兩個bean上時會搞得開發人員頭暈.

參考自動裝配的限制和不足:

  • propertyconstructor-arg設定的顯式依賴總是會覆蓋自動裝配. 不能自動裝配簡單型別,StringClass(或者這些型別的陣列). 這個限制是專門設計的.

  • 比起顯式裝配, 自動裝配精確度較低. 雖然, 正如前面表格提到的, Spring非常小心地避免在多個期望結果下導致的二義性進行猜測. Spring管理的物件間的關係以及不是顯式文件定義的了.

  • 裝配資訊可能是不可用的, 對於從Spring容器生成的文件的工具而言.

  • 容器內多個bean定義可能會匹配到自動裝配的setter型別或構造引數型別.對於陣列,或者Map例項, 這不是一個問題. 然而對於期望單一值的依賴, 這種二義性不能被隨意處理, 如果沒有唯一bean可用,異常將會丟擲.

對於後面的幾種場景, 你可能有如下幾個選擇:

  • 拋棄自動裝配, 擁抱顯式裝配

  • 設定bean的autowire-candidatefalse以防止自動裝配, 正像下一節描述的.

  • 通過設定<bean/>元素的primary屬性為true將其定義為優先匹配物件.

  • 使用有更多細粒度控制的註解驅動配置, 如在註解配置中描述的.

從自動配置中排除bean

在單個bean級別, 你可以從自動裝配中排除bean.在XML配置中, 可以通過設定bean的autowire-candidatefalse. 容器能使得特定bean排除在自動裝配之外(包括註解格式的配置如@Autowired).

autowire-candidate屬性被設計為僅對基於型別的裝配有效. 對於通過name引用的顯式引用無效, 即使指定的bean沒有被標記為候選也會被解析. 結果就是當名字匹配時, 通過name自動裝配仍然會注入bean.

你可以通過基於bean名稱的模式匹配去限制bean的自動裝配. 根級別元素<beans/>通過屬性default-autowire-candidate接收一個或多個模式.例如:限制名稱以Repository結尾的bean的候選狀態, 可以使用模式*Repository. 可以通過逗號分隔多個模式. bean元素上的autowire-candidate屬性的值truefalse總是有優先權. 對於這些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如何工作:
單例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的單例作用域:

原型bean

(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時還需要做少量的配置. (對於標準的作用域singletonprototype,初始化設定是不需要的)

如何完成初始化設定取決於你特定的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的例項. 也就是loginActionbean在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一樣, 你可以隨意修改例項的內部狀態, 而其他由同一個userPreferencesbean定義生成的 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>, 這個物件有幾個訪問方式的變體, 包含getIfAvailablegetIfUnique.

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的定義並對比上面表述的作用域(注意下面userPreferencesbean定義是不完整的):

<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是單例的: 它是每個容器唯一的, 並且它的依賴(在本例中是userPreferencesbean)僅僅注入了一次. 這意味著userManagerbean都是在相同的userPreferences物件上進行操作的(也就是最初被注入的那個).

這不是你想要的結果, 當將一個短生命週期的bean注入到長生命週期的bean中時(例如, 注入一個HTTP Session作用域的協作bean到單例bean中). 相反, 你需要一個單例的userManager物件, 並且對於HTTPsession生命週期, 你需要userPreferences物件並將其指定為HTTP Session. 因此, 容器建立了一個暴露了與UserPreferences類相同公共介面的物件.(理想情況下這個物件是UserPreferences例項), 這個物件能夠從作用域機制中(HTTP request,session等)獲取到真實的物件. 容器將代理物件注入到userManagerbean, 它並不知道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的作用域機制是可擴充套件的. 你可以自定義或者重新定義已經存在的作用域, 雖然稍後會知道這不是最佳實踐而且你不能重寫內建的singletonprototype作用域.

建立自定義作用域

為了整合自定義的作用域到容器中, 你需要實現介面org.springframework.beans.factory.config.Scope, 這個介面將會在本節描述. 對於如何實現自定義作用域的觀念, 參看Spring框架實現的Scope實現還有Scopejava文件, 裡面解釋了更多需要實現的方法.

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內建的此類名稱是singletonprototype. 方法的第二個引數是自定義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的特性. 本節按下面方式組織:

  • 生命週期回撥
  • ApplicationContextAwareBeanNameAware
  • 其他Aware介面

1.6.1 生命週期回撥

為了與容器管理的bean的生命週期互動, 你可以實現Spring InitializingBeanDisposableBean 介面. 容器會為前者呼叫afterPropertiesSet(),為後者呼叫destroy() 以使bean執行在bean初始化和銷燬時應該做的操作.

在現代Spring程式中,JSR-250 的 @PostConstruct@PreDestroy 註解被認為是獲取生命週期回撥的最佳實踐. 使用這些註解意味著你的bean不會耦合到Spring特定的介面中. 更多資訊請參看:使用@PostConstruct@PreDestroy.

如果你不想用JSR-250註解但仍然想去除耦合, 考慮後設資料中使用 init-methoddestroy-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上的公共closeshutdown方法. (任何實現了java.lang.AutoCloseablejava.oi.Closeable的類將會匹配到) 你也可以為<beans>元素分配給屬性default-destroy-method一個特定值, 用來在所有bean上應用動作(參看預設初始化和銷燬方法). 注意, 這個java程式碼配置的預設行為.

預設初始化和銷燬方法

當你不用Spring特定的InitializingBeanDisposableBean 介面回撥的初始化和銷燬方法時, 你一般編寫方法的名稱類似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-methoddestroy-method重寫預設的方法.

Spring容器擔保在bean的依賴全部被提供後能夠立即呼叫配置的初始化回撥. 因此,初始化回撥發生在原始bean引用上, 這意味著AOP注入還沒有應用到bean上. 一個完整的目標bean是首先被建立然後一個AOP代理注入鏈被引用. 如果目標bean和代理被單獨定義, 你的程式碼就能夠繞開代理與目標bean互動. 因此, 在init方法上應用注入是不合邏輯的, 因為這樣將耦合目標bean的生命週期到它的代理或注入器, 並且當你的程式碼與原始的目標bean直接互動時留下奇怪的語義.

組合生命週期機制

從Spring2.5開始, 控制bean生命週期行為有三種選擇:

  • 回撥介面 InitializingBeanDisposableBean
  • 自定義的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()方法被顯式呼叫, 但它發生在上下文關閉時. 另一方面, 重新整理回撥賦予了SmartLifecyclebean的另一種特性. 當上下文被重新整理(在所有物件建立和初始化後), 回撥就被呼叫了. 此時, 預設的生命週期處理器檢查每個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 ApplicationContextAwareBeanNameAware

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引用的替代方式. "傳統的"constructorbyType裝配模型(如同在裝配協作者一節描述的)能分別為構造器引數或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介面

除了(之前討論的)ApplicationContextAwareBeanNameAware外, 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中可用 <embed>
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, 否則應用程式上下文將(嘗試)預例項化這個abstractbean.

1.8 容器擴充套件點

通常, 攻城獅不需要ApplicationContext的子類實現. 相反, Spring容器可以通過實現特定介面的方式擴充套件. 以下幾節描述了這些介面.

1.8.1 使用BeanPostProcessor自定義bean

BeanPostProcessor介面定義了一些回撥方法, 你可以實現提供你自己的(或者覆蓋預設的)初始化邏輯, 依賴解析邏輯等. 如果你想要在容器完成初始化, 配置和例項化bean之後實現一些自己的邏輯, 你可以插入一個或多個BeanPostProcessor的實現.

你可以配置多個BeanPostProcessor例項, 並能夠通過order屬性的值來控制這些例項執行的順序.如果BeanPostProcessor實現了Ordered介面, 你就可以設定這個屬性了. 如果你寫了自己的BeanPostProcessor, 你就也應該考慮實現Ordered介面. 更多細節可參看javadoc關於BeanPostProcessorOrdered介面的部分. 也可以參看: 程式設計註冊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介面. 更多細節參看:BeanFactoryPostProcessorOrdered的java文件.

如果你想修改實際的bean例項(也就是通過配置後設資料建立的物件), 那你就需要使用BeanPostProcessor(之前章節討論的通過BeanPostProcessor自定義bean). 雖然技術上可以使用BeanFactoryPostProcessor與bean例項協作(例如, 通過使用BeanFactory.getBean()), 但這樣做將導致早產的bean例項, 侵犯了標準的Spring容器生命週期.這樣容易導致副作用, 例如繞開bean處理.

同時BeanFactoryPostProcessor例項是容器級別的作用域. 這僅僅在使用容器層次結構的時候是有意義的. 如果你容器中定義了一個BeanFactoryPostProcessor, 那他就只在這個容器定義的bean起作用.在一個容器中的bean定義不能被另一個容器中的BeanFactoryPostProcessor處理, 就算兩個容器都是相同層次結構的一部分.

一個bean工廠的post-processor, 當它在ApplicationContext中宣告時自動執行, 為了給定義在容器中的後設資料做出修改. Spring包含很多預定義的工廠post-processor, 例如PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer. 你也可以使用自定義的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, 也就是在ApplicationContextpreInstantiateSingletons()階段.

示例: 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使用, 其中包含了driverurl屬性.

組合屬性名稱也是受支援的, 只要路徑path的每個被複寫的部分都是非空, 最終屬性除外(假設是使用構造器初始化的). 在接下來的例子中, beantomfred屬性的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為myBeanFactoryBean, 呼叫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上的註解. 這意味著如果你為一個DispatcherServletWebApplicationContext上新增標籤<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;
    }

    // ...
}

確保你的目標元件(如MovieCatalogCustomerPreferenceDao)和使用@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的名稱被認為是預設的指定值. 因此,你可以定義一個idmain的bean替代內部的指定元素, 結果是一樣的. 雖然你可以使用bean名稱引用特定的bean, @Autowired根本上是使用可選語義的修飾語進行型別驅動注入.這就意味著限定的值,即便有名稱後備, 總是會限定在匹配型別範圍內. 他們語義上不會表達為對一個beanid的引用. 好的限定值是main,MEMApersistent, 表達獨立於bean的id外的一種特定角色, 這個bean的id可能是自動生成的匿名定義, 就像前面例子中一樣.

限定還可被用到集合, 就像前面討論的一樣, 如:Set<MovieCatalog>. 這種情況下, 所有匹配的bean, 根據宣告的限定, 是作為集合注入. 這暗示了限定符不需要是唯一的. 相反他們構成了過濾標準. 例如, 你可以使用相同的限定值"action"定義多個MovieCatalogbean, 所有他們都被注入到使用@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/>標籤的子元素, 然後為其指定typevalue來匹配你自定義的註解. 型別通過註解類的全限定名匹配. 最為替換, 如果沒有名字衝突, 你可以使用短名稱. 下面例子展示了全部兩種方式:

<?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
}

自動包裝的域是用自定義限定註解的, 其包含兩個屬性:genreformat, 如下所示:

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.PostConstructjavax.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元素時, AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor兩者都是隱式包含的. 這意味著這兩個元件被自動探測並一起包裝了--都沒有任何XML提供的bean配置後設資料.

你可以通過包含annotation-config屬性並設定為false來關閉 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor.

1.10.4 使用過濾器來自定義掃描

預設情況下, 用@Component, @Repository, @Service, @Controller標記的類, 或者使用@Component元註解的自定義註解標記的類是探測的候選元件. 儘管這樣, 你也可以通過應用過濾器修改和擴充套件行為. 為註解@ComponentScan新增includeFiltersexcludeFilters引數(或者component-scan元素的include-filterexclude-filter子元素). 每個元素都需要typeexpression屬性, 下面的表格描述了過濾選項:

表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的處理或應用其他限制.

你可能宣告@Beanstatic, 允許呼叫它們,而無需將其包含的配置類作為例項建立。這在定義post-processor的bean時有特殊的意義(例如, 型別BeanFactoryPostProcessorBeanPostProcessor), 因為這樣的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名稱生成器會返回首字母小寫的不合格類名. 例如, 付過下面的元件類被探測到, 他們的名字應該是myMovieListermovieFinderImpl.

@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元素中使用qualifiermeta子元素來實現的. 當依賴於類路徑掃描的自動探測元件時, 你可以在候選類上新增限定後設資料註解. 下面三個例子展示了這種技術:

@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.Namedjavax.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註解方法. 即使如此, 他們總是跟@Configurationbean一起使用的.

@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,Dependency1Dependency2使用了諸如@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程式

一種WebApplicationContextAnnotationConfigApplicationContext變體是AnnotationConfigWebApplicationContext. 當配置ContextLoaderListenerSevlet監聽器, 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-methoddestroy-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有個公有的closeshutdown方法自動參與銷燬回撥. 如果你定義了相關的方法又不想在容器關閉時呼叫,你可以新增@Bean(destroyMethod="")到bean的定義來關閉預設的(inferred)推斷模式.

你可能想在獲取JNDI資源時預設這麼做, 這種資源的生命週期是在程式外管理的. 特別的, 確保在操作DataSource時總是這麼做, 因為在Java EE 程式伺服器上已知是有問題的.

下面示例展示瞭如何阻斷自動的銷燬回撥:

@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
    return (DataSource) jndiTemplate.lookup("MyDS");
}

此外, 使用@Bean方法, 通常使用程式設計方式使用JNDI查詢, 也使用Spring的JndiTemplateJndiLocatorDelegate幫助類或者直接的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() {
        // ...
    }
}
@Scopescoped-proxy

通過作用域代理, Spring提供了一種方便的方法與作用域依賴協作. 最簡單的方式是在XML配置<aop:scoped-proxy/>新增建立的代理.在Java中配置bean使用@Scope註解的proxyMode屬性提供了等效的支援. 預設沒有代理(ScopedProxyMode.NO), 但可以指定ScopedProxyMode.TARGET_CLASSScopedProxyMode.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指定多個名稱, 也就是別名. @Beanname屬性可以接收一個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.classConfigB.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類在上下文初始化過程中處理得非常早,通過這種方式強制注入依賴項可能會導致意外的早期初始化。儘可能使用基於引數的注入,如前面的示例所示。

同時請特別小心的使用BeanPostProcessorBeanFactoryPostProcessor定義. 這些應該總是宣告為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;
}

檢視@Conditionaljava文件獲取更多細節.

組合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引用它, 並且也不可能顯式通過名字被容器獲取. 相似的, DataSourcebean僅僅是按型別裝配, 所有顯式的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目錄. 我們的dataSourcebean現在如下:

@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>

上面例子中, dataSourcebean在 productionus-east都被啟用情況下暴露出來.

啟用profile

現在我們升級了配置, 我們依然需要通知Spring哪個profile被啟用. 如果我們啟動樣例程式, 我們會看到一個NoSuchBeanDefinitionException異常丟擲, 因為容器找不到名叫dataSource的bean.

可以有幾種方式啟用profile, 但最直接的是通過使用ApplicationContextEnvironment 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, 全部層級如下, 最上面事最高優先順序的:

  1. ServletConfig 引數 (如果恰當, 例如, 對於DispatcherServlet上下文)
  2. ServletContext 引數(web.xml 上下文引數)
  3. JNDI環境變數(java:comp/env/入口)
  4. JVM 系統屬性(-D命令列引數)
  5. JVM 系統環境變數(作業系統環境變數)

最重要的, 整個機制是可配置的. 也許你有自定義的propertis源想要整合到這裡. 這麼做的話, 繼承和例項化你自己的PropertySource併為當前Environment新增PropertySources. 如下:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

前面程式碼中, MyPropertySource被以最大優先順序新增. 如果它包含my-property屬性, 這個屬性將被找到並返回, 優先於任何其他PropertySource中的my-property. MutablePropertySourcesAPI暴露了很多允許精確操作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被載入, 它就自動載入上下文中定義的MessageSourcebean. 這個類必須有名字messageSource. 如果找到這個bean, 所有前面的方法的呼叫將委託給訊息源. 如果沒有訊息源被找到, ApplicationContext嘗試找到包含相同名稱的父容器. 如果找到了, 這個bean就作為MessageSource使用. 如果ApplicationContext不能找到任何訊息源, 則空的DelegatingMessageSource被初始化用來接受上面定義的方法的呼叫.

Spring提供了兩個MessageSource實現, ResourceBundleMessageSourceStaticMessageSource. 二者都實現了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,exceptionwindows在你的類路徑下. 任何解析訊息的請求都是以通過物件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的檔案中, 這個檔案在你類路徑的根目錄下. messageSourcebean定義通過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()將釋出該事件.這裡, "啟動"意味著所有的Lifecyclebean接收到顯式的啟動訊號. 一般而言, 這個訊號在顯式停止後用來重啟bean, 但它也用來啟動那些沒有被配置為自動啟動的bean(例如, 沒有被初始化啟動的元件).
ContextStoppedEvent ConfigurableApplicationContext介面的方法stop()呼叫後ApplicationContext被停止是釋出該事件. 這裡"停止"意味著所有Lifecyclebean接收一個顯式的停止訊號. 結束的上下文可能通過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>

總體來看, 當emailServicesendEmail()方法被呼叫後, 如果有任何在黑名單郵件訊息, 自定義的事件BlackListEvent就釋出了. blackListNotifierbean作為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是建立的真實實體. 例如, 你可以僅僅通過EntityCreatedEventPerson建立事件監聽器:

@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 檔案來說:

  1. 打包所有程式類到RAR檔案(標準的JAR檔案, 只是字尾不同). 新增所需的庫JAR到RAR架構的根下. 新增META-INF/ra.xml部署描述符(見javadocSpringContextResourceAdapter)還有相關的XML定義檔案(典型的如META-INF/applicationContext.xml).

  2. 將結果RAR檔案丟到應用程式伺服器的部署路徑下.

此種部署單元是自包含的. 他們沒有暴露給外部元件, 甚至沒暴露給相同程式的其他模組. 與基於RAR的ApplicationContext互動通常通過它與其他模組共享的JMS目標發生. 基於RAR的ApplicationContext也可能定時作業或與檔案系統種的檔案互動. 如果它需要允許從外界同步訪問, 它可以(舉例來說)到處RMI端點,端點是用來在相同機器上被其他模組使用的.

1.16 BeanFactory

BeanFactoryAPI提供了基本的Spring IoC容器功能. 它的專門的約定幾乎是用來與Spring其他部分或相關三方框架整合, 在高階的容器GenericApplicationContext種, 它的DefaultListableBeanFactory實現是一個關鍵的代理.

BeanFactory和相關的介面(如BeanFactoryAware,InitializingBean,DisposableBean)是與其他元件重要的整合點. 不需要任何註解或反射, 他們允許有效的在容器和它的元件間互動. 應用級別的bean可能使用相同的回撥介面但一般更喜歡宣告式的DI, 不管通過註解或通過程式設計方式配置.

注意核心BeanFactoryAPI和它的DefaultListableBeanFactory實現不會假設配置格式或任何使用的元件註解. 所有這些調劑通過擴充套件(如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)並在共享的作為後設資料呈現的BeanDefinition物件上操作. 這是使得Spring容器靈活可擴充套件的關鍵.

1.16.1 BeanFactory 或者 ApplicationContext?

本節解釋了BeanFactoryApplicationContext容器級別的不同和啟動時的含義.

不管你是否有理由, 你應該使用ApplicationContext, GenericApplicationContext和它的子類AnnotationConfigApplicationContext是為自定義啟動更一般的實現. 這些是Spring核心容器一般目的而言主要的入口點: 配置檔案載入, 觸發類路徑掃描, 程式設計註冊bean定義和註解類, 還有(從5.0開始)註冊函式式bean定義.

因為ApplicationContext包含了BeanFactory的所有功能, 它一般是更建議使用的, 除非有所有bean的處理控制的需求. 在ApplicationContext(如GenericApplicationContext實現)內, 按慣例幾種bean被探測(也就是, 通過名字或型別-特別的,post-processors), 而DefaultListableBeanFactory並不知道任何特定的bean.

對許多容器擴充套件特性來說, 如註解處理和AOP代理, BeanPostProcessor擴充套件點是關鍵. 如果你僅僅使用普通的DefaultListableBeanFactory, post-processors預設不會被探查和啟用. 這種情況可能是疑惑的, 因為你的配置沒有任何錯誤. 當然此場景下 , 容器需要通過附加的設定完全啟動.

下面列出了通過BeanFactoryApplicationContext介面和實現提供的特性.

表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更好用. 特別是當一個典型企業應用中依賴於BeanFactoryPostProcessorBeanPostProcessor例項進行擴充套件時.

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.

其他方法允許你獲取實際的表示資源的URLFile物件(如果底層實現是相容的並支援此功能).

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.Filejava.nio.file.Path處理的Resource實現.支援作為FileURL解析.

2.3.4 ServletContextResource

這是一個對ServletContext資源的Resource實現, ServletContext資源解析為在相關web程式根路徑內的相對路徑.

支援流訪問或URL訪問, 但僅在web程式結構解壓並且資源以及存在於物理檔案系統時允許java.io.File訪問. 不管是否解壓到檔案系統或直接從Jar或其他的類似資料庫的地方(這是可能的)訪問, 都依賴於Servlet容器.

2.3.5 InputStreamResource

InputStreamResource是對於一個給定InputStreamResource實現. 僅用於沒有合適的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. 下面一對例項使用filehttp字首:

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實現的替代. 傳統的constructorbyType自動裝配模式(如在自動裝配協作者中描述的)能為建構函式引數或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型別,你可以使用字首. 下面兩個例子展示瞭如何強制一個ClassPathResourceUrlResource(後者被用來訪問檔案系統檔案):

<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.xmldaos.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)時, 它對待絕對路徑和相對路徑能按預期處理. 相對路徑相對於當前工作目錄,而絕對路徑相對於檔案系統的根。

FileSystemApplicationContextResourceLoader時, 因向後相容而有所變化. 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");

實踐中, 如果你需要絕對路徑, 你應該避免使用FileSystemResourceFileSystemXmlApplicationContext. 並通過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");

相關文章