第3章 高級裝配
3.1 環境與profile
3.1.1 配置profile bean
在Java中配置
分開配置
@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();
}
}
and
@Configuration
@Profile("prod")
public class ProductionProfileConfig {
@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();
}
}
一同配置
@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 jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource)jndiObjectFactoryBean.getObject();
}
}
在XML中配置profile
方案1:beans 中添加屬性
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd"
profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
方案2:bean中添加屬性
<?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:jee="http://www.springframework.org/schema/jee"
xmlns:p="http://www.springframework.org/schema/p"
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-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.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.dbcp2.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" />
</beans>
<beans profile="prod">
<jee:jndi-lookup id="dataSource"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
</beans>
3.1.2 激活profile
依賴兩個獨立屬性:
- spring.profiles.default(推薦開發環境使用此參數,設置DispatcherServlet的web.xml文件)
- spring.profiles.active(QA、生產或者其他環境,根據具體情況使用系統屬性、環境變量、或JNDI設置)
激活方式:
- 作爲DispatcherServlet的初始化參數
- 作爲Web應用的上下文參數
- 作爲JNDI條目
- 作爲環境變量
- 作爲JVM的系統屬性
- 再集成測試類上,使用@ActiveProfiles註解設置
使用profile進行測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= {PersistenceTestConfig.class,
ProductionProfileConfig.class,
DevelopmentProfileConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
@Autowired(required=false)
private DataSource dataSource;
@Test
public void testDataSource() {
assertNotNull(dataSource);
}
}
3.2 條件化的Bean
public class MagicExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("magic");
}
}
@Configuration
public class BeanConfig {
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
}
ConditionContext接口
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
AnnotatedTypeMetadata接口
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationType);
Map<String, Object> getAnnotationAttributes(String annotationType);
Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString);
}
Spring4 Profile 源碼
@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();
}
ProfileCondition源碼
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;
}
}
3.3 處理自動裝配的歧義性
下面的代碼自動裝配就會產生歧義
@Component
public class Cake implements Dessert {
}
@Component
public class Cookies implements Dessert {
}
@Component
public class IceCream implements Dessert {
}
@Configuration
@ComponentScan
public class DessertConfig {
Dessert dessert;
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
}
3.3.1 標示首選的bean
@Primary
@Component
@Primary
public class IceCream implements Dessert {
}
但是多個bean被標示@Primary依然會出現歧義性
3.3.2 限定自動裝配的bean
@Qualifier 默認beanID作爲限定參數
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
創建自定義限定符
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
}
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
與@bean註解一同使用
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
使用自定義限定符註解
下面的代碼有問題,都出現了重複註解
@Component
@Primary
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert {
}
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert {
}
解決方案自定義註解,繼承@Qualifier註解
Cold註解
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
}
Creamy註解
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {
}
Fruity註解
@Retention(RUNTIME)
@Target({ TYPE, FIELD, METHOD, CONSTRUCTOR })
@Qualifier
public @interface Fruity {
}
自定義限定符的多重限定的實現方式
@Component
@Cold
@Creamy
public class IceCream implements Dessert {
}
@Component
@Cold
@Fruity
public class Popsicle implements Dessert {
}
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
3.4 bean 的作用域
Spring定義的多種作用域
- 單例(Singleton)
- 原型(Prototype)
- WEB會話(Session)
- WEB請求(Request)
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {
}
bean中
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notePad() {
return new Notepad();
}
XML中
<bean id="notepad" class="com.C3_4.Notepad" scope="prototype" />
3.4.1 使用會話Session和請求Request作用域
@Component
@Scope(value=ConfigurableBeanFactory.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public interface ShoppingCart {
}
注意代理:StoreService是單例bean,ShoppingCart是會話(Session)bean,StoreService在Spring的應用上下文加載的時候創建,但這個時候ShoppingCart不存在,因爲他屬於會話(Session)作用域。Spring並不會將實際的ShoppingCart bean注入,而是注入代理bean,這個代理與ShoppingCart有相同的方法。當StoreService真正調用ShoppingCart的方法時,代理會對其進行懶解析並將調用委託給真正的ShoppingCart的bean。
@Component
public class StoreService {
ShoppingCart shoppingCart;
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
proxyMode屬性:
- 接口-ScopedProxyMode.INTERFACE
- 類(基於CGLIB)-ScopedProxyMode.TARGET_CLASS
public enum ScopedProxyMode {
DEFAULT,
NO,
INTERFACES,
TARGET_CLASS;
}
3.4.2 在XML中聲明作用域代理
默認是基於CGLIB創建目標類的代理
<bean id="cart" class="com.C3_4.ShoppingCart" scope="session">
<aop:scoped-proxy/>
</bean>
基於接口代理
<bean id="cart" class="com.C3_4.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target-class="false" />
</bean>
3.5 運行時值注入
3.5.1 注入外部的值
硬編碼方式:
@Bean
public CompactDisc sgtPeppers() {
return new BlankDisc("sgt. pepper's Lonely Hearts Club Band", "The Beatles");
}
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
p:title="sgt. pepper's Lonely Hearts Club Band"
p:artist="The Beatles" />
避免硬編碼,Spring提供兩種運行時求值方式:
- 屬性佔位符(Property placeholder)
- Spring表達式語言(SpEL)
3.5.1 注入外部的值
app.properties
disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class ExpressiveConfig {
@Autowired
Environment env;
@Bean
public BlankDisc disc() {
return new BlankDisc(
env.getProperty("disc.title"),
env.getProperty("disc.artist")
);
}
}
深入學習Spring的Environment
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();
String[] getDefaultProfiles();
boolean acceptsProfiles(String... profiles);
}
public interface PropertyResolver {
boolean containsProperty(String key);
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);
<T> Class<T> getPropertyAsClass(String key, Class<T> targetType);
String getRequiredProperty(String key) throws IllegalStateException;
<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
String resolvePlaceholders(String text);
String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}
解析屬性佔位符
${}
!!!@Value不能用在構造方法,測試不好使
public void set(
@Value("${disc.title}") String title,
@Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
使用佔位符需要配置PropertySourcesPlaceholderConfigurer() bean,通過基於Srping的Environment及其屬性源來解析佔位符
// 這個推薦使用
@Bean
public static PropertySourcesPlaceholderConfigurer palceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
// 這個不推薦使用
@Bean
public static PropertyPlaceholderConfigurer palceholderConfigurer() {
return new PropertyPlaceholderConfigurer();
}
XML配置
<context:property-placeholder />
3.5.2 使用Spring表達式語言進行裝配
SpEL(Spring Expression Language)特性:
- 使用Bean的Id來引用bean
- 調用方法和訪問對象的屬性
- 對值進行算術、關係和邏輯運算
- 正則表達式匹配
- 集合操作
SpEL樣例:
#{1}
#{T(System).currentTimeMillis()}
#{sgtPeppers.artist}
${systemProperties['disc.title']}
public BlankDisc(
@Value("#{systemProperties['disc.title']}") String title,
@Value("#{systemProperties['disc.artist']}") String artist) {
this.title = title;
this.artist = artist;
}
XML
<bean id="compactDisc" class="soundsystem.BlankDisc"
c:_title="#{systemProperties['disc.title']}"
c:_artist="#{systemProperties['disc.artist']}" />
表示字面值
#{3.14159}
#{9.87E4}
#{'hello'}
#{false}
引用bean、屬性和方法
#{sgtPeppers}
#{sgtPeppers.artist}
#{artistSelector.selectArtist()}
#{artistSelector.selectArtist().toUpperCase()}
"?."運算符
#{artistSelector.selectArtist()?.toUpperCase()}
在表達式中使用類型
Class對象 T(java.lang.Math) 常量 T(java.lang.Math).PI 靜態方法 T(java.lang.Math).random()
SpEL運算符
#{2 * T(java.lang.Math).PI * circle.radius}
#{2 * T(java.lang.Math).PI * circle.radius ^ 2}
#{disc.title + ' by ' + disc.artist}
#{counter.total == 100}
#{counter.total eq 100}
#{scoreboard.score > 1000 ? "winner!" : "Loser"}
// Elvis運算符
#{disc.title ?: 'Rattle and Hum'}
正則表達式
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
計算集合
"[]"獲取元素
#{jukebox.songs[4].title}
#{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size()].title}
#{'This is a test'[3]}
".?[]"集合過濾 ".^ []"集合第一個 ".$[]"集合最後一個
#{jukebox.songs.?[artist eq 'Aerosmith']}
".![]"投影運算符
//返回title列表
${jukebox.songs.![title]}
//返回title列表
${jukebox.songs.?[artist eq 'Aerosmith'].![title]}