高階裝配
- 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章