03 高級裝配
3.1 環境與profile
對於某些特定的bean或配置,在開發環境、QA環境和生產環境所需要的配置是不一致的,例如數據庫配置、加密算法、外部系統的集成等
對於這個問題,經典的解決方案是:
在單獨的配置類或配置文件中配置每個可能變化的bean,然後在構建階段決定要將哪一個配置編譯到可部署的應用中
這種方式的問題是在於要為每種環境重新構建應用
從開發階段遷移到QA階段時,重新構建也許不是什麽大問題,但是,從QA階段遷移到生產階段時,重新構建可能引入bug並且會在QA團隊的成員中帶來不安的情緒
對於這個問題,Spring提供了不需要重新構建的解決方案
1.配置profile bean
Spring為環境相關的bean所提供的解決方案其實與構建時的方案沒有太大的差別,還是根據環境決定該創建那個bean和不創建哪個bean
不過,Spring並不是在構建的時候做出這樣的決策,而是等到運行時再來確定
這樣的結果就是一個部署單元能夠適用於所有的環境,沒有必要進行重新構建
這種思想有點類似靜態聯編與動態聯編的區別、又類似於自己new一個對象和利用反射構造對象的區別
Spring3.1引入了bean profile的功能
要使用profile,首先要將所有不同的bean整理到一個或多個profile之中,然後在將應用部署到每個環境時,要確保對應的profile處於激活狀態即可
可使用@Profile指定某個bean所對應的環境,只有對應的環境被激活時,才會構造對應的Bean,否則@Bean註解會被忽略掉
若在XML中,可以重復利用<beans>的profile屬性定義對應的環境
2.激活profile
Spring根據spring.profiles.active和spring.profiles.default兩個變量來確定哪個profile處於激活狀態
如果設置了spring.profiles.active屬性,就會用它來確定哪個profile是激活的
如果沒有設置spring.profiles.active,那麽會去查找spring.profiles.default的值
如果二者都沒有設置,表示沒有激活的profile,那麽只會創建那些沒有在profile中的bean
有多種方式來設置這兩個屬性:
DispatcherServlet的初始化參數:<init-param>標簽
Web應用上下文參數:<context-param>標簽
JNDI條目
作為環境變量
作為JVM的系統屬性
在測試類上,使用@ActiveProfiles註解設置
建議的做法:
開發階段使用DispatcherServlet的參數將spring.profiles.default設置開發環境的profile
一般為了兼顧ContextLoaderListener,還會在Web上下文中設置相同的profile
當應用部署到QA、生產或者其他生產環境時,負責部署的人根據情況使用系統屬性、環境變量或者JNDI設置spring.profiles.active即可
當設置了spring.profiles.active後,spring.profiles.default的值已經無所謂了,系統會優先使用spring.profiles.active的值
執行單元測試時,可以使用@ActiveProfiles註解設置profile,已激活相應的生產環境
3.2 條件化的bean
Spring4引入@Conditional註解,用於滿足某些條件時,才實例化某些Bean
@Conditional參數為一個Class,且這個類要求實現Condition接口,當調用matches方法的返回值為true時,才創建bean,否則不差UN高進
編寫matches方法時,涉及對ConditionContext好AnnotatedTypeMetadata對象的使用
Spring4開始,對@Profile註解進行了重構,使其基於@Conditional和Condition實現
3.3 處理自動裝配的歧義性
當有多個同類型的bean,Spring無法按類型自動裝配時(可能會嘗試按名稱),會拋出NoUniqueBeanDefinitionException異常
可以使用@Primary註解標註首選Bean,告知Spring在歧義時首選這個Bean
在XML中可使用bean的primary="true"完成相同的功能
如果配置了多個首選Bean,那麽仍然會進一步導致歧義問題
此時可以考慮使用@Qualifier註解為bean設置自己的限定符,然後在註入時用@Qualifier定義要註入的bean的id
其實,@Autowired會先嘗試按類型裝配,若有多個同類型,則嘗試按變量名自動裝配,若都沒有,自動裝配失敗
因此,只需@Qualifier定義的名字和@Autowired的變量名稱一致即可省略裝配時的@Qualifier註解
還可以使用自定義註解,同時添加多個限定條件,來表示某個Bean可以同時具有多個名字
3.4 bean的作用域
Spring中定義了多種作用域,可以基於這些作用域創建bean:
單例:singleton,在整個應用中,只創建bean的一個實例
原型:prototype,每次註入或者獲取這個bean時,都會創建一個新的bean實例(如和Struts2整合時就要使用這個作用域)
會話:session,在web應用中,為每個會話創建一個bean實例
請求:在Web應用中,為每個請求創建一個bean實例
默認情況下,Spring應用上下文中的bean都是以單例的形式創建的,如果要使用其他的作用域,可使用@Scope註解
@Scope註解可以與@Component或@Bean註解一起使用
普通工程常見作用域
@Scope("singleton") = @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON),這個也是默認值
@Scope("prototype") = @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
web工程常見的作用域
@Scope("request") = @Scope(WebApplicatonContext.SCOPE_REQUEST)
@Scope("session") = @Scope(WebApplicatonContext.SCOPE_SESSION)
在XML中,可以使用<bean>元素的scope屬性來設置作用域
1.使用會話和請求作用域
註意使用請求會話作用域時,要設置proxyMode屬性,該屬性用於解決將會話或請求作用域的bean註入到單例的bean中所導致的問題
對於一個單例的bean,Spring上下文加載時就會進行創建,這個bean可能要求註入一個機遇請求或會話作用域的bean
但容器啟動吧階段,用於沒有發出請求,這個bean是不存在的,因此Spring註入的是一個代理的Bean,這個bean暴露的接口和真正的bean的接口是一致的
當proxyMode設置為ScopedProxyMode.INTERFACES,該代理技術要求這個bean要實現接口,以把接口委托給代理類
我猜測底層估計使用的是JDK的動態代理實現的,JDK的動態代理技術要求被代理的類必須實現接口
當proxyMode設置為ScopedProxyMode.TARGET_CLASS時,采用CGLIB來生成代理
由於CGLib是采用繼承技術實現的代理,因此對於被代理的類只要不是final,能被繼承即可
2.在XML中生命作用域代理
可以使用<bean>標簽的子標簽<aop:scoped-proxy/>設置代理技術
默認情況下,它會使用CGLib創建目標類的代理,若想更改,可以將proxy-target-class屬性設置為false
<aop:scoped-proxy proxy-target-class="false"/>
3.5 運行時註入
有時可能希望避免硬編碼,而是在運行時再確定,Spring提供兩種在運行時求值的方式
可使用註解@PropertySource("classpath:/db.properties")引入相關配置文件,然後在代碼中自動註入Environment,獲取properties
Environment簡單介紹
String getProperty(String key)
String getProperty(String key, String defaultValue)
T getProperty(String key, Class<T> type)
T getProperty(String key, Class<T> type, T defaultValue)
前兩個方法以String類型返回,且第二個具有默認值
後兩個可以指定目標類型,例如連接池數量,可以指定為Integer類型,並且可以設置一個默認值
如果對應的key不存在,則返回null,如果希望屬性必須定義,可以使用getRequiredProperty()方法
此時如果對應的屬性沒有定義,會拋出IllegalStateException
可以使用containsProperty檢查屬性是否存在
Environment還提供了一系列方法來檢查哪些profile處於激活狀態
String[] getActiveProfiles():返回激活profile名稱的數組
String[] getDefaultProfiles():返回默認profile名稱的數組
boolean acceptsProfiles(String... profiles):如果environment支持給定的profile,就返回true
解析屬性占位符
在XML中,占位符的使用形式為${...}
在JavaConfig中,使用@Value註解使用占位符設置資源
為了使用占位符,必須配置一個PropertyPlaceholderConfigurer或PropertySourcesPlaceholderConfigurer
Spring3.1後推薦使用PropertySourcePlaceholderConfigurer,因為它能基於Environment及其屬性源來解析占位符
如果使用XML配置,<context:property-placeholder/>會自動生成PropertySourcePlaceholderConfigurer
使用Spring表達式語言進行裝配
Spring3引入了Spring表達式語言,SpEL,它能以一種強大和簡介的方式將值裝配到bean屬性和構造器參數中,使用的表達式會在運行時計算得到值
SpEL有很多特性,包括:
使用bean的ID來引用bean
調用方法和訪問對象的屬性
對值進行算術、關系和邏輯運算
正則表達式匹配
集合操作
與屬性占位符${}類似,SpEL表達式要放到#{...}之中
T()表達式會將java.lang下的包作為基礎路徑,如#(System)表示java.lang.System類,T(String)表示String類
在SpEL表達式中可以使用ID引用其他bean
可以通過systemProperties對象引用系統屬性
如果使用JavaConfig配置,則利用@Value註解填寫SpEL表達式
如果使用XML,則一般填寫在property或construct-arg的value屬性中
常見字面值:
#{3.1415}為浮點數
#{9.87E4}為科學計數法表示的浮點數
#{‘Hello‘}為字符串
#{true}為Boolean類型
可以使用.來引用屬性或者調用方法
此外,?.運算符可以確保對象的非空性,若為空則不引用屬性或者調用方法,表達式返回null
例如,#{artist.selectArtist()?.toUpperCase()}
在SpEL中使用類型
T()運算符的結果其實是一個Class對象,如果需要的話,甚至可以裝配到一個Class類型的bean屬性中
但T()真正的價值在於可以訪問目標類型的靜態方法和常量,例如#{T(java.lang.Math).random()}
SpEL常用運算符
*, ^, +, ==, eq, ? :, ?:
計算正則表達式
spEL通過matches運算符支持表達式中的模式匹配,返回true或者false
#{admin.email matches ‘[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+\\.com‘}
計算集合
選取集合中的元素:#{jukebox.song[4].title}
選取String中給定下標字符:#{‘This id a test‘[3]}
.?[]查詢運算符,可用於對集合元素進行過濾:#{jukebox.songs.?[artist eq ‘Aerosmith‘]}
.^[]在集合中查詢第一個匹配的項
.$[]在集合中查詢最後一個匹配的項
.![]可用於投影操作:#{jukebox.song.![title]}
03 高級裝配