天天看點

Spring實戰3:裝配bean的進階知識主要内容:

environments and profiles conditional bean declaration 處理自動裝配的歧義 bean的作用域 the spring expression language

在軟體開發中,常常設定不同的運作環境:開發環境、預發環境、性能測試環境和生産環境等等。

不同的環境下,應用程式的配置項也不同,例如資料庫配置、遠端服務位址等。以資料庫配置為例子,在開發環境中你可能使用一個嵌入式的記憶體資料庫,并将測試資料放在一個腳本檔案中。例如,在一個spring的配置類中,可能需要定義如下的bean:

使用embeddeddatabasebuilder這個建構器可以建立一個記憶體資料庫,通過指定路徑下的schema.sql檔案中的内容可以建立資料庫的表定義,通過test-data.sql可以準備好測試資料。

開發環境下可以這麼用,但是在生産環境下不可以。在生産環境下,你可能需要從容器中使用jndi擷取datasource對象,這中情況下,對應的建立代碼是:

使用jndi管理datasource對象,很适合生産環境,但是對于日常開發環境來說太複雜了。

另外,在qa環境下你也可以選擇另外一種datasource配置,可以選擇使用普通的dbcp連接配接池,例如:

上述三種辦法可以為不同環境建立各自需要的javax.sql.datasource執行個體,這個例子很适合介紹不同環境下建立bean,那麼有沒有一種辦法:隻需要打包應用一次,然後部署到不同的開發環境下就會自動選擇不同的bean建立政策。一種方法是建立三個獨立的配置檔案,然後利用maven profiles的預編譯指令處理在特定的環境下打包哪個配置檔案到最終的應用中。這種解決方法有一個問題,即在切換到不同環境時,需要重新建構應用——從開發環境到測試環境沒有問題,但是從測試環境到生産環境也需要重新建構則可能引入一定風險。

spring提供了對應的方法,使得在環境切換時不需要重新建構整個應用。

spring提供的方法不是在構件時針對不同的環境決策,而是在運作時,這樣,一個應用隻需要建構一次,就可以在開發、qa和生産環境運作。

在spring 3.1之中,可以使用@profile注解來修飾javaconfig類,當某個環境對應的profile被激活時,就使用對應環境下的配置類。

在spring3.2之後,則可以在函數級别使用@profile注解(是的,跟@bean注解同時作用在函數上),這樣就可以将各個環境的下的bean定義都放在同一個配置類中,還是以之前的例子:

除了被@profile修飾的其他bean,無論在什麼開發環境下都會被建立。

和在javaconfig的用法一樣,可以從檔案級别定義環境資訊,也可以将各個環境的bean放在一個xml配置檔案中。

上述三個javax.sql.datasource的bean,id都是datasource,但是在運作的時候隻會建立一個bean。

spring提供了spring.profiles.active和spring.profiles.default這兩個配置項定義激活哪個profile。如果應用中設定了spring.profiles.active選項,則spring根據該配置項的值激活對應的profile,如果沒有設定spring.profiles.active,則spring會再檢視spring.profiles.default這個配置項的值,如果這兩個變量都沒有設定,則spring隻會建立沒有被profile修飾的bean。

有下列幾種方法設定上述兩個變量的值:

dispatcherservlet的初始化參數

web應用的上下文參數(context parameters)

jndi項

環境變量

jvm系統屬性

在內建測試類上使用@activeprofiles注解

開發人員可以按自己的需求設定spring.profiles.active和spring.profiles.default這兩個屬性的組合。

我推薦在web應用的web.xml檔案中設定spring.profiles.default屬性——通過設定dispatcherservlet的初始參數和<context-param>标簽。

按照上述方法設定spring.profiles.default屬性,任何開發人員隻需要下載下傳源碼就可以在開發環境中運作程式以及測試。

然後,當應用需要進入qa、生産環境時,負責部署的開發者隻需要通過系統屬性、環境變量或者jndi等方法設定spring.profiles.active屬性即可,因為spring.profiles.active優先級更高。

在運作內建測試時,可能希望運作跟生産環境下相同的配置;但是,如果配置重需要的beans被profiles修飾的,則需要在跑單元測試之前激活對應的profiles。

spring提供了@activeprofiles注解來激活指定的profiles,用法如下:

假設你希望隻有在項目中引入特定的依賴庫時、或者隻有當特定的bean已經被建立時、或者是設定了某個環境變量時,某個bean才被建立。

spring 4之前很難實作這種需求,不過在spring 4中提出了一個新的注解——@conditional,該注解作用于@bean注解修飾的方法上,通過判斷指定的條件是否滿足來決定是否建立該bean。

舉個例子,工程中有一個magicbean,你希望隻有當magic環境變量被指派時才建立magicbean,否則該bean的建立函數被忽略。

這個例子表示:隻有當magicexistscondition類已經存在時,才會建立magicbean。

@conditional注解的源碼列舉如下:

可以看出,傳入@conditional注解的類一定要實作condition接口,該接口提供matchs()方法——如果matches()方法傳回true,則被@conditional注解修飾的bean就會建立,否則對應的bean不會建立。

在這個例子中,magicexistscondition類應該實作condition接口,并在matches()方法中實作具體的判斷條件,代碼如下所示:

上述代碼中的matchs()方法簡單且有效:它首先擷取environment變量,然後再判斷環境變量中是否存在magic屬性。在這個例子中,magic的值是多少并不重要,它隻要存在就好。

magicexistscondition的matchs()方法是通過conditioncontext擷取了environment執行個體。matchs()方法的參數有兩個:conditioncontext和annotatedtypemetadata,分别看下這兩個接口的源碼:

利用conditioncontext接口可做的事情很多,列舉如下:

通過getregistry()方法傳回的beandefinitionregistry執行個體,可以檢查bean的定義;

通過getbeanfactory()方法傳回的configurablelistablebeanfactory執行個體,可以檢查某個bean是否存在于應用上下文中,還可以獲得該bean的屬性;

通過getenvironment()方法傳回的environment執行個體,可以檢查指定環境變量是否被設定,還可以獲得該環境變量的值;

通過getresourceloader()方法傳回的resourceloader執行個體,可以得到應用加載的資源包含的内容;

通過getclassloader()方法傳回的classloader執行個體,可以檢查某個類是否存在。

通過isannotated()方法可以檢查@bean方法是否被指定的注解類型修飾;通過其他方法可以獲得修飾@bean方法的注解的屬性。

從spring 4開始,@profile注解也利用@conditional注解和condition接口進行了重構。作為分析@conditional注解和condition接口的另一個例子,我們可以看下在spring 4中@profile注解的實作。

可以看出,@profile注解的實作被@conditional注解修飾,并且依賴于profilecondition類——該類是condition接口的實作。如下列代碼所示,profilecondition利用conditioncontext和annotatedtypemetadata兩個接口提供的方法進行決策。

可以看出,這代碼寫得不太好了解:profilecondition通過annotatedtypemetadata執行個體擷取與@profile注解相關的所有注解屬性;然後檢查每個屬性的值(存放在value執行個體中),對應的profiles别激活——即context.getenvironment().acceptsprofiles(((string[]) value))的傳回值是true,則matchs()方法傳回true。

environment類提供了可以檢查profiles的相關方法,用于檢查哪個profile被激活:

string[] getactiveprofiles()——傳回被激活的profiles數組;

string[] getdefaultprofiles()——傳回預設的profiles數組;

boolean acceptsprofiles(string... profiles)——如果某個profiles被激活,則傳回true。

在一文中介紹了如何通過自動裝配讓spring自動履歷bean之間的依賴關系——自動裝配非常有用,通過自動裝配可以減少大量顯式配置代碼。不過,自動裝配(autowiring)要求bean的比對具備唯一性,否則就會産生歧義,進而抛出異常。

舉個例子說明自動裝配的歧義性,假設你有如下自動裝配的代碼:

dessert是一個接口,有三個對應的實作:

因為上述三個類都被@component注解修飾,是以都會被component-scanning發現并在應用上下文中建立類型為dessert的bean;然後,當spring試圖為setdessert()方法裝配對應的dessert參數時,就會面臨多個選擇;然後spring就會抛出異常——nouniquebeandefinitionexception。

雖然在實際開發中并不會經常遇到這種歧義性,但是它确實是個問題,幸運的是spring也提供了對應的解決辦法。

在定義bean時,可以通過指定一個優先級高的bean來消除自動裝配過程中遇到的歧義問題。

