Spring裝配Bean(五)profile註解和解決自動注入的歧義性

z1340954953發表於2018-05-31

配置profile bean

Spring為環境相關的bean所提供的解決方案其實和構建時候的方案沒有太大區別,Spring會根據環境決定該建立那個bean和

不建立那個bean。

Spring的bean profile的功能。要使用profile,首先將所有不同的bean定義到一個或者多個profile之中,在將應用部署到每個環境中,要確保對應的profile處於啟用(active)的狀態

* Java配置中,使用@Profile註解指定某個bean屬於那個profile

首先建立一個bean,用在開發環境和生產環境

@Configuration
@Profile("dev")
public class DevConfiguration {
	@Bean
	public HelloWorld helloWorld(){
		return new HelloWorld("dev environment .....");
	}
}
@Configuration
@Profile("prod")
public class ProductConfiguration {
	public HelloWorld helloWorld(){
		return new HelloWorld("product environment .... ");
	}
}

從Spring3.2開始,profile註解可以用在方法上,上面改為

@Configuration
public class HellloConfiguration {
	@Bean
	@Profile("dev")
	public HelloWorld devhelloWorld(){
		return new HelloWorld("dev environment .....");
	}
	@Bean
	@Profile("prod")
	public HelloWorld prodhelloWorld(){
		return new HelloWorld("product environment .....");
	}
}

只有規定的profile被啟用,對應的bean才會被建立,沒有指定profile的Bean始終都會建立

* XML中配置profile

* Beans標籤使用profile屬性表示當前的bean的profile

<?xml version="1.0" encoding="UTF-8"?>  
<beans profile="dev" 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-3.1.xsd
            http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-3.1.xsd 
           ">  
  <bean id="helloWorld" class="com.erong.service.HelloWorld">
  	<property name="message" value="dev bean ....."></property>
  </bean>
</beans>  

需要注意的是xsi:schemaLocation中定義的beans的xsd檔案必須是3.1之後的,只有profile被啟用並且屬性值相同的bean才能建立

* 重複使用元素來指定多個profile

-- profile_dev.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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans  
           http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
            http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-3.1.xsd 
           ">
    <context:annotation-config></context:annotation-config>
	<beans profile="dev">
		<bean id="helloWorld" class="com.erong.service.HelloWorld">
			<constructor-arg value="dev hello world bean ...."></constructor-arg>
		</bean>
	</beans>
	<beans profile="business">
		<bean id="helloWorld" class="com.erong.service.HelloWorld">
			<property name="message" value="business bean ....."></property>
		</bean>
	</beans>
	<beans profile="prod">
		<bean id="helloWorld" class="com.erong.service.HelloWorld">
			<property name="message" value="prod bean ....."></property>
		</bean>
	</beans>
</beans>  

所有的bean定義到一個xml檔案中,定義了三個bean,但是執行時候只會建立一個bean,取決於處於啟用狀態的是profile

* 啟用profile

Spring在確定那個profile處於啟用狀態,需要依賴兩個獨立的屬性:spring.profiles.active和spring.profiles.default

如果設定了spring.profiles.active屬性的話,它的值就會用來確定那個profile是啟用的。

如果沒有設定,將使用spring.profiles.default的值,兩個屬性都沒有設定將會不啟用profile

設定方式:

1. 作為DispatcherServlet的初始化引數

2. 作為Web應用的上下文引數

3. 作為JNDI條目

4. 作為環境變數

5. 作為JVM系統屬性

<!-- 為上下文設定預設的profile -->
	<context-param>
		<param-name>spring.profiles.default</param-name>
		<param-value>dev</param-value>
	</context-param>
	<!-- 為servlet設定預設profile -->
	<servlet>
		<servlet-name>myServlet</servlet-name>
		<servlet-class>com.bing.servlet.MyServlet</servlet-class>
		<init-param>
			<param-name>spring.profiles.default</param-name>
			<param-value>dev</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>myServlet</servlet-name>
		<url-pattern>/app</url-pattern>
	</servlet-mapping>

注意到,profile使用的都是複數形式,可以同時啟用多個profile,使用逗號隔開

* 使用profile進行測試

當執行整合測試時,如果配置中的bean定義在profile中,那麼在執行測試中,需要有一種方式來啟用合適的profile

Spring提供了@ActiveProfiles註解,我們可以使用它指定執行測試時要啟用那個profile。

測試類:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value="classpath:profile_dev.xml")
@ActiveProfiles(value={"dev"})
public class CDPlayerTest {
	@Autowired
	private HelloWorld helloWorld;
	@Test
	public void test(){
		System.out.println(
		helloWorld.getMessage());
	}
}

最後應該輸出:

Your Message: dev hello world bean ....

測試過程中發現的問題:

