本章内容:
- Spring profile
- 条件化的bean声明
- 自动装配与歧义性
- bean的作用域
- Spring表达式语言
3.1 环境与profile
在软件开发的不同阶段需要不同的环境和配置。
1 2 3 4 5 6 7
| @Bean(destroyMethod = "shutdown") public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .addScript("classpath:ch3.sql") .addScript("classpath:ch3.1.sql") .build(); }
|
为了适应环境更换的需求,可以将所需要的所有的配置类配置到每个bean中,然后在构建阶段选择需要使用的bean,但是从开发环境切换到生产环境时可能会发生问题。
3.1.1 配置profile bean
Spring为此种场景提供了profile功能。
使用profile注解来声明在合适的阶段使用合适的bean。将所有的bean整理到一个profile中,确保在需要的时候active相应的bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.ch3;
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;
import javax.sql.DataSource;
@Configuration public class DataSourceConfig { @Bean(destroyMethod = "shutdown") @Profile("dev") public DataSource embeddedDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:test.sql") .addScript("classpath:test1.sql") .build(); }
@Bean @Profile("prod") public DataSource jndiDataSource() { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jndi/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); } }
|
虽然所有的bean都被声明在一个profile里,但是只有当指定的profile被激活时,相应的bean才会被创建,没有指定profile的bean始终都会被创建,与激活的profile没有关系。
在XML中配置profile:
可以通过beans元素的profile属性,在xml中配置profile。
1 2 3 4 5 6 7 8 9 10 11
| <?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.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd" profile="dev">
<jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:test.sql"/> <jdbc:script location="classpath:test1.sql"/> </jdbc:embedded-database> </beans>
|
只有profile属性与当前激活的profile相匹配的配置文件才会被用到。
重复使用beans属性指定多个profile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?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" 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.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
<beans profile="dev"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:test.sql"/> <jdbc:script location="classpath:test1.sql"/> </jdbc:embedded-database> </beans> <beans profile="prod"> <jee:jndi-lookup jndi-name="jdbc/MyDatabase" id="dataSource" resource-ref="true" proxy-interface="javax.sql.DataSource"/> </beans> </beans>
|
虽然id都一样,类型都是javax.sql.dataSource,但是只会创建指定profile的bean。
3.1.2 激活profile
Spring在确定处于激活状态的profile时,依赖于两个独立的属性:
- spring.profiles.active
- spring.profiles.default
优先级从上到下,如果spring.profiles.active没有设置,则看spring.profiles.default,否则只会创建没有定义在profiles中的bean。
有多种方式设置这两个属性:
- 作为DispatcherServlet的初始化参数
- 作为web应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM属性
- 在集成测试类上使用@ActiveProfiles属性
在web.xml配置文件中设置默认的profile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <context-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </context-param>
<servlet> <servlet-name>appServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
|
可以同时激活多个profile,以逗号分隔。
使用profile进行测试:
Spring提供了@ActiveProfiles注解,用来指定测试时使用的profile。
1 2 3 4 5 6
| @RunWith(SpringJunit4ClassRunner.class) @ContextConfiguration(classes={PersistenceTestConfig.class}) @ActiveProfiles("dev") public class PersistenceTest { }
|
3.2 条件化的bean
需求:
- 希望一个或多个bean只有在类路径下包含某个特定的库时才创建
- 希望某个bean在特定的bean声明之后再创建
Spring 4引入了@Conditional注解,只有条件计算结果为true才会创建bean,否则不创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.ch3;
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 MagicExistsCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { Environment environment = conditionContext.getEnvironment(); return environment.containsProperty("magic"); } }
|
1 2 3 4 5
| @Bean @Conditional(MagicExistsCondition.class) public MagicBean magicBean() { return new MagicBean(); }
|
ConditionContext接口:
1 2 3 4 5 6 7 8 9 10 11
| public interface ConditionContext { BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader(); }
|
- getRegistry:根据返回值可以检查bean定义
- getEnvirnment:检查环境变量
- getResourceLoader:读取加载的资源
- getClassLoader:加载并检查类是否存在
AnnotatedTypeMetadata接口:
1 2 3 4 5 6 7 8 9 10 11
| public interface AnnotatedTypeMetadata { boolean isAnnotated(String var1);
Map<String, Object> getAnnotationAttributes(String var1);
Map<String, Object> getAnnotationAttributes(String var1, boolean var2);
MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);
MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2); }
|
3.3 处理启动装配的歧义性
仅有一个bean匹配所需结果时,自动装配才是有效的,如果有多个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数和方法参数。
Spring提供的解决方案:
- 将可选bean中的其中一个声明为首选(primary)
- 使用限定符(qualifier)缩小可选范围
3.3.1 标示首选的bean
将其中一个可选的bean声明为首选可以避免自动装配的歧义性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Autowired public void setDessert(Dessert dessert) { this.dessert = dessert; }
@Component @Primary public class IceCream implements Dessert { }
@Bean @Primary public Dessert dessert() { return new IceCream(); }
|
xml配置:
1
| <bean id="iceCream" class="com.test.dessert.IceCream" primary="true"/>
|
3.3.2 限定自动装配的bean
设置首选bean的局限性在于 @Primary无法将可选方案范围限定到一个无歧义性的选项中 ,当首选bean的数量超过一个时,无法进一步缩小限定范围。
@Qualifier注解是使用限定符的主要方式,与@Autowired协同使用,在注入时指定要注入的bean。
1 2 3 4 5
| @Autowired @Qualifier("iceCream") public void setDessert(Dessert dessert) { this.dessert = dessert; }
|
@Qualifier注解的参数就是想要注入的bean的id,所有使用@Component注解的类都会创建为bean,且id为首字母小写的类名。
基于默认id作为限定符是简单的,但是当类名被更改之后会使限定符失效。
创建自定义的限定符:
可以设置自己的限定符,而不依赖于bean id作为限定符。
1 2 3 4 5
| @Component @Qualifier("cold") public class IceCream implements Dessert { }
|
此时cold限定符分配给了IceCream bean,只需要在合适的地方引入cold限定符即可自动装配。
1 2 3 4 5
| @Bean @Qualifier("cold") public Dessert iceCream() { return new IceCream(); }
|
此时类限定名的变更不会影响到自动装配。但是当应用中出现同名的注解@Qualifier(“cold”)时,歧义性又会再次出现。
这时需要多个@Qualifier注解来进一步缩小限定范围。
3.4 bean的作用域
默认情况下,Spring应用上下文中的所有bean都是以单例模式创建的。不管给定的bean被注入到其他bean多少次,每次注入的都是同一个实例。
如果一个类是可变(mutable)的,那么对其进行重用时可能会遇到意想不到的问题。
Spring定义的bean作用域:
- 单例(Singleton):在整个应用中,只创建一个bean;
- 原型(Prototype):每次注入或者通过上下文获取bean时都创建一个新的bean;
- 会话(Session):在Web应用中,为每个回话创建一个bean;
- 请求(Request):在Web应用中,为每个请求创建一个bean。
@Scope注解:
用来指定bean的作用域:
1 2 3 4 5 6
| @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad { }
|
XML配置:
1
| <bean id="notepad" class="com.app.Notepad" scope="prototype" />
|
3.5 运行时值注入
Spring提供了两种运行时求值的方式:
- 属性占位符(Property placeholder);
- Spring表达式语言(S片EL)。
1 2 3 4 5 6 7 8 9 10 11
| @Configuration @PropertySource("classpath:/com/soundsys/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:
getProperty()方法的四种重载方式:
- String getProperty(String key);
- String getProperty(String key, String defaultValue);
- T getProperty(String key, Class type);
- T getProperty(String key, Class type, T defaultValue);
使用重载形式的getProperty()方法可以避免类型转换:
1
| int connectionCount = env.getProperty("db.connection.count", Integer.class, 10);
|
Environment常见方法:
- boolean containsProperty(String property);
- String[] getActiveProfiles();
- String[] getDefaultProfiles();
- boolean acceptsProfiles(String… profiles)。
解析属性占位符:
Spring支持将属性定义到外部的属性文件中,并使用占位符将其值插入到Spring bean中。在Spring装配中,占位符的形式为使用 “${…}” 的形式包装的属性名称。
1
| <bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}" />
|
使用组件扫描和自动装配时:
1 2 3 4
| public BlankDisc(@Value("${disc.title}" String title, @Value("${disc.artist}") String artist) { this.title = title; this.artist = artist; }
|
SpEL表达式语言:
将表达式语言放到 “#{…}” 之中。
- “#{1 + 1}”
- “#{T(System).currentMillis()}”
- “#{sgtPeppers.artist}”
- “#{false}”
- “#{artistSelector.selectArtists().toUpperCase()}”
SpEL运算符:
| 运算符类型 |
运算符 |
| 算术运算符 |
+、-、*、/、%、 |
| 比较运算符 |
<、>、==、<=、>=、lt、gt、eq、le、ge |
| 逻辑运算符 |
and、or、not、| |
| 条件运算符 |
?:(ternary)、?:() |
| 正则表达式 |
matches |
计算正则表达式:
1
| #{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
|
计算集合:
1 2 3 4 5
| #{jukebox.songs[4].title} #{jukebox.songs[T(java.lang.Math).random()*jukebox.songs.size()].title} #{jukebox.songs.?[artist eq 'Aerosmith']} //.?[]得到集合的一个子集 #{jukebox.songs.^[artist eq 'Areosmith']} //.^[]查询集合中的第一个匹配项 #{jukebox.songs.$[artist eq 'Areosmith'].![title]} //.$[]查询集合中的最后一个匹配项,.![]从集合的每个成员中选择特定的属性放到另外一个集合中
|