1.環境與profile
Spring爲環境相關的bean所提供的解決方案其實與 構建時的方案沒有太大的差別。當然,在這個過程中需要根據環境決定創建哪個bean和不創建哪個bean。不過Spring並不是在構建的時候做出這樣的決策,而是等到運行的時再來確定。這樣的結果就是同一個部署單元(可能會使war文件)能夠適用於所有的環境,沒有必要重新 構建。
在3.1版本中,Spring引入了bean profile的功能。要使用profile,首先需要將所有不同的bean定義整理到一個或多個profile之中,在將應用部署到每個環境時,要確保對應的profile處於激活(active)的狀態。
在Java配置中,使用@Profile註解指定某個bean屬於哪一個profile。
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig{
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder();
}
}
@Profile應用在了類級別上,它會告訴Spring這個配置類中的bean只有在dev profile激活時纔會創建。
從Spring 3.2開始,可以在方法級別上使用@Profile註解,與@Bean註解一起使用。
@Configuration
public class DevelopmentProfileConfig{
@Bean(destroyMethod="shutdown")
@Profile("dev")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder();
}
}
而沒有指定profile的bean始終都會被創建,與激活哪一個profile沒有關係。
在XML中配置profile
我們也可以通過<beans>
元素的profile屬性,在XML中配置profile bean。
<beans
...
profile="dev">
<jdbc:embedded-database id="dataSource">
...
</jdbc:embedded-database>
</beans>
你可以在根<beans>
元素中嵌套定義<beans>
元素,而不是爲每個環境都創建一個profile XML文件。這能將所有的profile bean定義放到同一個XML文件中。
激活profile
Spring在確定哪個profile處於激活狀態時,需要依賴兩個獨立的屬性:spring.profiles.active和spring.profiles.default。
如果設置了active屬性的話,那麼它的值就會用來確定哪個profile是激活的。但如果沒有設置spring.profiles.active屬性的話,那Spring將會查找spring.profiles.default的值。如果這兩個屬性都沒有設置的話,那就沒有激活的profile,因此只會創建那些沒有定義在profile中的bean。
有多種方式來設置這兩個屬性:
- 作爲DispatcherServlet的初始化參數
- 作爲Web應用的上下文參數
- 作爲JDNI條目
- 作爲環境變量
- 作爲JVM的系統屬性
- 在集成測試類上,使用@ActiveProfiles註解設置
例如,在Web應用中,設置spring.profiles.default的Web.xml文件如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- 爲上下文設置默認的profile -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 爲Servlet設置默認的profile -->
<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>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
按照這種方式設置spring.profiles.default,所有的開發人員都能從版本控制軟件中獲得應用程序源碼,並使用開發環境的設置(如嵌入式數據庫)運行代碼,而不需要任何額外的配置。
當應用程序部署到QA、生產或其他環境之中時,負責部署的人根據情況使用系統屬性、環境變量或JNDI設置spring.profiles.active即可。當設置active以後,default設置成什麼值已經無所謂了,系統會優先使用spring.profiles.active中所設置的profile。而且,你可以同時激活多個profile,通過列出多個profile名稱,並以逗號分隔來實現。
Spring提供了@ActiveProfiles註解,來指定測試時要激活哪個profile。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
...
}
在條件化創建bean方面,Spring的profile機制是一種很棒的方法,這裏的條件要基於哪個profile處於激活狀態來判斷。Spring 4.0提供了一個更爲通用的機制來實現條件化的bean定義。
2.條件化的bean——使用Spring 4和@Conditional註解定義條件化的bean
Spring4 引入了一個新的@Conditional註解,可以用到帶有@Bean註解的方法上。如果給定的條件計算結果爲true,就會創建這個bean,否則的話這個bean會被忽略。
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
設置給@Conditional的類可以是任意實現了Condition接口的類型。(這個接口實現起來很簡單,只需要提供matches()方法的實現即可,如果返回matches()方法返回true,那麼就會創建帶有@Conditional註解的bean。如果matches()方法返回false,將不會創建這些bean。)
matches()方法會得到ConditionContext和AnnotatesTypeMetadata對象用來做出決策。
通過ConditionContext,我們可以做到如下幾點:
- 藉助getRegistry()返回的BeanDefinitionRegistry檢查bean定義
- 藉助getBeanFactory()返回的ConfigurableListableBeanFactory檢查bean是否存在,甚至探查bean的屬性
- 藉助getEnviroment()返回的Environment檢查環境變量是否存在以及它的值是什麼
- 讀取並探查getResourceLoader()返回的ResourceLoader所加載的資源
- 藉助getClassLoader()返回的ClassLoader加載並檢查類是否存在
AnnotatedTypeMetadata則能夠讓我們檢查帶有@Bean註解的方法上還有其他的註解。藉助isAnnotated()方法,我們能夠判斷帶有@Bean註解的方法是不是還有其他特定的註解。藉助其他的那些方法,我們能夠檢查@Bean註解的方法上其他註解的屬性。
從Spring 4 開始,@Profile註解進行了重構,使其基於@Conditional和Condition實現。
3.處理自動裝配的歧義性
當自動注入時遇到有多個可選方案時,系統會拋出NoUniqueBeanDefinitionException異常。
Spring提供了兩種方法來解決歧義性:
- 標示首選的bean:當創建bean的時候,將其中一個可選的bean設置爲首選(primary)bean,當遇到歧義性的時候,Spring將會使用首選的bean。@Primary能夠與@Component組合用在組件掃描的bean上,也可以與@Bean組合用在Java配置的bean聲明中。
@Component
@Primary
public class IceCream implements Dessert {...}
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
XML:
<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />
2.限定自動裝配的bean
@Qualifier註解是使用限定符的主要形式。它可以與@Autowired和@Inject協同使用,在注入的時候指定想要注入的是哪個bean。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert)
{
this.dessert = dessert;
}
@Qualifier(“iceCream”)所引用的bean要具有String類型的“iceCream”作爲限定符。如果沒有指定其他的限定符的話,所有的bean都會給定一個默認的限定符,這個限定符與bean的ID相同。
我們可以使用@Qualifier和@Component組合使用來自定義類限定符,而不是依賴bean ID作爲限定符。
@Component
@Qualifier("cold")
public class IceCream implements Dessert {...}
@Qualifier也可以與@Bean一起使用
創建自定義註解
我們也可以創建自定義的限定符註解,藉助這樣的註解來表達bean所希望限定的特性。所需要做的就是創建一個註解,其本身要使用@Qualifier註解來標註。例如,創建一個@Cold註解
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}
使用:
@Component
@COld
@Creamy
public class IceCream implements Dessert {...}
@Autowried
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
在這裏@Cold和@Creamy都是自定義的限定符註解,它們可以組合使用。
4.bean的作用域
在默認情況下,Spring應用上下文中所有bean都是作爲以單例(singleton)的形式來創建的。
Spring定義了多種作用域,可以基於這些作用域創建bean,包括:
- 單例(Singleton):在整個應用中,只創建bean的一個實例。
- 原型(Prototype):每次注入或者通過Spring應用上下文獲取的時候,都會創建一個新的bean的實例。
- 會話(Session):在Web應用中,爲每個會話創建一個bean實例。
- 請求(Request):在Web應用中,爲每次請求創建一個bean實例。
單例是默認的作用域,如果選擇其他的作用域,可以使用@Scope註解,例如聲明爲原型:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}
這裏使用ConfigurableBeanFactory類的SCOPE_PROTOTYPE常量設置了原型作用域,也可以直接使用@Scope(“prototype”),只不過上面更加安全並且不易出錯。
使用xml的話,可以使用<bean>
元素的scope屬性來設置作用域:
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
使用會話和請求作用域
@Bean
@Scope(
value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES
)
public ShoppingCart car() {...}
將其注入到單例StoreService bean的Setter方法中:
@Component
public class StoreService{
@Autowried
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
因爲StoreService是一個單例bean而ShoppingCart是一個會話作用域bean,所以,當StoreService實例創建的時候,ShoppingCart並沒有創建,而且由於這兩個bean之間是一對多的關係,我們並不想讓一個固定的ShoppingCart bean實例注入到StoreService中。
實際上,Spring也並不會將ShoppingCart bean注入到StoreService中,Spring會注入一個到ShoppingCart bean的代理,這個代理會暴露於ShoppingCart相同的方法,所以StoreService會認爲它就是一個ShoppingCart 實例(其實並不然)。但是,當StoreService調用ShoppingCart的方法時,代理會對其進行懶解析並將調用委託給當前會話作用域內真正的ShoppingCart bean。
如之前的代碼所示,proxyMode屬性被設置成了ScopedProxyMode.INTERFACES,這表明這裏代理實現ShoppingCart接口,並將調用委託給時間bean。如果ShoppingCart不是接口而是類的話,需要將proxyMode屬性設置爲ScopedProxyMode.TARGET_CLASS,以此來表明要以生成目標類擴展的方式創建代理。
使用xml的話:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scope-proxy proxy-target-class="false" />
</bean>
默認情況下,它會使用CGLib創建目標代理,也就是類代理。通過將proxy-target-class屬性設置爲false,進而要求它生成基於接口的代理。
5.運行時值注入
Spring提供了兩種在運行時求值的方式:
- 屬性佔位符
- Spring表達式語言
在Spring中處理外部值的最簡單方式就是聲明屬性源並通過Spring的Enviroment來檢索屬性。
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties") //聲明屬性源
public class EcpressiveConfig {
@Autowried
Environment env;
@Bean
public BlankDisc disc() {
return new BlankDisc(
env.getProperty("disc.title");//檢索屬性值
)
}
}
深入學習Spring的Environment
Environment的getProperty方法有四個重載的變種形式:
- 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 defualtValue)
如果希望一個屬性必須要定義,可以使用getRequiredProperty()方法,如果這個屬性沒有定義的話,將會拋出IllegalStateException異常。
檢查某個屬性是否存在:env.containsProperty(“disc.artis”);
將屬性解析爲類:getPropertyAsClass();
Environment還提供了一些方法來檢查哪些profile處於激活狀態
- String[] getActiveProfiles();返回激活profile名稱的數組;
- String[] getDefaultProfiles();返回默認profile名稱的數組;
- boolean acceptsProfiles(String…profiles):如果Environment支持給定profile的話,就返回true
解析屬性佔位符
在Spring裝配中,佔位符的形式爲“${…}”包裝的屬性名稱。
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" />
如果我們依賴於組件掃描和自動裝配來創建和初始化應用組件的話,那麼就米有指定佔位符的配置文件或類了。在這種情況下,我們可以使用@Value註解,它的使用方法與@Autowired註解非常類似:
public BlankDisc(@Value("${disc.title}") String title) {
this.title = title;
}
爲了使用佔位符,我們需要配置一個PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean.從Spring 3.1開始,推薦使用後一個,因爲它能夠基於Spring Environment及其屬性源來解析站位符。
@Bean
public static PropertySourcesPlaceholderConfigurer placehoiderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
如果使用xml配置的話,Spring context命名空間的<context:property-placeholder>
元素將會爲你生成PropertySourcesPlaceholderConfigurer bean:
<beans
...
記得要添加命名空間,太多此處已省略,哈哈>
<context:property-placeholder />
</beans>
使用Spring表達式語言進行裝配(SpEL)
Spring3 引入了Spring表達式語言,它能夠以一種強大和簡介的方式將值裝配到bean屬性和構造器參數中,在這個過程中所使用的表達式會在運行時計算得到值。例如:
public BlankDisc(@Value("#{systemProperties['disc.title']}" String title)){
this.title = title;
}
這裏#{systemProperties[‘disc.title’]}就是SpEL表達式。
至於SpEl,由於在這裏不能面面俱到的介紹它,將在後面用專門的一篇來介紹。