bean装配所设计的领域并不仅仅局限于上一章学习到的内容。Spring提供了很多技巧,实现更高级的bean装配功能。
在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另一个环境。开发阶段中,某些环境相关的做法可能并不适合迁移到生产环境中。数据库配置,加密算法以及外部系统的集成是跨环境部署时会发生变化的几个例子。
比如考虑一下数据库配置。在开发环境中,我们可能使用嵌入式数据库,并预先加载测试数据。例如,在Spring配置类中,我们可能会在一个带有@Bean
注解的方法上使用EmbeddedDatabaseBuilder:
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
这样会创建一个类型为javax.sql.DataSource的bean,这个bean是如何创建出来的才是最有意思的。使用EmbeddedDatabaseBuilder会搭建一个嵌入式的Hypersonic数据库,它的模式(schema)定义在schema.sql中,测试数据则是通过test-data.sql加载的。
当你在开发环境中运行集成测试或者启动应用进行手动测试的时候,这个DataSource是很有用的。每次启动它的时候,都能让数据库处于一个给定的状态。
但是对于生产环境中,你可能希望使用JNDI在容器中获取一个DataSource。在这种场景中使用如下的@Bean
方法会更加合适:
@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();
}
显然这里展示的两个版本的dataSource()不相同,虽然都会生成javax.sql.DataSource的bean,但是它们的相似点也仅限于此了。
我们必须要有一种方法来配置它,使得它在各种情况(不止两种)下,都是合适的。
配置profile bean
在Java配置中,可以使用@Profile
注解指定某个bean属于哪个profile。
既可以在类级别上,也可以在方法级别上使用。
下面是方法级别上的使用:
package com.myapp;
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 {
@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:
<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/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<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>
激活profile
Spring确定哪个profile处于激活状态时,需要依赖两个独立的属性:
如果设置了spring.profiles.active的话,它的值就会用来确定哪个profile是激活的。如果没有设置active,那么Spring将会查找default的值。如果均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。
有多种方法来设置这两个属性:
@ActiveProfiles
注解设置。我所喜欢的一种方式是使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,我会在Servlet上下文中设置,例如在Web应用中,web.xml文件如下所示:
略。P75
假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建,或者某个bean只有当另外一个特定的bean也声明后才创建。我们还可能要求只有某个特定的环境变量设置后,才会创建某个bean。
Spring4引入了@Conditional
注解,它可以用到带有@Bean
注解的方法上,如果给定条件为true则会创建这个bean,否则被忽略。
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
可以看到,如果MagicExistsCondition声明了,这个bean才创建。
@Conditional
将会通过Condition接口进行条件对比。
package org.springframework.context.annotation;
import org.springframework.core.type.AnnotatedTypeMetadata;
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
设置给@Conditional
的类可以是任意实现了Condition接口的类型。可以看出来,只需要提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional
注解的bean。
我们这里实现这个接口:
package com.habuma.restfun;
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 context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("magic");//检查是不是有magic属性
}
}
ConditionContext得到了Environment,实际上它是一个接口,可以做到如下几点:
AnnotatedTypeMetadata则能让我检查@Bean
注解的方法上还有什么其他的注解:借助isAnnotated()
能够判断。
因为在自动装配中,如果不仅有一个bean能够匹配结果的话,这种歧义会出错。
比如:
@Autowired
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
以及:
@Component
public class SgtPeppers implements CompactDisc {
...
}
@Component
public class BlankDisc implements CompactDisc {
...
}
因为都实现了@Component
注解,所以,Spring抛出NoUniqueBeanDefinitionException.
所以当歧义发生时,Spring提供了首选primary和限定quelifier来帮助Spring将可选的bean的范围缩小为只有一个bean。
标识首选的bean
@Component
@Primary
public class BlankDisc implements CompactDisc {
...
}
或者
@Configuration
public class CDConfig {
@Bean
@Primary
public CompactDisc compactDisc() {
return new SgtPeppers();
}
}
或者如果使用XML配置:
<bean id="iceCream" class="..IceCream" primary="true"/>
但是如果标识了两个PRIMARY,那就无法正常工作了。
限定自动装配的bean
@Qualifier
注解是使用限定符的主要方式。它与@Autowired
和@Inject
协同使用,在注入的时候指定想要注入进去的是哪个bean。例如:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert=dessert;
}
@Qualifier
注解所设置的参数就是要注入的bean的ID。
创建自定义的限定符
@Component
@Qualifier("blankDisc")
public class BlankDisc implements CompactDisc {
...
}
在这种情况下,在注入的地方,只要引用blankDisc就行:
@Autowired
@Qualifier(value = "blankDisc")
public void setCompactDisc(CompactDisc compactDisc) {
this.compactDisc= compactDisc;
}
当通过显式配置bean的时候,也可以与@Bean
一起使用:
@Configuration
public class SoundSystemConfig {
@Bean
@Qualifier("blankDisc")
public CompactDisc compactDisc(){
return new BlankDisc();
}
}
使用自定义的限定符注解
创建自定义限定符注解:
package soundsystem;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Blank {
}
怎么使用呢?
@Component
@Blank
public class BlankDisc implements CompactDisc {
...
}
所以,假如我们有几个CompactDisc都符合Blank的标签,我们还可以创建一个Light(轻音乐)来区分。
@Component
@Blank
@HipHop
public class BlankDisc implements CompactDisc {
...
}
@Component
@Blank
@Light
public class BlankDJsDisc implements CompactDisc {
...
}
@Scope
来设置singleton或者prototype等等作用域的bean。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class BlankDisc implements CompactDisc {...}
或者在JavaConfig里使用:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public CompactDisc compactDisc(){
return new BlankDisc();
}
或者使用XML来配置bean:
<bean id="notepad" class="..NotePad" scope="prototype"/>
还有session和request等等,暂略。
在XML中声明作用域代理
暂略。
我们在BlankDisc的构造器中,构造器含参数的情况,我们在JavaConfig里通常会这么做:
@Bean
public CompactDisc sgtPeppers(){
return new BlankDisc("title is xxx","author A");}
或者使用XML这样做:
<bean id="sgtPeppers" class="..BlankDisc"
c:_title="title is xxx"
c:_artist="author A"/>
这样的硬编码虽然可以,但是我们可能会希望避免硬编码值,而是想要这些值在运行时才确定。为了实现这种功能,Spring提供了两种方式:
注入外部的值
在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
@Configuration
@PropertySource("classpath:/soundsystem/app.properties")
public class ExpressiveConfig {
@Autowired
Environment environment;
@Bean
public CompactDisc disc(){
return new BlankDisc(environment.getProperty("disc.title"),
environment.getProperty("disc.artist"));
}
}
以及app.properties文件:
disc.title=This is title
disc.artist=Author A
这里的getProperty()
方法,有四种重载的形式:
很容易理解,假如我们从属性文件想要的到的是数字,需要这样写:
int connectionCount= env.getProperty("db.connection.count", Integer.class, 30)
如果想检查getProperty里的属性是不是没有定义:
boolean exists=env.containsProperty("disc.title")
而getRequiredProperty()
如果属性没有定义,则会抛出异常,比起getProperty()
返回Null更加安全。
当然,有时候我们不希望直接先写死赋值,这就需要解析属性占位符。
占位符的形式使用:${...}
在XML中,按照如下方式解析BlankDisc的构造器参数:
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="${disc.title}"
c:_artist="${disc.artist}">
而如果我们使用组件扫描和自动装配来创建和初始化应用组件,我们使用@Value
注解:
public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或者PropertySourcesPlaceholderConfigurer bean。推荐后面一种,因为它能基于Spring Enviroment及其属性源来解析占位符。
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
使用SpEL进行装配
SpEL它可以以强大简洁的方式装配到bean属性和构造器参数中,它有很多特性:
需要了解的第一件事就是SpEL表达式要放到#{...}
中。
这个属性占位符的${...}
相似。
比如
#{1}
,表示的就是1。
#{T(System).currentTimeMillis()}
它的最终结果就是计算表达式的那一刻当前时间的毫秒数。T()表达式将java.lang.System视为Java中对应的类型,因此可以调用static修饰的currentTimeMills()方法。
#{sgtPeppers.artist}
这个表达式会得到ID为sgtPeppers的bean的artist属性。
#{systemProperties{'disc.title'}}
通过systemProperties对象引用系统属性。
所以上面的内容,我们使用SpEL:
public BlankDisc(@Value("#{systemProperties{'disc.title'}}") String title, @Value("#{systemProperties{'disc.artist'}}") String artist) {
this.title = title;
this.artist = artist;
}
或者
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="#{systemProperties{'disc.title'}}"
c:_artist="#{systemProperties{'disc.artist'}}">
当然SpEL还可以引用bean、属性和方法。
比如:
之前的#{sgtPeppers.artist}
引用属性除外,
还有,
#{sgtPeppers}
引用bean
#{artistSelector.selectArtist()}
引用方法
#{artistSelector.selectArtist().toUpperCase()}
引用方法
#{artistSelector.selectArtist()?.toUpperCase()}
当然我们要判断selectArtist是否返回为空,我们使用?.
运算符,用于确保非空。
SpEL 在表达式中使用类型
如果要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键的运算符。例如:
#{T(java.lang.Math)}
#{T(java.lang.Math).PI}
使用pi的值
#{T(java.lang.Math).random()}
获得随机数
#{T(java.lang.Math).random()*circle.radius}
,要使用运算符:乘法
SpEL应该写的尽量简洁,因为它可能测试起来很困难,以上介绍的SpEL只是冰山一角。