1. 程式人生 > >系統學習Spring(三)——Bean的高階裝配

系統學習Spring(三)——Bean的高階裝配

在軟體開發中,常常設定不同的執行環境:開發環境、預發環境、效能測試環境和生產環境等等。

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

@Bean(destroyMethod = "shutdown")
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .addScript("classpath:schema.sql"
) .addScript("classpath:test-data.sql") .build(); }

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

開發環境下可以這麼用,但是在生產環境下不可以。在生產環境下,你可能需要從容器中使用JNDI獲取DataSource物件,這中情況下,對應的建立程式碼是:

@Bean
public DataSource dataSource() {
    JndiObjectFactoryBean jndiObjectFactoryBean =
             new
JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); }

使用JNDI管理DataSource物件,很適合生產環境,但是對於日常開發環境來說太複雜了。

另外,在QA環境下你也可以選擇另外一種DataSource配置,可以選擇使用普通的DBCP連線池,例如:

@Bean(destroyMethod = "close")
public DataSource dataSource() {
    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test");
    dataSource.setDriverClassName("org.h2.Driver");
    dataSource.setUsername("sa");
    dataSource.setPassword("password");
    dataSource.setInitialSize(20);
    dataSource.setMaxActive(30);
    return dataSource;
}

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

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

配置profile beans

Spring提供的方法不是在構件時針對不同的環境決策,而是在執行時,這樣,一個應用只需要構建一次,就可以在開發、QA和生產環境執行。

在Spring 3.1之中,可以使用@Profile註解來修飾JavaConfig類,當某個環境對應的profile被啟用時,就使用對應環境下的配置類。

在Spring3.2之後,則可以在函式級別使用@Profile註解(是的,跟@Bean註解同時作用在函式上),這樣就可以將各個環境的下的bean定義都放在同一個配置類中,還是以之前的例子:

利用註解配置

package com.spring.sample.config;

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;
import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {
    @Bean(destroyMethod = "shutdown")
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }
    @Bean
    @Profile("prod")
    public DataSource dataSource() {
        JndiObjectFactoryBean jndiObjectFactoryBean =
                new JndiObjectFactoryBean();
        jndiObjectFactoryBean.setJndiName("jdbc/myDS");
        jndiObjectFactoryBean.setResourceRef(true); 
        jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource) jndiObjectFactoryBean.getObject();
    }
}

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

利用XML檔案配置

和在JavaConfig的用法一樣,可以從檔案級別定義環境資訊,也可以將各個環境的bean放在一個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/beans 
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">

       <beans profile="dev">
              <jdbc:embedded-database id="dataSource">
                     <jdbc:script location="classpath:schema.sql"/>
                     <jdbc:script location="classpath:test-data.sql"/>
              </jdbc:embedded-database>
       </beans>

       <beans profile="qa">
              <bean id="dataSource"
                    class="org.apache.commons.dbcp.BasicDataSource"
                    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"/>
       </beans>
</beans>

上述三個javax.sql.DataSource的bean,ID都是dataSource,但是在執行的時候只會建立一個bean。

