高級裝配
環境與profile
在開發軟件的時候,將應用程序從開發環境,遷移到測試環境,或者是遷移到生產環境都是一項挑戰。最常見的就是對於DataSource的配置,這三種環境我們會根據不同的策略來生成DataSource bean。我們需要有一種方式來配置DataSource,使其在每種環境下都會選擇對應的配置。
配置profile bean
要使用profile,首先要將所有不同的bean定義整理到一個或多個profile之中,在將應用部署到不同環境中,要確保對應的profile文件處於激活狀態。
在Java配置中,使用@Profile註解指定某個bean屬於哪一個profile。
importjavax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; @Configuration @Profile("dev") public class DevelopProfileConfig { @Bean(destroyMethod="shutdown") public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:scheam.sql") .addScripts("classpath:test-data.sql") .build(); } }
在這裏@profile註解是應用在類級別了。它告訴Spring這個配置類中的bean只有在dev profile 激活時在會創建。如果dev profile沒有激活的話,那麽這個配置類下的bean會被忽略。
如果我們為每一個配置環境都去配置一個配置類,那無疑會增加配置類的個數,不利於維護。從Spring3.2開始,就可以在方法級別上使用@profile註解,與@Bean註解一同使用。這樣的話,我們就可以將多個bean放在一個配置類中了。
import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.jndi.JndiObjectFactoryBean; @Configuration public class DataSourceConfig { @Bean(destroyMethod="shutdown") @Profile("dev") public DataSource embeddedDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:scheam.sql") .addScript("classpath:test-data.sql") .build(); } @Bean @Profile("prod") public DataSource jndiDataSource() { JndiObjectFactoryBean jndiObjectFactoryBean=new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); } }
只有在對應的profile激活時,相應的bean才會被創建,但是可能會有其他的bean並沒有聲明在一個給定的profile範圍內。沒有指定profile的bean始終會被創建,與激活哪個profile無關。
在XML配置中,我們可以使用<beans>元素的profile屬性,在XML中配置profile bean。
在這裏使用beans元素中嵌套定義beans元素的方式,而不是為每個環境都創建一個profile XML文件,這樣將所有的profile bean定義在同一個XML文件中
profile.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:jdbc="http://www.springframework.org/schema/jdbc" xmlns:p="http://www.springframework.org/schema/p" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <beans profile="dev"> <jdbc:embedded-database id="datasource"> <jdbc:script location="classpath:scheam.sql"/> <jdbc:script location="classpath:test-data.sql"/> </jdbc:embedded-database> </beans> <beans profile="qa"> <bean id="datasource" class="org.apach.commons.dbcp.BesicDataSource" destroy-method="close" p:url="jdbc:h2:tcp://dbserver/~/test" p:driverClassName="org.h2.Driver" p:username="sa" p:password="password" p:initialSize="20" p:maxActive="30" /> </beans> <beans profile="prod"> <jee:jndi-lookup id="datasource" jndi-name="jdbc/myDatabase" resource-ref="true" proxy-interface="javax.sql.DataSource" ></jee:jndi-lookup> </beans> </beans>
激活profile
Spring在確定哪個profile處於激活狀態時,需要依賴兩個屬性:spring.profiles.active和spring.profiles.default。如果設置了spring.profiles.active就會根據它的值確定哪個profile是激活的。但如果沒有設置spring.profiles.active屬性的話,Spring就會去找spring.profiles.default的值.如果這兩個屬性都沒與設置,那就沒有激活的profile。
可以有多種方式設置這兩個屬性:
- 作為DispatcherServlet的初始化參數
- 作為Web應用的上下文參數
- 作為JNDI條目
- 作為環境變量
- 作為JVM的系統屬性
- 在集成測試類上,使用@ActiveProfiles註解設置
我在這裏使用前兩種方式,前兩種方式可以直接在web.xml中設置:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>springDispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- <init-param> <param-name>spring-profiles.default</param-name> <param-value>dev</param-value> </init-param> --> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springDispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param> <context-param> <param-name>spring.profiles.defaule</param-name> <param-value>dev</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app>
當應用部署到其他環境時,我們根據情況再來設置spring.profiles.active即可.spring.profiles.active的設置優先使用。可以註意到spring.profiles.active和spring.profiles.default的profiles是復數形式,可以設置多個profile名稱,並以逗號分隔,我們可以設置多個彼此不相關的profile。
使用profile進行測試
在運行集成測試的時候,Spring提供了@ActiveProfiles註解,我們可以死哦那個它來制定運行測試要激活哪個profile.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= {Config.class}) @ActiveProfiles("dev") public class PersistenceTest { ... }
條件化創建bean
Spring的profile機制是根據哪個profile處於激活狀態來條件化初始bean的一種體現。Spring4中可以使用一種更為通用的機制來實現條件化bean的定義,在這種機制下,條件完全由自己決定。Spring4使用的是@Conditional註解定義條件化bean。
@Conditional註解可以用在@Bean註解的方法上。如果給定條件為true,就會創建這個bean,否則的話,這個bean會被忽略。
@Bean @Conditional(MagicConditon.class) public MagicBean magicBean() { return new MagicBean(); }
@conditional註解給定了一個Class,它指明了條件——在這裏是MagicCondition(實現了Condition接口)。給定@conditional註解的類可以是任意實現了Condition接口的類型。
public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
這個接口比較簡單,只有一個matches()方法作為實現,如果該方法返回true就會創建提供了@Conditional註解的bean,否則就不會創建.
matches()方法會得到ConditionContext和AnnotatedTypeMetadata兩個入參。ConditionContext接口會有一些方法檢查bean的定義或bean的屬性,AnnotatedTypeMetadata則能夠讓我們檢查帶有@Bean註解的方法上還有其他註解,或是檢查@Bean註解的方法上其他註解的屬性.
在Spring4開始,@profile註解就是根據@Conditional和Condition實現的,我們可以參照它的實現。
處理自動裝配的歧義性
使用Spring的自動封裝來裝配bean真的是太方便了,但是,這是有且只有一個bean匹配所需條件時,自動裝配才有效。如果有多個bean匹配結果的話,這種歧義性會阻礙Spring自動裝配。例如使用@Autowired註解標註Dessert接口
@Autowired private Dessert dessert;
但是這個接口有三個實現:
@Component public class Cake implements Dessert {} @Component public class Cookies implements Dessert {} @Component public class IceCream implements Dessert {}
這三個實現類都用@Component註解,當組件掃描的時候,它們都會在Spring上下文中創建bean。然後當Spring試圖自動裝配Dessert的時候,它沒有唯一,無歧義的值。Spring就會拋出NoUniqueBeanDefinitonException。
其實在使用中,自動裝配的歧義性並不常見,因為一般情況是給定的類型只有一個實現類,自動裝配可以很好地運行。但是,如果真的出現歧義性,Spring提供了:可以將某一個可選bean設為首選(primary)的bean,或者使用限定(qualifier)來幫助Spring將可選的bean‘範圍縮小到只有一個bean.
標示首選bean
當聲明bean的時候,通過將其中一個可選的bean設置為首選(primary)bean能夠避免自動裝配的歧義性。當出現歧義性的時候會直接使用首選的bean。Spring使用的正是@Primary註解,這個註解能夠與@Component組合在組件掃描的bean上,也可以與@Bean組合用在Java配置的bean聲明上。
@Component @Primary public class Cake implements Dessert {}
或者使用的是JavaConfig則如下:
@Bean @Primary public Dessert iceCream(){ return new IceCream(); }
如果使用XML配置bean的話,同樣可以實現這樣的功能。<bean>元素有一個primary屬性來指定首選的bean:
<bean id="iceCream" class="cn.lynu.IceCream" primary="true"/>
但是,如果你標記了多個首選bean,那麽又會帶來新的歧義性,事實上也就沒有了首選,Spring也無法正常工作。解決歧義性使用限定符是一種更為強大的機制
限定自動裝配的bean
使用首選bean只能標示一個優先的選擇方案,當首選bean數量超過一個,我們就沒有其他方法進一步縮小範圍。而且,我們每次使用該類型,都會自動使用首選bean,如果在某處不想使用就不好處理了。使用限定可以縮小可選bean的範圍直到只有一個bean滿足條件,如果還存在歧義性,那麽你還可以繼續使用更多的限定來進一步縮小範圍。@Qualifier註解就是限定,它可以與@Autowired協同使用,在註入時指定想要註入的是哪個bean。
@Autowired @Qualifier("iceCream") private Dessert dessert;
我們已經知道了使用@Component註解聲明的類,默認使用bean的ID是首字母小寫的類名。因此@Qualifier("iceCream")指向的正是IceCream類的實例.這種情況是將限定與要註入的bean的名稱是緊密耦合的,對類名的任何修改都會影響限定失敗。這個時候,我們可以使用自定義的限定。
自定義的限定
我們可以自定義的設置限定,而不是依賴beanID作為限定,在這裏@Qualifier可以與@Component或@Bean配合使用。
組件:
@Component @Qualifier("cold") public class IceCream implements Dessert {}
或是在JavaConfig方式中:
@Bean @Qualifier("cold") public Dessert iceCream() { return new IceCream(); }
這樣我們在註入的時候就可以使用"cold"這個名稱了:
@Autowired @Qualifier("cold") private Dessert dessert;
使用自定義的限定註解
當一個限定不能解決問題的時候,可以使用多個限定來縮小範圍,也就是使用多個@Qualifier,但是在Java中不允許出現兩個相同的註解(Java8可以,但也要求該註解本身實現@Repeatable註解,不過,Spring的@Qualifier沒有實現),這個時候就需要我們創建一個註解了,該註解使用@Qualifier標註,就直接使用我們自定義的註解即可。
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold {}
表明bean限定的時候就可以用:
@Component @Clod public class IceCream implements Dessert {}
我們在註入的時候直接使用這個@Cold註解即可:
@Autowired @Cold private Dessert dessert;
我們可以自定義多個註解來進一步縮小範圍
bean的作用域
在默認情況下,Spring應用上下文的bean都是作為單例的形式被創建,不管給定的bean被註入到其他的bean多少次,每次註入的都是同一個實例。但有時我們使用的類是易變的,這個時候使用單例就不太好了,因為對象會被修改。
Spring提供了多種作用域,可以基於這些作用域創建bean:
- 單例(singleton):這整個應用中只創建bean的一個實例
- 原型(Prototype):每次註入的時候都會創建一個新的bean的實例(多例)
- 會話(Session):在Web開發中,為每一個會話創建一個bean實例
- 請求(Request):在Web開發中,為每一個請求創建一個bean實例
需要選擇除單例之外的作用域,要使用@Scope註解,它可以與@Component或@Bean一起使用。
@Component @Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class Notepad {}
使用ConfigurableBeanFactory.SCOPE_PROTOTYPE常量比較安全,當也可以使用字面量的方式:@Scope("prototype")
如果使用JavaConfig的方式:
@Bean @Scop(value=ConfigurableBeanFacory.SCOPE_PROTOTYPE) public Notepad notepad(){ return Notepad(); }
如果使用XML的方式,可以在<bean>元素的scope屬性設置:
<bean id="notepad" class="cn.lynu.Notepad" scope="prototype"/>
使用會話或請求作用域的bean
使用會話或請求作用域,還是用@Scope註解,它的使用方式與原型作用域大致相同:
@Component @Scope(value="session",proxyMode=ScopedProxyMode.TARGET_CLASS) public class ShoppingCart {}
Spring會在Web應用的每一個會話中創建一個ShoppingCart實例,但是在對每一個的會話操作中,這個bean實際相當於單例. 註意這裏使用了proxyMode屬性,這個屬性可以解決將會話或請求bean註入到單例bean中所遇到的問題:例如將ShoppingCart bean註入到單例的StoreService bean
@Component public class StoreService{ @AutoWired private ShoppingCart shoppingCart; }
當掃描到StoreService 就會創建這個bean,並試圖將ShoppingCart bean註入進去,但是ShoppingCart是會話作用域的,此時不存在,只有例如某個用戶登錄系統,創建會話之後,這個bean才存在;而且會話會有多個,ShoppingCart實例也會有多個,我們不想註入某個固定的實例,應該是當需要使用ShoppingCart實例恰好是當前會話中的那一個。
使用proxyMode就會先創建一個ShoppingCart bean的代理,將這個代理給StoreService ,當StoreService 真正使用的時候,代理會調用會話中真正的ShoppingCart bean。需要註意的是:如果ShoppingCart是一個接口,需要使用 ScopedProxyMode.INTERFACES JDK動態代理,但如果ShoppingCart是一個具體類,它必須使用CDLib來做代理,必須將proxyMode屬性設置為ScopedProxyMode.TERGET_CLASS.表明要生成代理類的拓展類的方式創建代理。
在XML中聲明作用域代理
如果使用XML的方式就不能使用@Scope註解及其proxyMode屬性了。<bean>元素的scope屬性能夠設置bean的作用域,但是如何指定代理模式呢?
<bean id="cart" class="cn.lynu.ShoppingCart" scope="session"> <aop:scoped-proxy /> </bean>
默認情況下,它使用CGLib常見目標類的代理,但是我們也可以將proxy-target-class屬性設置為false,就是用JDK的代理
<bean id="cart" class="cn.lynu.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false" /> </bean>
運行時值註入
有的時候,我們可能會希望避免硬編碼,而且想讓這些值在運行的時候再確定。為了實現這種功能,Spring提供了兩種在運行時求值的方式:
- 屬性占位符
- Spring表達式語言(SpEL)
註入外部的值
在Spring中,處理外部屬性最簡單的但是就是聲明屬性源並通過Spring的Environment來檢索屬性:
@Configuration @ComponentScan @PropertySource("classpath:app.properties") public class Config { @Autowired Environment environment; @Bean public BlankDisc disc() { return new BlankDisc(environment.getProperty("disc.title"), environment.getProperty("disc.artist")); } }
文件 app.properties 就是屬性源,使用@propertySource註解聲明,這個屬性文件會加載到Spring的Environment,我們再註入Environment,就可以通過Environment的方法獲得屬性了。使用getPropperty()方法及其重載方法時,當屬指定的屬性不存在時,可以通過重載方法給一個默認值,如果沒有默認值就會返回null。如果希望這個屬性必須存在,那麽可以使用getRequiredProperty()方法,Environment還有一些其他方法。
直接從Environment中獲得屬性的值,在JavaConfig的方式中非常方便。Spring還提供了占位符裝配屬性的方法,這些占位符的值來源於一個屬性源。
使用屬性占位符
Spring中可以使用屬性占位符的方式將值插入到Spring bean中,在Spring裝配中,占位符的形式為使用"${...}"包裹的屬性名稱。
<bean id="agtPeppers" class="cn.lynu.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}"/>
這樣XML文件中沒有任何的硬編碼,屬性的值都是從屬性源中獲得。如果我們使用的是組件掃描和自動裝配,沒有使用XML的話,在這種情況下,我們使用@Value註解:
@Component public class BlankDisc { private String title; private String artist; public BlankDisc() {} public BlankDisc(@Value("${disc.title}")String title, @Value("${disc.artist}")String artist) { this.title = title; this.artist = artist; } }
最後,為了屬性占位符可以使用,我們還需要配置一個PropertySourcePlaceholdConfigurer,它可以根據Spring的Env及其屬性源來解析占位符。
@Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); }
如果使用XML,需要context名稱空間的<context:propertyplaceholder>元素,這個元素會生成PropertySourcePlaceholdConfigurer bean:
<context:property-placeholder/>
高級裝配