在上述例子中,可以選擇一個最重要的bean,用@primary注解修飾:

如果你沒有使用自動掃描,而是使用基于java的顯式配置檔案,則如下定義@bean方法:

如果使用基于xml檔案的顯式配置,則如下定義:

不論哪種形式,效果都一樣:告訴spring選擇primary bean來消除歧義。不過,當應用中指定多個primary bean時,spring又不會選擇了,再次遇到歧義。spring還提供了功能更強大的歧義消除機制——@qualifiers注解。

@qualifier注解可以跟@autowired或@inject一起使用,指定需要導入的bean的id,例如,上面例子中的setdessert()方法可以這麼寫:

每個bean都具備唯一的id,是以此處徹底消除了歧義。

如果進一步深究,@qualifier("icecream")表示以"icecream"字元串作為qualifier的bean。每個bean都有一個qualifier,内容與該bean的id相同。是以,上述裝配的實際含義是:setdessert()方法會裝配一個以"icecream"為qualifier的bean,隻不過碰巧是該bean的id也是icecream。

以預設的bean的id作為qualifier非常簡單,但是也會引發新的問題:如果将來對icecream類進行重構,它的類名發生改變(例如gelato)怎麼辦?在這種情況下,該bean對應的id和預設的qualifier将變為"gelato",然後自動裝配就會失敗。

問題的關鍵在于:你需要指定一個qualifier,該内容不會受目标類的類名的限制和影響。

開發者可以給某個bean設定自定義的qualifier,形式如下:

然後,在要注入的地方也使用"cold"作為qualifier來獲得該bean:

即使在javaconfig中,也可以使用@qualifier指定某個bean的qualifier,例如:

在使用自定義的@qualifier值時,最好選擇一個含義準确的名詞,不要随意使用名詞。在這個例子中,我們描述icecream為"cold"bean,在裝配時,可以讀作:給我來一份cold dessert,恰好指定為icecream。類似的,我們把cake叫作"soft",把cookies*叫作"crispy"。

使用自定義的qualifiers優于使用基于bean的id的預設qualifier,但是當你有多個bean共享同一個qualifier時,還是會有歧義。例如,假設你定義一個新的dessertbean:

現在你又有兩個"cold"為qualifier的bean了,再次遇到歧義:最直白的想法是多增加一個限制條件,例如icecream會成為下面的定義:

而posicle類則如下定義:

在裝配bean的時候,則需要使用兩個限制條件,如下:

這裡有個小問題:java 不允許在同一個item上加多個相同類型的注解(java 8已經支援),但是這種寫法顯然很啰嗦。

解決辦法是:通過定義自己的qualifier注解,例如,可以建立一個@cold注解來代替@qualifier("cold"):

可以建立一個@creamy注解來代替@qualifier("creamy"):

這樣,就可以使用@cold和@creamy修飾icecream類,例如:

類似的,可以使用@cold和@fruity修飾popsicle類,例如:

最後,在裝配的時候,可以使用@cold和@creamy限定icecream類對應的bean:

在這一小節中,我們學習了兩種擴充spring的方式:為了建立自定義的conditional注解,我們建立一個新的注解,并用@conditional注解修飾它(@profile的實作);為了建立自定義的qualifier注解,我們建立一個新的注解,并用@qualifier注解修飾它。

預設情況下,spring應用上下文中的bean都是單例對象,也就是說,無論給某個bean被多少次裝配給其他bean,都是指同一個執行個體。

大部分情況下,單例bean很好用:如果一個對象沒有狀态并且可以在應用中重複使用,那麼針對該對象的初始化和記憶體管理開銷非常小。

但是,有些情況下你必須使用某中可變對象來維護幾種不同的狀态,是以形成非線程安全。在這種情況下,把類定義為單例并不是一個好主意——該對象在重入使用的時候可能遇到線程安全問題。

spring定義了幾種bean的作用域,列舉如下:

singleton——在整個應用中隻有一個bean的執行個體;

prototype——每次某個bean被裝配給其他bean時,都會建立一個新的執行個體;

session——在web應用中,在每次會話過程中隻建立一個bean的執行個體;

request——在web應用中,在每次http請求中建立一個bean的執行個體。

