Spring in Action --- 第三章 高级装配
Spring profile
使用Spring profile 可以设定在不同的环境中启用不同的bean.由于环境的不同,数据库配置,加密算法以及与外部系统的集成可能会有不同的表现,特别是数据库,一般开发环境,QA环境,预发布环境,生产环境会分开
Spring并不是在构建的时候做出这样的决策的,而是等到运行时再来确定
在java中
使用@Porfile
注解指定某个bean属于哪一个profile,
@Profile("dev")
表示profile环境是dev.
只有相应的profile激活的时候,才会创建对应的bean,但是,没有指定profile的bean始终都会被创建,与激活哪个profile没有关系
在XML中
<beans xmlns="http://springframework.org/schema/beans"
xmlns:...
profile="dev">
</beans>
创建不同的XML文件设置不同的profile,也可以在根<bean>
元素中嵌套定义<beans>
元素,而不是为每一个环境创建一个profile XML文件.
激活 profile
Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default,如果设置了 spring.profiles.active 属性,那么它的值就会用来确定哪个profile是激活的,但是如果没有设置,那 Spring会查找spring.profiles.defalut的值.如果两者均为设置,那么就没有激活的profile,Spring只会创建哪些没有定义在profile中的bean.
设置属性的方式
- 作为 DispatcherServlet 的初始化参数
- 作为 Web 应用的上下文参数
- 作为 JNDI 条目
- 作为环境变量
- 作为JVM的系统属性
示例:
在web.xml中为上下文设置默认的profile
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
在web.xml中为Servlet设置默认的profile
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>
prg.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</init-param>
</servlet>
使用profile进行测试
Spring提供了@ActiveProfiles
注解指定运行测试时要激活哪个profile.
条件化的 bean
Spring4以后,引入了@Conditional
,可以用到带有@Bean
注解的方法上,用于计算给定的条件如果为true,就会创建这个bean,否则的话这个bean会被忽略
例如有一个Test的类,我们希望只有设置了 test 环境属性的时候,Spring才会实例化这个类,如果没有,就会被忽略.下面的配置展示了注解的使用方式
@Bean
@Conditional(TestExistsCondition.class)
public Test test() {
return new Test();
}
@Condition
中给定了一个Class,指明创建该bean的条件,设置给它的类可以是任意实现了Condition接口的类型.下面展现了TestExistsCondition类的实现
public class TestExistsCondition() {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("test");
}
}
其中的两个参数 ConditionContext, AnnotatedTypeMetadata 都是接口,具体请看P77
处理自动装配的歧义性
在使用自动装配时,如果一个接口有多个实现类并且都被定义为bean,当Spring试图装配的时候就会无法做出选择从而抛出 NoUniqueBeanDefinitionException, Spring提供了两种方式来解决这样的问题:
- 将可选bean的某一个设置为首选的bean
- 使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean
标示首选的bean
在 bean 上使用@Primary注解标示当前的bean是首选bean,如果你是用XML来实例化bean
,可以用下面的方式:
<bean id="iceCream" class="com.IceCream" primary="true">
</bean>
但是,如果你设置了两个或者更多的首选bean,那么它就无法正常工作了
限定自动装配的bean
@Qualifier注解是使用限定符的主要方式:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert
}
上面的代码片段是最简单的例子,为@Qualifier注解所设置的参数就是想要注入的bean的ID,所有的@Component注解创建的类的ID都是首字母变为小写的类名.需要注意的是,如果没有指定其他的限定符的话,所有的bean都会给定一个默认的限定符,这个限定符与bean的ID相同
这里有个问题就是,指定的限定符与要注入的bean的名称是紧耦合的,如果bean的名字修改了,那么就会无法注入,解决方案是在bean上使用@qualifier
指定该bean的限定符
使用自定义的限定符注解
如果iceCream同时被使用在两个或更多的bean上,那怎么办呢?你可能会想到的是使用更多的限定符来缩小范围:
@Component
@Qualifier("iceCream")
@Qualifier("cold")
public class IceCream implements Dessert {...}
这里只有一个小问题,Java不允许在同一个条目上重复出现相同类型的多个注解.如果你这么做了,编译将会报错.但是,我们可以创建自定义的限定符注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Qualifier
public @interface Cold{}
上面的代码片段定义了一个自定义注解,它本身要使用@Qualifier来标注,这样就可以不断缩小范围,直到唯一.
bean 的作用域
在默认情况下,Spring应用上下文中所有的bean都是作为以单利的形式创建的,不过,Spring提供了多种作用域,可以基于这些作用域创建bean:
- 单例:在整个应用中,只创建bean的一个实例
- 原型:每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例
- 会话:在Web应用中,为每个会话创建一个bean实例
- 请求:在Web应用中,为每个请求创建一个bean实例
默认是单例模式,如果选择其他的作用域,要使用@Scope注解,例如,要将一个bean生命为原型,需要这样做:
@bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{...}
使用组件扫描来发现和声明bean,只需要加上@Component
注解替换@Bean
,同样,如果使用XML来配置bean的话,可以使用scope属性来设置作用域:
<bean id="notepad" class ="com.notepad" scope="prototype" />
对于会话作用域,在当前会话的相关操作中,bean实际上是单例的:
@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() {...}
要注意的是,同时还有一个proxyMode属性被设置,请看下面的代码:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoopingCart) {
this.shoppingCart = shoppingCart
}
}
这个service是一个单例的bean,当它创建的时候,Spring会试图将ShoppingCart bean注入,但是ShoppingCart是会话作用域的,此时并不存在,知道某个用户进入到系统才会出现实例,另外,系统中将会有多个ShoppingCart实例被创建,我们希望注入的bean刚好是当前会话对应的那一个,因此,Spring会向StroeService中注入一个到ShoppingCart的代理.关于具体的描述,请看P87
运行时注入
在代码中,最好的方式是不要在代码中出现硬代码,因此我们需要借助Spring的一些配置来注入外部的值:
@Configuration
@PropertySource("")
public class ExpressiveConfig {
@Autowired
Environment env;
@Bean
public BlankDisc disc() {
return new BlankDisc(env.getProperty("disc.title"));
}
}
我们使用了@ PropertySource
注解引用了外部的一个属性文件,其中有disc.title属性,会被读取并配置到bean中,如果这个属性没有被定义,获取到的值会是null,如果你希望这个属性必须被定义,可以使用getRequiredProperty()方法.
另外,可以使用Environment.containsProperty()方法来判断是否有某一个属性.最后,如果想将属性解析为类的话,可以使用getPropertyAdClass()方法.
如果我们依赖组件扫描和自动装配来创建和初始化应用组件的话,娜美可以使用@Value
注解,它与@Autowired
非常相似:
public BlankDisc(@Value("${disc.title}") String title) {
this.title = title;
}
为了使用占位符,我们必须要配置一个PropertyPlaceHolderConfigurer bean或 PropertySourcePlaceHolderConfigurer bean,具体请看P92
注解
@Profile
Spring 3.2以后,注解支持在类和方法级别上使用
@Profile("...")
括号中可以定义任意的字符串,但建议用有意义的字符串