一、环境与profile
问题:我们必须要有一种方法来配置DataSource,使其在每种环境下都会选择最为合适的配置。
方案1:在单独的配置类(或XML文件)中配置每个bean,然后在构建阶段(可能会使用Maven的profiles)确定要将哪一个配置编译到可部署的应用中。这种方式的问题在于要为每种环境重新构建应用。
方案2:spring在运行时才去根据环境决定创建哪个bean和不创建哪个bean,可以保证同一个部署单元能够适用于所有的环境,没有必要进行重新构建。
实现方式:
1、配置profile
(1)java方式
使用@Profile
注解指定某个bean
属于哪一个profile;
spring 3.1:@Profile只能定义在类级别;
Spring3.2:@Profile可以定义在方法级别;
没有指定@Profile的bean都会被创建,与激活哪个profile无关;
@Configuration
@Profile("dev") // 告诉此类中bean只有在dev profile激活时才去创建,没有没有激活,带有@Bean注解的的方法都会被忽略掉
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();
}
}
@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();
}
}
(2)xml配置
所有配置文件都会放到部署单元(war文件),但只有profile属性与当前激活profile相匹配的配置文件才会被用到;
<?xml version="1.0" encoding="UTF-8"?>
<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>标签嵌套定义<beans>元素,即可以将所有profile bean定义放在一个文件
<beans .....">
<beans profile="dev">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
<beans profile="prod">
<jee:jndi-lookup id="dataSource"
lazy-init="true"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
</beans>
2、激活profile
(1)按属性顺序判断是否激活profile:spring.profiles.active -> spring.profiles.default
(2)激活方式:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用的上下文参数;
- 作为JNDI条目;
- 作为环境变量;
- 作为JVM的系统属性;
- 在集成测试类上,使用@ActiveProfiles注解设置
如在web.xml中配置spring.profiles.default属性,开发人员可以通过spring.profiles.active配置不同环境的
Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=DataSourceConfig.class)
@ActiveProfiles("dev")
public static class DevDataSourceTest {
...
}
二、条件化的bean
根据不同条件来判断是否创建:只有某个特定bean也声明了之后才会创建、只有某个特定环境变量设置后才会创建等。。。
Spring 4之后,可以使用@Conditional注解来标记条件化bean,它可以作用到带有@Bean注解的方法上,根据@Conditional(条件)中条件的计算结果来判断是否创建这个bean。
@Bean
// 只有满足MagicExistCondition中的条件才会创建MagicBean
@Conditional(MagicExistCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
// 需要实现Condition接口
public class MagicExistCondition implements Condition{
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata){
// 这里添加代码判断是否match
// 返回true则创建bean、false不创建
}
}
三、处理自动装配的歧义性
装配bean时匹配的bean存在多个,此时的解决方法有:
1、标示首选的bean-@primary标记首选
@Component
@Primary // 可与@Component、@Bean配合使用
public class Iceream implements Dessert { ... }
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
<bean id="iceCream" class="com.desserteater.IceCream"
primary="true" />
如果存在多个首选的bean,则又会存在歧义了,此时需要使用限定符了;
2、限定自动装配的bean-@Qualifier
@Qualifier可以与@Autowried、@Inject注解配合使用;
@Autowired
@Qualifier("iceCream")// bean的id
// 当iceCream代表的bean进行了重构,id变了就会出问题
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
为了解决上面对id限定符强依赖问题,以下通过自定义限定符解决;
@Component // 与@Component配合使用
@Qualifier("cold")
public class IceCream implements Dessert { ... }
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
@Bean // 与@Qualifier配合使用
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
如果针对多个相同限定符的bean,则歧义问题又会出现,可以使用自定义限定符注解;
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy { }
@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;
}
四、bean的作用域
1、Spring定义了多种作用域,可以基于这些作用域创建bean:
- 单例(Singleton):在整个应用中,只创建bean的一个实例(默认)。
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
- 会话(Session):在Web应用中,为每个会话创建一个bean实例。
- 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
选择除单例的其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 原型bean
// 或 @Scope("prototype")
public class NotePad { ... }
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTYPE)
public Notepad notepad() {
return new Notepad();
}
<bean id="notepad" class="com.myapp.Notepad"
scope="prototype" />
2、使用会话和请求作用域:P87
@Bean
@Scope(
value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
// 表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean
public ShoppingCart cart() { ... }
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
...
}
ScopedProxyMode.TARGET_CLASS:表明要以生成目标类扩展的方式创建代理。
3、xml中声明作用域代理
<!--
scope :设置bean作用域
aop:scope-proxy:设置作用域代理,与@Scope注解的proxyMode属性功能相同
默认情况下,会使用CGLib创建目标类的代理。但是也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理
-->
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scope-proxy />
</bean>
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scope-proxy proxy-target-class="false" />
</bean>
五、运行时注入
避免将属性值等字面量信息写死在代码中或xml中,可以使用spring提供的2种运行时求值方式:
- 属性占位符(Property placeholder)
- Spring表达式语言(SpEL)
1、注入外部值
最简单方式是声明属性源并通过Spring的Environment来检索属性。
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
// 属性文件会被加载到Environment中
public class EnvironmentConfig {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title"), // 在app.properties中声明
env.getProperty("disc.artist"));
}
}
2、属性占位符
(1)配置:
配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。从Spring 3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。
a、java方式
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer {
return new PropertySourcesPlaceholderConfigurer();
}
b、xml方式
<?xml version="1.0" encoding="UTF-8"?>
<beans .....">
<context:property-placeholder />
<!-- 可以生成PropertySourcesPlaceholderConfigurer bean-->
</beans>
(2)使用:
xml中使用${disc.title} 形式,disc.title具体值定义在外部文件中;
java代码中使用@value注解,如@Value("${disc.title}") String title
3、Spring表达式语言
(1)SpEL拥有很多特性,包括:
- 使用bean的ID来引用bean;
- 调用方法和访问对象的属性;
- 对值进行算术、关系和逻辑运算;
- 正则表达式匹配;
- 集合操作。
(2)SpEL表达式要放到#{...}
之中,属性占位符是放在${...}
中
(3)更多见课本 P96
1、#{1}: 结果为1
2、#{T(System).currentTimeMillis()} :那一刻当前时间的毫秒数
T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用其static修饰的
currentTimeMillis()方法。
3、#{sgtPeppers.artist}: 计算得到ID为sgtPeppers的bean的artist属性
4、#{systemProperties['disc.title']}:通过systemProperties对象引用系统属性
5、#{3.14159}:浮点值
6、#{9.87E4}:科学计数法
7、#{'Hello'}:字符串
8、#{true}:Boolean类型
9、#{sgtPeppers}:引用bean
10、#{artistSelector.selectArtist()}:引用bean的方法
11、#{artistSelector.selectArtist().toUpperCase()}:
12、#{artistSelector.selectArtist()?.toUpperCase()}:
如果artistSelector.selectArtist()返回为null,不会调toUpperCase(),表达式结果也为null