singleton域是預設的作用域,如前所述,對于可變類型來說并不理想。我們可以使用@scope注解——和@component或@bean注解都可以使用。

例如,如果你依賴component-scanning發現和定義bean,則可以用如下代碼定義prototype bean:

除了使用scope_prototype字元串指定bean的作用域,還可以使用@scope("prototype"),但使用configurablebeanfactory.scope_prototype更安全,不容易遇到拼寫錯誤。

另外,如果你使用javaconfig定義notepad的bean,也可以給出下列定義:

如果你使用xml檔案定義notepad的bean,則有如下定義:

無論你最後采取上述三種定義方式的哪一種定義prototype類型的bean,每次notepad被裝配到其他bean時,都會重新建立一個新的執行個體。

在web應用中,有時需要在某個request或者session的作用域範圍内共享同一個bean的執行個體。舉個例子,在一個典型的電子商務應用中,可能會有一個bean代表使用者的購物車,如果購物車是單例對象,則所有的使用者會把自己要買的商品添加到同一個購物車中;另外,如果購物車bean設定為prototype,則在應用中某個子產品中添加的商品在另一個子產品中将不能使用。

對于這個例子,使用session scope更合适,因為一個會話(session)唯一對應一個使用者,可以通過下列代碼使用session scope:

在這裡你通過value屬性設定了webapplicationcontext.scope_session,這告訴spring為web應用中的每個session建立一個shoppingcartbean的執行個體。在整個應用中會有多個shoppingcart執行個體,但是在某個會話的作用域中shoppingcart是單例的。

這裡還用proxymode屬性設定了scopedproxymode.interfaces值,這涉及到另一個問題:把request/session scope的bean裝配到singleton scope的bean時會遇到。首先看下這個問題的表現。

假設在應用中需要将shoppingcartbean裝配給單例storeservicebean的setter方法:

因為storeservice是單例bean,是以在spring應用上下文加載時該bean就會被建立。在建立這個bean時 ,spring會試圖裝配對應的shoppingcartbean,但是這個bean是session scope的,目前還沒有建立——隻有在使用者通路時并建立session時,才會建立shoppingcartbean。

而且,之後肯定會有多個shoppingcartbean:每個使用者一個。理想的情景是:在需要storeservice操作購物車時,storeservice能夠和shoppingcartbean正常工作。

針對這種需求,spring應該給storeservicebean裝配一個shoppingcartbean的代理,如下圖所示。代理類對外暴露的接口和shoppingcart中的一樣,用于告訴storeservice關于shoppingcart的接口資訊——當storeservice調用對應的接口時,代理采取延遲解析政策,并把調用委派給實際的session-scoped shoppingcartbean。

Spring實戰3:裝配bean的進階知識主要内容:

scoped proxies enable deferred injected of request- and session-coped beans

因為shoppingcart是一個接口,是以這裡工作正常,但是,如果shoppingcart是具體的類,則spring不能建立基于接口的代理。這裡必須使用cglib建立class-based的bean,即使用scopedproxymode.target_class訓示代理類應該基礎自目标類。

這裡使用session scope作為例子,在request scope中也有同樣的問題,當然解決辦法也相同。

如果你在xml配置檔案中定義session-scoped或者request-scoped bean,則不能使用@scope注解以及對應的proxymode屬性。<bean>元素的scope屬性可以用來指定bean的scope,但是如何指定代理模式?

可以使用spring aop指定代理模式:

<aop: scoped-proxy>在xml配置方式扮演的角色與proxymode屬性在注解配置方式中的相同,需要注意的是,這裡預設使用cglib庫建立代理,是以,如果需要建立接口代理,則需要設定proxy-target-class屬性為false:

為了使用<aop: scoped-proxy>元素,需要在xml配置檔案中定義spring的aop名字空間:

一般而言,讨論依賴注入和裝配時,我們多關注的是如何(how)實作依賴注入(構造函數、setter方法),即如何建立對象之間的聯系。

這種寫死的方式有時可以,有時卻需要避免寫死——在運作時決定需要注入的值。spring提供以下兩種方式實作運作時注入:

property placeholders

the spring expression language(spel)

在spring中解析外部值的最好方法是定義一個配置檔案,然後通過spring的environment執行個體擷取配置檔案中的配置項的值。例如,下列代碼展示如何在spring 配置檔案中使用外部配置項的值。