啟用profiles

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的初始引數和標籤。

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">
        <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:applicationContext.xml</param-value>
      </context-param>
    
        <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>appServletName</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>appServletName</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    </web-app>

    按照上述方法設定spring.profiles.default屬性,任何開發人員只需要下載原始碼就可以在開發環境中執行程式以及測試。

    然後,當應用需要進入QA、生產環境時,負責部署的開發者只需要通過系統屬性、環境變數或者JNDI等方法設定spring.profiles.active屬性即可,因為spring.profiles.active優先順序更高。

    另外,在執行整合測試時,可能希望執行跟生產環境下相同的配置;但是,如果配置重需要的beans被profiles修飾的,則需要在跑單元測試之前啟用對應的profiles。

    Spring提供了@ActiveProfiles註解來啟用指定的profiles,用法如下:

    Conditional beans

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

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

    舉個例子,工程中有一個MagicBean,你希望只有當magic環境變數被賦值時才建立MagicBean,否則該Bean的建立函式被忽略。

    @Bean
    @Conditional(MagicExistsCondition.class)
    public MagicBean magicBean() {
        return new MagicBean();
    }

    這個例子表示:只有當MagicExistsCondition類已經存在時,才會建立MagicBean。

    @Conditional註解的原始碼列舉如下:

    package org.springframework.context.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import org.springframework.context.annotation.Condition;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface Conditional {
        Class<? extends Condition>[] value();
    }

    可以看出,傳入@Conditional註解的類一定要實現Condition介面,該介面提供matchs()方法——如果matches()方法返回true,則被@Conditional註解修飾的bean就會建立,否則對應的bean不會建立。

    在這個例子中,MagicExistsCondition類應該實現Condition介面,並在matches()方法中實現具體的判斷條件,程式碼如下所示:

    package com.spring.sample.config;
    
    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.env.Environment;
    import org.springframework.core.type.AnnotatedTypeMetadata;
    
    public class MagicExistsCondition implements Condition {
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            Environment env = conditionContext.getEnvironment();
            return env.containsProperty("magic"); //檢查magic環境變數是否被設定
        }
    }

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

    MagicExistsCondition的matchs()方法是通過ConditionContext獲取了環境例項。matchs()方法的引數有兩個:ConditionContext和AnnotatedTypeMetadata,分別看下這兩個介面的原始碼:

    //ConditionContext
    public interface ConditionContext {
        BeanDefinitionRegistry getRegistry();
        ConfigurableListableBeanFactory getBeanFactory();
        Environment getEnvironment();
        ResourceLoader getResourceLoader();
        ClassLoader getClassLoader();
    }

    利用ConditionContext介面可做的事情很多,列舉如下:

  • 通過getRegistry()方法返回的BeanDefinitionRegistry例項,可以檢查bean的定義;
  • 通過getBeanFactory()方法返回的ConfigurableListableBeanFactory例項,可以檢查某個bean是否存在於應用上下文中,還可以獲得該bean的屬性;
  • 通過getEnvironment()方法返回的Environment例項,可以檢查指定環境變數是否被設定,還可以獲得該環境變數的值;
  • 通過getResourceLoader()方法返回的ResourceLoader例項,可以得到應用載入的資源包含的內容;
  • 通過getClassLoader()方法返回的ClassLoader例項,可以檢查某個類是否存在。
  • //AnnotatedTypeMetadata
    public interface AnnotatedTypeMetadata {
        boolean isAnnotated(String var1);
        Map<String, Object> getAnnotationAttributes(String var1);
        Map<String, Object> getAnnotationAttributes(String var1, boolean var2);
        MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);
        MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2);
    }
    

    通過isAnnotated()方法可以檢查@Bean方法是否被指定的註解型別修飾;通過其他方法可以獲得修飾@Bean方法的註解的屬性。

    從Spring 4開始,@Profile註解也利用@Conditional註解和Condition介面進行了重構。作為分析@Conditional註解和Condition介面的另一個例子,我們可以看下在Spring 4中@Profile註解的實現。

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Documented
    @Conditional({ProfileCondition.class})
    public @interface Profile {
        String[] value();
    }

    可以看出,@Profile註解的實現被@Conditional註解修飾,並且依賴於ProfileCondition類——該類是Condition介面的實現。如下列程式碼所示,ProfileCondition利用ConditionContext和AnnotatedTypeMetadata兩個介面提供的方法進行決策。

    class ProfileCondition implements Condition {
        ProfileCondition() {
        }
    
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            if(context.getEnvironment() != null) {
                MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
                if(attrs != null) {
                    Iterator var4 = ((List)attrs.get("value")).iterator();
    
                    Object value;
                    do {
                        if(!var4.hasNext()) {
                            return false;
                        }
                        value = var4.next();
                    } while(!context.getEnvironment().acceptsProfiles((String[])((String[])value)));
    
                    return true;//傳給@Profile註解的引數對應的環境profiles已啟用
                }
            }
    
            return true; //預設為true
        }
    }
    

    可以看出,這程式碼寫得不太好理解: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的匹配具備唯一性,否則就會產生歧義,從而丟擲異常。

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

    @Autowired
    public void setDessert(Dessert dessert) {
        this.dessert = dessert;
    }

    Dessert是一個介面,有三個對應的實現:

    @Component
    public class Cake implements Dessert { ... }
    @Component
    public class Cookies implements Dessert { ... }
    @Component
    public class IceCream implements Dessert { ... }

    因為上述三個類都被@Component註解修飾,因此都會被component-scanning發現並在應用上下文中建立型別為Dessert的bean;然後,當Spring試圖為setDessert()方法裝配對應的Dessert引數時,就會面臨多個選擇;然後Spring就會丟擲異常——NoUniqueBeanDefinitionException。

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

    @Primary指定優先bean

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

    在上述例子中,可以選擇一個最重要的Bean,用@Primary註解修飾:

    @Component
    @Primary
    public class IceCream implements Dessert { ... }

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

    @Bean
    @Primary
    public Dessert iceCream() {
      return new IceCream();
    }

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

    <bean id="iceCream"
                 class="com.dasserteater.IceCream"
                 primary="true" />

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

    @Qualifier指定bean的ID

    @Qualifier註解可以跟@Autowired或@Inject一起使用,指定需要匯入的bean的ID,例如,上面例子中的setDessert()方法可以這麼寫:

    @Autowired
    @Qualifier("iceCream")
    public void setDessert(Dessert dessert) {
        this.dessert = dessert;
    }

    每個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,形式如下:

    @Component
    @Qualifier("cold")
    public class IceCream implements Dessert { ... }

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

    @Autowired
    @Qualifier("cold")
    public void setDessert(Dessert dessert) {
        this.dessert = dessert;
    }

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

    @Bean
    @Qualifier("cold")
    public Dessert iceCream() {
      return new IceCream();
    }

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

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

    @Component
    @Qualifier("cold")
    public class Popsicle implements Dessert { ... }

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

    @Component
    @Qualifier("cold")
    @Qualifier("creamy")
    public class IceCream implements Dessert { ... }

    而Posicle類則如下定義:

    @Component
    @Qualifier("cold")
    @Qualifier("fruity")
    public class Popsicle implements Dessert { ... }

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

    @Bean
    @Qualifier("cold")
    @Qualifier("creamy")
    public Dessert iceCream() {
      return new IceCream();
    }

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

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

    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
                      ElementType.METHOD, ElementType.TYPE})
    @Rentention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Cold { }

    可以建立一個@Creamy註解來代替@Qualifier(“creamy”):

    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
                      ElementType.METHOD, ElementType.TYPE})
    @Rentention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Creamy { }

    這樣,就可以使用@Cold和@Creamy修飾IceCream類,例如:

    @Component
    @Cold
    @Creamy
    public class IceCream implements Dessert { ... }

    類似的,可以使用@Cold和@Fruity修飾Popsicle類,例如:

    @Component
    @Cold
    @Fruity
    public class Popsicle implements Dessert { ... }

    最後,在裝配的時候,可以使用@Cold和@Creamy限定IceCream類對應的bean:

    @Autowired
    @Cold
    @Creamy
    public void setDessert(Dessert dessert) {
        this.dessert = dessert;
    }

    bean的作用域

    預設情況下,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:

    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class Notepad{ ... }

    除了使用SCOPE_PROTOTYPE字串指定bean的作用域,還可以使用@Scope(“prototype”),但使用ConfigurableBeanFactory.SCOPE_PROTOTYPE更安全,不容易遇到拼寫錯誤。

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

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Notepad notepad() {
        return new Notepad();
    }

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

    <bean id="notepad"
                class="com.myapp.Notepad"
                scope="prototype" />
    

    無論你最後採取上述三種定義方式的哪一種定義prototype型別的bean,每次Notepad被裝配到其他bean時,都會重新建立一個新的例項。

    request和session作用域

    在Web應用中,有時需要在某個request或者session的作用域範圍內共享同一個bean的例項。舉個例子,在一個典型的電子商務應用中,可能會有一個bean代表使用者的購物車,如果購物車是單例物件,則所有的使用者會把自己要買的商品新增到同一個購物車中;另外,如果購物車bean設定為prototype,則在應用中某個模組中新增的商品在另一個模組中將不能使用。

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

    @Bean
    @Scope(value=WebApplicationContext.SCOPE_SESSION,
                    proxyMode=ScopedProxyMode.INTERFACES)
    public ShoppingCart cart() { ... }

    在這裡你通過value屬性設定了WebApplicationContext.SCOPE_SESSION,這告訴Spring為web應用中的每個session建立一個ShoppingCartbean的例項。在整個應用中會有多個ShoppingCart例項,但是在某個會話的作用域中ShoppingCart是單例的。

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

    假設在應用中需要將ShoppingCartbean裝配給單例StoreServicebean的setter方法:

    @Component
    public class StoreService {
    
        @Autowired
        public void setShoppingCart(ShoppingCart shoppingCart) {
            this.shoppingCart = shoppingCart;
        }
        ...
    }

    因為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。

    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檔案中定義scoped代理

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

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

    <bean id="cart"
                class="com.myapp.ShoppingCart"
                scope="session"
          <aop: scoped-proxy />
    </bean>

    在XML配置方式扮演的角色與proxyMode屬性在註解配置方式中的相同,需要注意的是,這裡預設使用CGLIB庫建立代理,因此,如果需要建立介面代理,則需要設定proxy-target-class屬性為false:

    <bean id="cart"
                class="com.myapp.ShoppingCart"
                scope="session"
          <aop: scoped-proxy proxy-target-class="false" />
    </bean>

    為了使用元素,需要在XML配置檔案中定義Spring的aop名字空間:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="htttp://www.springframework.org/schema/beans"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xmlns:aop="http://www.springframework.org/schema/aop"
                  xsi:schemaLocations="
                       http://www.springframework.org/schema/aop
                       http://www.springframework.org/schema/aop/spring-aop.xsd
                       http://www.springframework.org/schema/beans
                       http://www.springframework.org/schema/beans/spring-beans.xsd">
        ........

    執行時值注入

    一般而言,討論依賴注入和裝配時,我們多關注的是如何(how)實現依賴注入(建構函式、setter方法),即如何建立物件之間的聯絡。

    依賴注入的另一個方面是何時(when)將值裝配給bean的屬性或者建構函式。在裝配bean—依賴注入的本質一文中,我們執行了很多值裝配的任務,例如有如下程式碼:

    @Bean
    public CompactDisc sgtPeppers() {
        return new BlankDisc(
                 "Sgt. Pepper's Lonely Hearts Club Band",
                 "The Beatles");
    }

    這種硬編碼的方式有時可以,有時卻需要避免硬編碼——在執行時決定需要注入的值。Spring提供以下兩種方式實現執行時注入:

  • Property placeholders
  • he Spring Expression Language(SpEL)
  • 注入外部的值

    在Spring中解析外部值的最好方法是定義一個配置檔案,然後通過Spring的環境例項獲取配置檔案中的配置項的值。例如,下列程式碼展示如何在Spring 配置檔案中使用外部配置項的值。

    package com.spring.sample.config;
    
    import com.spring.sample.soundsystem.CompactDisc;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.core.env.Environment;
    
    @Configuration
    @PropertySource("classpath:/app.properties")
    public class ExpressiveConfig {
            @Autowired
            Environment env; 
    
           @Bean
            public CompactDisc disc() {
                  return new BlankDisc(env.getProperty("disc.title"),
                    env.getProperty("disc.artist"));
        }
    }

    這裡,@PropertySource註解引用的配置檔案內容如下:

    disc.title=Sgt. Pepper's Lonely Hearts Club Band
    disc.artist=The Beatles

    屬性檔案被載入到Spring的Environment例項中,然後通過getProperty()方法解析對應配置項的值。

    在Environment類中,getProperty()方法有如下幾種過載形式

  • String getProperty(String var1);
  • String getProperty(String var1, String var2);
  • T getProperty(String var1, Class var2);
  • T getProperty(String var1, Class var2, T var3);
  • 前兩個方法都是返回String值,利用第二個引數,可以設定預設值;後兩個方法可以指定返回值的型別,舉個例子:假設你需要從連線池中獲取連線個數,如果你使用前兩個方法,則返回的值是String,你需要手動完成型別轉換;但是使用後兩個方法,可以由Spring自動完成這個轉換:

    int connection = env.getProperty("db.connection.count", Integer.class, 30)

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

    @Bean
    public CompactDisc disc() {
        return new BlankDisc(
                env.getRequiredProperty("disc.title"),
                env.getRequiredProperty("disc.artist"));
    }

    在上述程式碼中,如果disc.title或者disc.artist配置項不存在,Spring都會丟擲IllegalStateException異常。

    如果你希望檢查某個配置項是否存在,則可以呼叫containsProperty()方法:boolean titleExists = env.containsProperty("disc.title");。如果你需要將一個屬性解析成某個類,則可以使用getPropertyAsClass()方法:Class cdClass = env.getPropertyAsClass("disc.class", CompactDisc.class);

    在Spring中,可以使用${ … }將佔位符包裹起來,例如,在XML檔案中可以定義如下程式碼從配置檔案中解析對應配置項的值:

    <bean id="sgtPeppers"
                 class="soundsystem.BlankDisc"
                 c:_title="${disc.title}"
                 c:_artist="${disc.artist}" />

    如果你使用component-scanning和自動裝配建立和初始化應用元件,則可以使用@Value註解獲取配置檔案中配置項的值,例如BlankDisc的建構函式可以定義如下:

    public BlankDisc(
                @Value("${disc.title}") String title,
                @Value("${disc.artist}") String artist) {
          this.title = title;
          this.artist = artist;
    }

    為了使用佔位符的值,需要配置PropertyPlaceholderConfigerbean或者PropertySourcesPlaceholderConfigurerbean。從Spring 3.1之後,更推薦使用PropertySourcesPlaceholderConfigurer,因為這個bean和Spring 的Environment的來源一樣,例子程式碼如下:

    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    如果使用XML配置檔案,則通過元素可以獲得PropertySourcesPlaceholderConfigurerbean:

    <?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:context="http://www.springframework.org/schema/context"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
           <context:property-placeholder location="classpath:/app.properties" />
    </beans>
    

    使用SpEL裝配

    Spring 3引入了Spring Expression Language(SpEL),這是一種在執行時給bean的屬性或者建構函式引數注入值的方法。

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

  • 可以通過bean的ID引用bean;
  • 可以呼叫某個物件的方法或者訪問它的屬性;
  • 支援數學、關係和邏輯操作;
  • 正則表示式匹配;
  • 支援集合操作
    在後續的文章中,可以看到SpEL被用到依賴注入的其他方面,例如在Spring Security中,可以使用SpEL表示式定義安全限制;如果在Spring MVC中使用Thymeleaf模板,在模板中可以使用SpEL表示式獲取模型資料。
    SpEL是一門非常靈活的表示式語言,在這裡不準備花大量篇幅來涵蓋它的所有方面,可以通過一些例子來感受一下它的強大能力。
  • 首先,SpEL表示式被#{ … }包圍,跟placeholders中的${ … }非常像,最簡單的SpEL表示式可以寫作#{1}。在應用中,你可能回使用更加有實際含義的SpEL表示式,例如#{T(System).currentTimeMillis()}——這個表示式負責獲得當前的系統時間,而T()操作符負責將java.lang.System解析成類,以便可以呼叫currentTimeMillis()方法。

    SpEL表示式可以引用指定ID的bean或者某個bean的屬性,例如下面這個例子可以獲得ID為sgtPeppers的bean的artist屬性的值:#{sgtPeppers.artist};也可以通過#{systemProperties['disc.title']}引用系統屬性。

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

    public BlankDisc(
                @Value("#{systemProperties['disc.title']}") String title,
                @Value("#{systemProperties['disc.artist']}") String artist) {
          this.title = title;
          this.artist = artist;
    }

    SpEL表示式可以表示整數值,也可以表示浮點數、String值和Boolean值。例如可以使用#{3.14159}表式浮點數3.14159,並且還支援科學計數法——#{9.87E4}表示98700;#{'Hello'}可以表示字串值、#{false}可以表示Boolean值。

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

    SpEL表示式可以通過bean的ID引用bean,例如#{sgtPeppers};也可以引用指定bean的屬性,例如#{sgtPeppers.artist};還可以呼叫某個bean的方法,例如#{artistSelector.selectArtist()}表示式可以呼叫artistSelector這個bean的selectArtist()方法。

    SpEL表示式也支援方法的連續呼叫,例如#{ar