第三章:高级装配
本章内容:
- Spring profile
- 条件化的bean声明
- 自动装配与歧义性
- bean的作用域
- Spring表达式语言
3.1 环境与profile
在软件开发的不同阶段需要不同的环境和配置。
@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。
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;
/**
* @author [email protected]
* @since 2018/11/24 下午1:00
*/
@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。
<?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:
<?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:
<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。
@RunWith(SpringJunit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
}
3.2 条件化的bean
需求:
- 希望一个或多个bean只有在类路径下包含某个特定的库时才创建
- 希望某个bean在特定的bean声明之后再创建
Spring 4引入了@Conditional注解,只有条件计算结果为true才会创建bean,否则不创建。
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;
/**
* @author [email protected]
* @since 2018/11/24 下午2:07
*/
public class MagicExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
return environment.containsProperty("magic");
}
}
@Bean
@Conditional(MagicExistsCondition.class) //条件化创建bean
public MagicBean magicBean() {
return new MagicBean();
}
ConditionContext接口:
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
- getRegistry:根据返回值可以检查bean定义
- getEnvirnment:检查环境变量
- getResourceLoader:读取加载的资源
- getClassLoader:加载并检查类是否存在
AnnotatedTypeMetadata接口:
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声明为首选可以避免自动装配的歧义性。
@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配置:
<bean id="iceCream" class="com.test.dessert.IceCream" primary="true"/>
3.3.2 限定自动装配的bean
设置首选bean的局限性在于 @Primary无法将可选方案范围限定到一个无歧义性的选项中 ,当首选bean的数量超过一个时,无法进一步缩小限定范围。
@Qualifier注解是使用限定符的主要方式,与@Autowired协同使用,在注入时指定要注入的bean。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
@Qualifier注解的参数就是想要注入的bean的id,所有使用@Component注解的类都会创建为bean,且id为首字母小写的类名。
基于默认id作为限定符是简单的,但是当类名被更改之后会使限定符失效。
创建自定义的限定符:
可以设置自己的限定符,而不依赖于bean id作为限定符。
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
}
此时cold限定符分配给了IceCream bean,只需要在合适的地方引入cold限定符即可自动装配。
@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的作用域:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
//或者 @Scope("prototype")
public class Notepad {
//something
}
XML配置:
<bean id="notepad" class="com.app.Notepad" scope="prototype" />
3.5 运行时值注入
Spring提供了两种运行时求值的方式:
- 属性占位符(Property placeholder);
- Spring表达式语言(S片EL)。
@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()方法可以避免类型转换:
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装配中,占位符的形式为使用 “${…}” 的形式包装的属性名称。
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}" />
使用组件扫描和自动装配时:
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 |
计算正则表达式:
#{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}
#{jukebox.songs.?[artist eq 'Aerosmith']} //.?[]得到集合的一个子集
#{jukebox.songs.^[artist eq 'Areosmith']} //.^[]查询集合中的第一个匹配项
#{jukebox.songs.$[artist eq 'Areosmith'].![title]} //.$[]查询集合中的最后一个匹配项,.![]从集合的每个成员中选择特定的属性放到另外一个集合中