這裡,@propertysource注解引用的配置檔案内容如下:

屬性檔案被加載到spring的environment執行個體中,然後通過getproperty()方法解析對應配置項的值。

在environment類中,getproperty()方法有如下幾種重載形式:

string getproperty(string var1);

string getproperty(string var1, string var2);

<t> t getproperty(string var1, class<t> var2);

<t> t getproperty(string var1, class<t> var2, t var3);

前兩個方法都是傳回string值,利用第二個參數,可以設定預設值;後兩個方法可以指定傳回值的類型,舉個例子:假設你需要從連接配接池中擷取連接配接個數,如果你使用前兩個方法,則傳回的值是string,你需要手動完成類型轉換;但是使用後兩個方法,可以由spring自動完成這個轉換:

除了getproperty()方法,還有其他方法可以獲得配置項的值,如果不設定預設值參數,則在對應的配置項不存在的情況下對應的屬性會配置為null,如果你不希望這種情況發生——即要求每個配置項必須存在,則可以使用getrequiredproperty()方法:

在上述代碼中,如果disc.title或者disc.artist配置項不存在,spring都會抛出illegalstateexception異常。

如果你希望檢查某個配置項是否存在,則可以調用containsproperty()方法:<code>boolean titleexists = env.containsproperty("disc.title");</code>。如果你需要将一個屬性解析成某個類,則可以使用getpropertyasclass()方法:<code>class&lt;compactdisc&gt; cdclass = env.getpropertyasclass("disc.class", compactdisc.class);</code>

在spring中,可以使用${ ... }将占位符包裹起來,例如,在xml檔案中可以定義如下代碼從配置檔案中解析對應配置項的值:

如果你使用component-scanning和自動裝配建立和初始化應用元件,則可以使用@value注解擷取配置檔案中配置項的值,例如blankdisc的構造函數可以定義如下:

為了使用占位符的值,需要配置propertyplaceholderconfigerbean或者propertysourcesplaceholderconfigurerbean。從spring 3.1之後,更推薦使用propertysourcesplaceholderconfigurer,因為這個bean和spring 的environment的來源一樣,例子代碼如下:

如果使用xml配置檔案,則通過&lt;context:property-placeholder&gt;元素可以獲得propertysourcesplaceholderconfigurerbean:

spring 3引入了spring expression language(spel),這是一種在運作時給bean的屬性或者構造函數參數注入值的方法。

spel有很多優點,簡單列舉如下:

可以通過bean的id引用bean;

可以調用某個對象的方法或者通路它的屬性;

支援數學、關系和邏輯操作;

正規表達式比對;

支援集合操作

在後續的文章中,可以看到spel被用到依賴注入的其他方面,例如在spring security中,可以使用spel表達式定義安全限制;如果在spring mvc中使用thymeleaf模闆,在模闆中可以使用spel表達式擷取模型資料。

spel是一門非常靈活的表達式語言,在這裡不準備花大量篇幅來涵蓋它的所有方面,可以通過一些例子來感受一下它的強大能力。

首先,spel表達式被#{ ... }包圍,跟placeholders中的${ ... }非常像,最簡單的spel表達式可以寫作<code>#{1}</code>。在應用中,你可能回使用更加有實際含義的spel表達式,例如<code>#{t(system).currenttimemillis()}</code>——這個表達式負責獲得目前的系統時間,而t()操作符負責将java.lang.system解析成類,以便可以調用currenttimemillis()方法。

spel表達式可以引用指定id的bean或者某個bean的屬性,例如下面這個例子可以獲得id為sgtpeppers的bean的artist屬性的值:<code>#{sgtpeppers.artist}</code>;也可以通過<code>#{systemproperties['disc.title']}</code>引用系統屬性。

上述這些例子都非常簡單,我們接下來看下如何在bean裝配中使用spel表達式,之前提到過,如果你使用component-scanning和自動裝配建立應用元件,則可以使用@value注解獲得配置檔案中配置項的值;除了使用placeholder表達式,還可以使用spel表達式,例如blankdisc的構造函數可以按照下面這種方式來寫:

spel表達式可以表示整數值,也可以表示浮點數、string值和boolean值。例如可以使用<code>#{3.14159}</code>表式浮點數3.14159,并且還支援科學計數法——<code>#{9.87e4}</code>表示98700;<code>#{'hello'}</code>可以表示字元串值、<code>#{false}</code>可以表示boolean值。

