1. 程式人生 > >高級裝配

高級裝配

我們 import schema 開發 而且 自定義 factor blank GC

環境與profile

在開發軟件的時候,將應用程序從開發環境,遷移到測試環境,或者是遷移到生產環境都是一項挑戰。最常見的就是對於DataSource的配置,這三種環境我們會根據不同的策略來生成DataSource bean。我們需要有一種方式來配置DataSource,使其在每種環境下都會選擇對應的配置。

配置profile bean

要使用profile,首先要將所有不同的bean定義整理到一個或多個profile之中,在將應用部署到不同環境中,要確保對應的profile文件處於激活狀態。

在Java配置中,使用@Profile註解指定某個bean屬於哪一個profile。

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; @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/>

高級裝配