Spring – 高階裝配

麥冬發表於2017-12-24

高階裝配

  • Spring profile
  • 條件化的bean
  • 自動裝配與歧義性
  • bean的作用域
  • Spring表示式語言

環境與profile

  • profile可以為不同的環境(dev、prod)提供不同的資料庫配置、加密演算法等
  • @Profile註解可以在類級別和方法級別,沒有指定profile的bean始終都會被建立
  • XML的方式配置 datasource.xml
import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jndi.JndiObjectFactoryBean;

/**
 * 不同環境所需要的資料庫配置
 */
public class DataSourceDemo {

    /**
     * EmbeddedDatabaseBuilder會搭建一個嵌入式的Hypersonic資料庫
     * 模式(schema)定義在schema.sql
     * 測試資料通過test-data.sql載入
     */
    @Bean(destroyMethod="shutdown")
    public DataSource dataSource_1(){
        return new EmbeddedDatabaseBuilder()
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }
    
    /**
     * 使用JNDI從容器中獲取一個DataSource
     */
    @Bean
    public DataSource dataSource_2(){
        JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
        jndiObjectFactoryBean.setJndiName("jdbc/myDs");
        jndiObjectFactoryBean.setResourceRef(true);
        jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource) jndiObjectFactoryBean.getObject();
    }
    
    /**
     * 配置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("root");
        dataSource.setPassword("root");
        dataSource.setInitialSize(20);
        dataSource.setMaxActive(30);
        return dataSource;
    }
    
}

啟用profile

  • 依賴屬性:spring.profiles.active和spring.profiles.default

    • 作為DispatcherServlet的初始化引數
    • 作為Web應用的上下文引數
    • 作為JNDI條目
    • 作為環境變數
    • 作為JVM的系統屬性
    • 在整合測試類上,使用@ActiveProfile註解設定
  • 在web.xml中配置 見web.xml03
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 {

    /**
     * 為dev profile裝配的bean
     * @return
     */
    @Bean(destroyMethod="shutdown")
    @Profile("dev")
    public DataSource embeddedDataSource(){
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }
    
    /**
     * 為prod profile裝配的bean
     * @return
     */
    @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();
    }
}
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 DevelopmentProfileConfig {

    @Bean(destroyMethod="shutdown")
    public DataSource dataSource(){
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:schema.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }
}
import org.junit.runner.RunWith;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * 使用profile進行測試
 * 當執行整合測試時,Spring提供了@ActiveProfiles註解,使用它來指定執行測試時要啟用哪個profile
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {

}

條件化的bean

  • 假設你希望一個或多個bean只有在應用的類路徑下包含特定庫時才建立
  • 或者希望某個bean只有當另外某個特定的bean也宣告瞭之後才會建立
  • 還可能要求只有某個特定的環境變數設定之後,才會建立某個bean
  • Spring4引入一個新的@Conditional註解,它可以用在帶有@Bean註解的方法上
  • 例如,假設有個名為MagicBean的類,我們希望只有設定了magic環境屬性的時候,Spring才會例項化這個類
  • 通過ConditionContext,可以:

    • 藉助getRegistry()返回的BeanDefinitionRegistry檢查bean定義
    • 藉助getBeanFactory()返回的ConfigurableListableBeanFactory檢查bean是否存在,甚至檢查bean的屬性
    • 藉助getEnvironment()返回的Environment檢查環境變數是否存在以及它的值是什麼
    • 讀取並探查getResourceLoader()返回的ResourceLoader所載入的資源
    • 藉助getClassLoader()返回的ClassLoader載入並檢查類是否存在
  • @Profile這個註解本身也使用了@Conditional註解,並且引用ProfileCondition作為條件

    • ProfileCondition會檢查value屬性,該屬性包含了bean的profile名稱。檢查profile是否啟用
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 MagicExistsConditional implements Condition {

    /**
     * 方法簡單功能強大
     */
    @Override
    public boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata) {
        Environment env = ctxt.getEnvironment();
        //檢查magic屬性
        return env.containsProperty("magic");
    }

}

處理自動裝配的歧義

  • 僅有一個bean匹配所需的結果時,自動裝配才是有效的,如果不僅有一個bean能夠匹配結果的話,這種歧義性會阻礙Spring自動裝配屬性、構造器引數或方法引數
  • 標示首選(primary)的bean

    • @Component註解上加@Primary註解
    • 在Java配置顯示宣告 中@Bean註解上加@Primary註解
    • XML配置bean primary=”true”
  • 限定自動裝配的bean

    • 不止一個限定符時,歧義性問題依然存在
    • 限定bean:在註解@Autowired上加@Qualifier(“beanName”)
    • 限定符和注入的bean是耦合的,類名稱的任意改動都會導致限定失敗

* 建立自定義的限定符

* 在@Component註解上加@Qualifier("selfDefinedName")註解
* 可以在@Autowired上加@Qualifier("selfDefinedName")
* 也可以在Java配置顯示定義bean時,在@Bean註解上加@Qualifier("selfDefinedName")
  • 使用自定義的限定符註解

    • 如果有多個相同的限定符時,又會導致歧義性
    • 在加一個@Qualifier註解:Java不允許在同一條目上出現相同的多個註解
      (JDK8支援,註解本身在定義時帶有@Repeatable註解就可以,不過Spring的@Qualifier註解上沒有)
    • 定義不同的註解,在註解上加@Qualifier,例如在bean上加@Cold和@Creamy(自定義註解)