單獨使用字面值是乏味的,一般不會使用到隻包含有字面值的spel表達式,不過在構造更有趣、更複雜的表達式時支援字面值這個特性非常有用。

spel表達式可以通過bean的id引用bean,例如<code>#{sgtpeppers}</code>;也可以引用指定bean的屬性,例如<code>#{sgtpeppers.artist}</code>;還可以調用某個bean的方法,例如<code>#{artistselector.selectartist()}</code>表達式可以調用artistselector這個bean的selectartist()方法。

spel表達式也支援方法的連續調用,例如<code>#{artistselector.selectartist().touppercase()}</code>,為了防止出現nullpointerexception異常,最好使用類型安全的操作符,例如<code>#{artistselector.selectartist()?.touppercase()}</code>。?.操作符在調用右邊的函數之前,會確定左邊的函數傳回的值不為null。

在spel中能夠調用類的方法或者常量的關鍵是t()操作符,例如通過<code>t(java.lang.math)</code>可以通路math類中的方法和屬性——<code>#{(java.lang.math).random()}</code>和<code>#{t(java.lang.math).pi}</code>。

spel提供了不同種類的操作符,如下表所示:

Spring實戰3:裝配bean的進階知識主要内容:

spel operators for manipulating expression values

在操作文本字元串時,最常用的是檢查某個文本是否符合某種格式。spel通過matches操作符支援正規表達式比對。例如:<code>#{admin.email matches '[a-za-z0-9._%+-]+@[a-za-z0-9.-]+\.com'}</code>可以檢查admin.email表示的郵件位址是否正确。

通過spel表達式還可以操作集合和數組,例如<code>#{jukebox.songs[4].title}</code>這個表達式可以通路jukebox的songs數組的第5個元素。

也可以實作更複雜的功能:随機選擇一首歌——<code>#{jukebox.songs[t(java.lang.math).random() * jukebox.songs.size()].title}</code>。

spel提供了一個選擇操作符——.?[],可以獲得某個集合的子集,舉個例子,假設你獲得jukebox中所有artist為aerosmith的歌,則可以使用這個表達式:<code>#{jukebox.songs.?[artist eq 'aerosmith']}</code>。可以看出,.?[]操作符支援在[]中嵌套另一個spel表達式。

spel還提供了其他兩個選擇操作符:.^[]用于選擇第一個比對的元素;.$[]用于選擇最後一個比對的元素。

最後,spel還提供了一個提取操作符:.![],可以根據指定的集合建立一個符合某個條件的新集合,例如<code>#{jukebox.songs.![title]}</code>可以将songs的title都提取出來構成一個新的字元串集合。

ok,spel的功能非常強大,但是這裡需要給開發人員提個醒:别讓你的spel表達式過于智能。你的表達式越智能,就越難對它們進行單元測試,是以,盡量保證你的spel表達式簡單易了解。

這一章幹貨十足,我們基于第二章介紹的bean裝配技術開始讨論,陸續介紹了關于bean裝配的一些進階知識。

首先我們介紹了通過spring的profiles解決多環境部署的問題,通過在運作時根據代表指定環境的profile選擇性建立某個bean,spring可以實作無需重新建構就可以在多個環境下部署同一個應用。

profiles bean是運作時建立bean的一種解決方案,不過spring 4提供了一個更普遍的解決方案:利用@conditional注解和condition接口實作條件性建立bean。

我們還介紹了兩種機制來解決自動裝配時可能遇到的歧義性問題:primary beans和qualifiers。盡管定義一個primary bean非常簡單,但它仍然有局限,是以我們需要利用qualifier縮小自動裝配的bean的範圍,而且,我們也示範了如何建立自己的qualifiers。

盡管大多數spring bean是單例對象,但是在某些情況下具備其他作用域的對象更加合适。spring 應用中可以建立singletons、prototypes、request-scoped或session-scoped。在使用request-scoped或者session-scoped類型的bean時,還需要解決将非單例對象注入到單例對象時遇到的問題——利用代理接口或代理類。

最後,我們也介紹了spring表達式語言(spel),利用spel可以實作在運作時給bean注入值。