Spring系列(三) Bean裝配的高級技術


profile

不同於maven的profile, spring的profile不需要重新打包, 同一個版本的包文件可以部署在不同環境的服務器上, 只需要激活對應的profile就可以切換到對應的環境.

  • @Profile({"test","dev"}) Java Config 通過這個注解指定bean屬於哪個或哪些profile. 參數value是一個profile的字符串數組. 此注解可以添加到類或方法上. XML Config 對應的節點是beans的屬性profile="dev", 可以在根<beans>節點下嵌套定義分屬不同profile的節點, 形成如下結構
<beans ...>
    <beans profile="dev">
        <bean ...>
        <bean ...>
    </beans>
    <beans profile="prod">
        <bean ...>
    </beans>
    ...
</beans>
  • 通過spring.profiles.activespring.profiles.default可以設置激活哪個profile,如果是多個就用","分開, spring優先使用spring.profiles.active的設置, 如果找不到, 就使用spring.profiles.default設置的值, 如果二者都沒設置, spring會認為沒有要激活的profile, 它只會創建不加@Profile的那些bean.

可以通過多種方式設置這兩個屬性:

  1. DispacherServlet的初始化參數
  2. Web應用的上下文參數
  3. JNDI
  4. 環境變量
  5. JVM系統屬性
  6. 測試類上使用@ActiveProfiles注解

下面是web.xml中在上下文以及在servlet中設置spring.profiles.default的代碼

<web-app>
    <!--上下文設置default profile-->
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>

    <servlet>
        ...
        <!--設置default profile-->
        <init-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value>
        </init-param>
    </servlet>
</web-app>

條件化的 bean

Spring4.0 引入了條件化bean, 這些bean只有在滿足一定條件下才會創建. profile就是條件化的一種使用方式, 事實上4.0版本后的profile實現機制與其他條件化bean完全一樣, 我們可以通過profile的源碼一窺條件化bean的機制.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class) //注意這里, 條件化bean的注解
public @interface Profile {
    String[] value();
}
  • @Conditional(ConditionClass.class) Java Config 可以將這個注解添加到@Bean注解的方法上, 參數value是一個Class類型的變量, 這個類必須實現Condition接口, 方法matchs()返回true則加載bean, 否則忽略.

Condition 接口定義如下:

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); 
}

ProfileCondition的定義

class ProfileCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 獲取profile注解的屬性
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            // 獲取value屬性的值
            for (Object value : attrs.get("value")) {
                // 測試profile是否激活
                if (context.getEnvironment().acceptsProfiles((String[]) value)) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
}
  • 通過matchs()方法的兩個參數我們可以做到 (1)通過ConditionContext獲取到上下文所需信息;(2)通過AnnotatedTypeMetadata獲取到bean的注解
public interface ConditionContext {
    // 獲取bean的注冊信息, 從而可以檢查bean定義
    BeanDefinitionRegistry getRegistry();
    // 獲取bean工廠,從而可以判斷bean是否存在,獲取其他bean,獲取bean的狀態信息
    @Nullable
    ConfigurableListableBeanFactory getBeanFactory();
    // 獲取環境變量,profile中正是使用此對象的方法acceptsProfiles()檢查profile是否激活
    Environment getEnvironment();
    // 獲取加載的資源
    ResourceLoader getResourceLoader();
    // 獲取classLoader
    @Nullable
    ClassLoader getClassLoader();
}

public interface AnnotatedTypeMetadata {
    // 檢查是否由某注解
    boolean isAnnotated(String annotationName);
    // 下面幾個方法可以獲取bean的注解,包括其屬性
    @Nullable
    Map<String, Object> getAnnotationAttributes(String annotationName);
    @Nullable
    Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
    @Nullable
    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
    @Nullable
    MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);

}

自動裝配的歧義

自動裝配大大簡化了spring的配置, 對於大多數應用對象的依賴bean, 程序實現的時候一般只有一個匹配, 但也存在匹配到多個bean的情況, Spring處理不了這種情況, 這時候就需要由開發人員為其消除裝配的歧義.

  • @Primary 注解用來指定被注解的bean為首選bean. 但如果多個匹配的bean都添加了該注解依然無法消除歧義.
  • @Qualifier 注解用來限定bean的范圍. 與@AutoWired@Inject共同使用時,參數value用來指定beanid, 但使用默認的beanid對重構不友好. 與@Component@Bean一起使用時,參數value為bean指定別名, 別名一般是帶有描述bean特征的詞描述, 然后在注入時就可以使用別名. 但別名可能需要定義多個, Qualifier不支持數組, 也不允許定義多個(原因見下面).
  • @Qualifier 不允許對同一bean重復標記, 這是因為Qualifier注解定義沒有添加@Repeatable注解.

可以使用自定義限定注解的方式達到縮小bean范圍的目的.