1. 建立bean的時候,使用property元素對Bean進行賦值操作,呼叫的無參的構造器,並且呼叫的是對應的setter方法進行設值

2. 如果使用constructor-arg元素,必須存在相應引數的構造器方法建立Bean

* 條件化的bean

假設希望一個或多個bean只有在應用的類路徑下包含特定的庫時才建立。或者希望某個bean在另外某個特定的bean宣告之後才會建立。Spring4之後,引入一個全新的註解@Conditional註解,它可以用到帶有@Bean註解的方法上。

如果給定的條件計算結果為true,就建立這個bean

1. JavaConfig配置類

@Configuration
public class CDConfig {
	@Bean
	@Conditional(MagicExistCondition.class)
	public CompactDisc sgtPeppers(){
		return new SgtPeppers();
	} 
}

@Conditional將會根據類MagicExistCondition的matches方法返回的結果判斷是否建立Bean

-- MagicExistCondition 實現介面 org.springframework.context.annotation.Condition

public class MagicExistCondition implements Condition {
	/**
	 * 檢查環境變數是否存在magic屬性
	 */
	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata arg1) {
		Environment env = context.getEnvironment();
		return env.containsProperty("magic");
	}

}

對於matches方法中的ConditionContext介面,提供的方法我們能夠檢查

public interface ConditionContext {

	/**
	 * 根據返回的BeanDefinitionRegistry,檢查bean的定義
	 */
	BeanDefinitionRegistry getRegistry();

	/**
	 * 藉助返回的ConfigurableListableBeanFactory,檢查bean是否存在
	 */
	ConfigurableListableBeanFactory getBeanFactory();

	/**
	 * 返回的Environment,來確定環境變數是否存在及值是什麼
	 */
	Environment getEnvironment();

	/**
	 * 獲取載入的資源
	 */
	ResourceLoader getResourceLoader();

	/**
	 * 檢查類是否存在
	 */
	ClassLoader getClassLoader();

}

AnnotatedTypeMetadata這個介面,能夠檢查@Bean註解方法上還存在其他註解

Note:@Profile註解底層使用到了@Conditional註解,並且引用ProCondition作為Condition介面的實現

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

	/**
	 * The set of profiles for which the annotated component should be registered.
	 */
	String[] value();

}
class ProfileCondition implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		if (context.getEnvironment() != null) {
			MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
			if (attrs != null) {
				for (Object value : attrs.get("value")) {
					if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
						return true;
					}
				}
				return false;
			}
		}
		return true;
	}

}

從原始碼看

1. 獲取Profile 註解的所有屬性的鍵值對

2. 檢查當前環境中生效的Profile和當前Bean的Profile的value的值是否相同,相同,返回true,建立這個bean

@Profile在使用時候也會通過Conditon介面的實現類,判斷Bean的profile是否處在啟用狀態,從而判斷是否建立Bean。


* 處理自動裝配的歧義性

如果存在多個類實現同一個介面,每個類都建立一個bean,自動注入的時候會選擇哪個Bean,Spring無法判斷就會丟擲異常


解決:

1. 使用@Primary註解或者xml中primary = true ,標識Bean是優先選擇的注入的

@Bean
	@Primary
	@Conditional(MagicExistCondition.class)
	public CompactDisc sgtPeppers(){
		return new SgtPeppers();
	} 
<bean id="helloWorld" class="com.erong.service.HelloWorld" primary="true">
			<constructor-arg value="dev hello world bean ...."></constructor-arg>
		</bean>

2. 使用@Qualifier註解限定裝配的Bean和@Autowire註解或者@Inject註解搭配使用,值為Bean的ID

@Autowired
@Qualifier("cdplayer")
private CDPlayer cdplayer;

* 建立自定義的限定符

可以為bean設定自己的限定符,而不是依賴於將bean ID 作為限定符

> bean的宣告上新增@Qualifier註解,設定Bean自己的限定符號

@Component
@Qualifier("compactDisc")
public class SgtPeppers implements CompactDisc {
@Bean
@Qualifier("compactDisc")
public SgtPeppers getInstance(){
       return new SgtPeppers();
}

這樣,建立的Bean的限定符號就為compactDisc,自動注入的時候@Qualifier使用這個限定符注入

note: 

1. @Bean註解放在方法上,如果方法存在形參,會自動掃描匹配的bean自動注入

2. @Bean註解建立的Bean的限定符,可以通過@Qualifier指定

3. @Bean建立的Bean,Bean ID可以同時是指定的限定符,也可以是方法名

* Bean的自定義限定符號相同,需要自定自己的限定符註解

@Target({java.lang.annotation.ElementType.FIELD, 
	java.lang.annotation.ElementType.METHOD, 
	java.lang.annotation.ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {

}


相關文章