bean的作用域

  • 預設情況下,Spring應用上下文所有bean都是作為以單例(singleton)的形式建立的
  • Spring定義了多種作用域

    • 單例(Singleton):在整個應用中,只建立bean的一個例項
    • 原型(Prototype):每次注入或者通過Spring應用上下文獲取的時候,都會建立一個新的bean例項
    • 會話(Session):在Web應用中,為每個會話建立一個bean例項
    • 請求(Request):在Web應用中,每個請求建立一個bean例項
  • 使用會話和請求作用域

    • 購物車bean,會話作用域最合適,因為它與給定的使用者關聯性最大 例如:ShoppingCart
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

/**
 * value=WebApplicationContext.SCOPE_SESSION值是session,每個會話會建立一個ShoppingCart
 * proxyMode=ScopedProxyMode.INTERFACES建立類的代理,確保當前購物車就是當前會話所對應的那一個,而不是其他使用者
 * 
 * XML的配置
 * <bean id="cart" class="com.leaf.u_spring.chapter03.ShoppingCart" scope="session">
 *         <aop:scoped-proxy />
 * </bean>
 * 
 * 使用的是CGLib生成代理類
 */
@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,
        proxyMode=ScopedProxyMode.INTERFACES)
public class ShoppingCart {

    
}

執行時值注入

  • Spring提供兩種執行的值注入

    • 屬性佔位符(Property placeholder)
    • Spring表示式語言(SpEL)
  • 深入學習Spring的Environment

    • String getProperty(String key) 獲取屬性值
    • String getProperty(String key, String defaultValue) 獲取屬性值,不存在時返回給定預設值
    • <T> T getProperty(String key, Class<T> targetType); 獲取指定型別的屬性值
    • <T> T getProperty(String key, Class<T> targetType, T defaultValue); 獲取指定型別的屬性值,不存在時返回預設值
    • boolean containsProperty(String key); 檢查指定屬性值是否存在
    • String[] getActiveProfiles(); 返回啟用profile名稱陣列
    • String[] getDefaultProfiles(); 返回預設profile名稱陣列
    • boolean acceptsProfiles(String… profiles); 如果environment支援給定profile的話,就返回true
  • 解析屬性佔位符

    • Spring一直支援將屬性定義到外部的屬性檔案中,並使用佔位符值將其插入到Spring bean中
    • 在Spring裝配中,佔位符的形式為使用”${…}”包裝的屬性名稱
    • 在JavaConfig使用 @Value
    • 在XML中 <context:property-placeholder location=”classpath:/conf/*.properties” />
  • 使用Spring表示式語言進行裝配

    • SpEl擁有特性:

      • 使用bean的ID來引用bean
      • 呼叫方法和訪問物件的屬性
      • 對值進行算術、關係和邏輯運算
      • 正規表示式匹配
      • c集合操作
      • Spring Security支援使用SpEL表示式定義安全限制規則
      • 在Thymeleaf模板檢視中使用SpEL表示式引用模型資料
    • SpEL表示式要放到 “#{…}”之中:
        * #{1}         - 1
        * #{T(System).currentTimeMillis()}    -當前毫秒數
        * #{sgtPeppers.artist}          - ID為sgtPeppers的bean的artist屬性
        * #{systemProperties[`disc.title`]}        -通過systemProperties物件引用系統屬性
* 表示字面值
    * 浮點數:#{3.14159}    科學計數法:#{9.87E4}        字串:#{`hello`}    boolean型別:#{false}
* 引入bean、屬性和方法
        * #{sgtPeppers} -bean        #{sgtPeppers.artist} -屬性
        * #{sgtPeppers.selectArtist()} -方法        
        * #{sgtPeppers.selectArtist()?.toUpperCase()} -?.判斷非空情況呼叫toUpperCase方法
* 在表示式中使用型別
    * 如果要在SpEL中訪問類作用域的方法和常量的話,要依賴T()這個關鍵的運算子。
    * 表達Java的Math類    T(java.lang.Math)
    * 把PI屬性裝配待bean屬性    T(java.lang.Math).PI
    * 呼叫T()運算子所得到的靜態方法    T(java.lang.Math).random()
* SpEL運算子
        * 算術運算、比較運算、邏輯運算、條件運算、正規表示式、三元運算子
        * #{2 * T(java.lang.Math).PI * circle.radius}        -計算圓周長
        * #{T(java.lang.Math).PI * circle.radius ^ 2}        -計算圓面積
        * #{disc.title + `by` + disc.artist}                -String型別的連線操作
        * #{counter.total == 100}或#{counter.total eq 100} -比較運算
        * #{scoreboard.score > 1000 ? `Winner!` : `Losser`}    -三元運算
        * #{disc.title ?: `Rattle and Hum`}                -檢查null,使用預設值代替null
        * #{admin.email matches `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.com`}        -匹配有效郵箱
        * #{jukebox.songs[4].title}    -計算ID為jukebox的bean的songs集合中第五個元素的title屬性
        * #{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size].title}        -獲取隨機歌曲的title
        * #{jukebox.songs.?[artist eq `Aerosmith`]}        -用來對集合過濾查詢
        * ‘.^[]’ 和 ‘.$[]’分別用來在集合中查詢第一個匹配項和最後一個匹配項
        * 投影運算子 (.![]),會從集合的每個成員中選擇特定的屬性放到另外一個集合中

引用:《Spring In Action 4》第3章

相關文章