下面代碼使用Qualifier的方式定義bean

@Component
@Qualifier("cheap")
public class QQCar implements Car{

}
@Component
@Qualifier("fast")
public class BenzCar implements Car{
    
}
@Component
@Qualifier("safe")
//@Qualifier("comfortable") //行不通,不能加兩個Qualifier
public class BMWCar implements Car{
    
}

再使用自定義限定注解的方式

// 定義
...
@Qualifier
public @interface Cheap{}

...
@Qualifier
public @interface Fast{}

...
@Qualifier
public @interface Safe{}

...
@Qualifier
public @interface Comfortable{}

// 使用
@Component
@Safe
@Comfortable
public class BMWCar implements Car{
    
}
...

使用自定義限定的方式可以隨意組合來限定bean的范圍, 因為沒有任何使用string類型, 所以也是類型安全的.

bean 作用域

bean的四種作用域:

  1. 單例(Singleton) 默認為此作用域, 全局唯一
  2. 原型(Prototype) 注入或從上下文獲取時均會創建
  3. 會話(Session) Web程序使用, 同一會話保持唯一
  4. 請求(Request) Web程序使用,每次請求唯一
  • @Scope Java Config 用此注解指定bean的作用域, 參數value用來指定作用域, 如value=ConfigurableBeanFactory.SCOPE_PROTOTYPE, 原型和單例的常數定義在ConfigurableBeanFactory中, 會話和請求的常數定義在WebApplicationContext中 ; 另一個參數proxyMode=ScopedProxyMode.INTERFACES用來指定創建代理的方式, 示例中指定了使用基於接口的代理方法; 如果使用proxyMode=ScopedProxyMode.TARGET_CLASS則會使用CGLib來生成基於類的代理.
    XML Config對應的時bean的scope="prototype"屬性, 針對web作用域,還需要指定使用代理,示例代碼如下.
<bean id="beanid" class="..." scope="session">
    <!--需要引用aop命名空間的.  proxy-target-class 默認為true, 使用CGLib創建代理, 指定為false則使用接口方式創建代理-->
    <aop:scoped-proxy proxy-target-class="false">
</bean>
  • 作用域為什么使用代理模式? 如果bean不是單例的, spring會為其創建一個代理對象, 再將這個代理對象注入進去, 實際運行過程中由代理對象委托調用實際的bean. 這樣有兩點好處:(1)懶加載, bean可以在需要時才創建;(2)便於作用域擴展

屬性占位符和SpEL

spring提供了兩種運行時注入值的方式, 這樣就避免了將字面量硬編碼到程序或配置文件里面.

  1. 屬性占位符
  2. SpEL

一. 屬性占位符

先看下面的例子:

@Configuration
@PropertySource("classpath:/path/app.property")
public class MyConfig{

    @AutoWired
    Environment env;

    @Bean
    public Car getCar(){
        return new QQCar(env.getProperty("qqcar.price"));
    }
}
  • @PropertySource("classpath:/path/app.property") 可以指定加載資源文件的位置, 結合使用Environment類的實例env,可以獲取到資源文件中配置的節點.Environment不僅可以獲取到配置的字面量, 它也可以獲取復雜類型, 將其轉化為對應Class的實例.<T> T getProperty(String key, Class<T> targetType);
  • XML Congif 可以使用占位符, 形如${qqcar.price}, 前提是配置了context命名空間下的<context:property-placeholder /> 它會生成類型為PropertySourcesPlaceholderConfigurer的bean, 該bean用來解析占位符
  • 如果開啟組件掃描和自動裝配的話, 就沒有必要指定資源文件或類了, 可以使用@Value(qqcar.price). 同樣, 需要一個類型為PropertySourcesPlaceholderConfigurer的bean.
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){
    return new PropertySourcesPlaceholderConfigurer();
}

二. 功能更強大的SpEL

SpEL的特性如下:

  1. 使用bean的Id來引用bean
  2. 調用方法和屬性: 可以通過beanid來調用bean的方法和屬性, 就像在代碼中一樣.
  3. 對值進行算術,邏輯,關系運算: 特別注意: ^乘方運算符; ?:為空運算符
  4. 正則表達式匹配: 形如#{beanid.mobile matches '1(?:3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8\d|9\d)\d{8}'}
  5. 集合操作: .?[]查找運算符; .^[]查找匹配的第一項;.$[]查找匹配的最后一項;.![]投影運算符(比如獲取集合中符合條件的對象, 並取其中某個屬性映射到結果集合中);

SpEL的示例 #{1},#{T(System).out.print("test")},#{beanId.name},#{systemProperies["path"]}

雖然SpEL功能強大, 我們可以通過SpEL編寫復雜的表達式, 但過分復雜的表達式不適合理解和閱讀, 同時也會增大測試的難度. 因此建議盡量編寫簡潔的表達式.


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com