3.Spring學習筆記之高級裝配

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.activespring.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提供了兩種方法來解決歧義性:

  1. 標示首選的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方法有四個重載的變種形式:

  1. String getProperty(String key)
  2. String getProperty(String key, String defaultValue)
  3. T getProperty(String key, Class<T> type )
  4. 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,由於在這裏不能面面俱到的介紹它,將在後面用專門的一篇來介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章