一、环境与profile
在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另一个环境,因为开发阶段中,某些环境相关的做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。比如数据库的配置中,有可能在测试的时候使用的嵌入式的数据库,并且加载相关的测试数据,但是在生产环境中可能会使用JNDI
获取一个DataSource
,或者配置一个数据库连接池C3P0
,每种取得DataSource
的方式都不一样,以前可能会在XML
中配置多种策略,然后在构建(比如在XML
文件中选择某种策略)的时候选择不同的策略。下面看spring
如何处理这个问题。
1.1 配置profile bean
其实spring
提供的方案和构建解决方案没有太大的差别,但是spring
并不是在构建时选择某种策略,而是在运行时再来确定。这样同一个部署单元能够适用于所有的环境,没必要重新构建。
在3.1
版本中,spring
引入了bean profile
的功能,要使用此功能,首先要将所有不同的bean
定义整理到一个或多个profile
中,在应用部署到每个环境时,要确保对应的profile
处于激活(active
)状态。
在Java
配置中,可以使用@Profile
注解指定某个bean
属于哪一个profile
,如配置一个嵌入式数据库DataSource
:
@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
只有在dev profile
激活时才会创建,这里可以表示是在开发环境下的bean
。我们还可以配置一个生产环境下的bean
:
@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();
}
说明:虽然这里一次性配置了多个profile
,但是只有被激活的那个profile
对应的bean
会被创建。
注意:以上的profile
配置都可以在总的数据源配置类DataSourceConfig
中进行配置。
注意:上面@Bean
中配置了destroyMethod
方法,一般情况下是会执行相关方法的,比如destroyMethod = "destroy"
就表示此bean
在销毁时会执行其destroy
方法,但是会默认匹配找到close、shutdown
方法(只要此类实现了java.lang.AutoCloseable
或java.io.Closeable
),具体信息请参看Spring
指导手册的6.6.1
小节。但是这里的DataSource
类和EmbeddedDatabaseBuilder
类中都没有shutdown
方法,不清楚配置是什么意思。
1.1.1 在 XML 中配置 profile
如果要配置一个profile
,可以向下面这样在beans
标签中配置:
说明:但是如果需要配置多个profile
,就不能这样了,我们可以重复使用
元素来指定多个profile
,如下所示:
1.2 激活 profile
spring
在确定哪个profile
处于激活状态时,需要依赖两个独立的属性:spring.profiles.active
和spring.profiles.default
。如果设置了前一个属性,那么它的值就用来确定哪个profile
是激活的,但是如果没有,则去后一个属性中的值,这个值即一个默认值。如果两个属性都没有设置,则没有profile
会被激活。有多种方式来设置这两个属性:
- 作为
DispatcherServlet
的初始化参数 - 作为
Web
应用的上下文参数 - 作为
JNDI
条目 - 作为环境变量
- 作为
JVM
的系统属性 - 在集成测试上,使用
@ActiveProfiles
注解设置
这里我们看使用DispatcherServlet
的参数将spring.profiles.default
设置为开发环境的profile
,需要在servlet
上下文进行设置(为了兼顾到ContextLoaderListener
)。如下所示:
web.xml
contextConfigLocation
/WEB-INF/spring/root-context.xml
spring.profiles.default
dev
org.springframework.web.context.ContextLoaderListener
appServlet
org.springframework.web.servlet.DispatcherServlet
spring.profiles.default
dev
1
appServlet
/
说明:这里我们配置了默认的profile
,如果今后还有其他的profile
,则可以设置spring.profiles.active
属性,这样就可以覆盖掉默认属性。同时我们也可以激活多个profile
,使用逗号分隔,但是激活多个profile
意义不大。
1.2.1 使用 profile 进行测试
配置好一个或多个profile
之后,在测试或者实际运行的时候需要激活某个profile
,此时我们可以使用@ActiveProfiles
注解来将某个profile
激活:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(class={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest{
......
}
1.2.2 具体测试
这里由于书中例子不是很完整,所以这里我们使用《8、装配bean(补)(spring笔记)》这一节中的例子测试一下,首先我们对配置类做一下改动(使用@Bean
配置方式):
Config.java
@Configuration
public class Config {
@Bean
@Profile("dev")
public UserDao getUserDao4MySql(){
return new UserDao4MySqlImpl();
}
@Bean
@Profile("product")
public UserDao getUserDao4Oracle(){
return new UserDao4OracleImpl();
}
@Bean
public UserManager getUserManager(UserDao userDao){
return new UserManagerImpl(userDao);
}
}
测试的时候我们可以选择激活哪一个数据库配置:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=win.iot4yj.spring.config.Config.class)
@ActiveProfiles("product")
public class IoCTest {
}
可以看到这里激活了Oracle
的配置。对于XML
配置方式,其实差不多,这里不再细说。
二、条件化的 bean
有时候我们希望某个bean
在满足某些条件时才创建,否则就不创建。假设有一个MagicBean
类,我们希望只有设置了magic
环境属性的时候,Spring
才会实例化这个类,否则就忽略此类:
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
说明:这里我们使用@Conditional
注解指明条件为MagicExistsCondition
。@Conditional
将会通过Condition
接口进行条件对比:
public interface Condition{
boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata);
}
说明:设置给@Conditional
的类可以是任意实现了Condition
接口的类型。可以看到我们只要实现matches
方法即可:
package com.habuma.restfun;
import ...
public class MagicExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("magic");
}
}
说明:
上述
matches
方法通过给定的ConditionContext
对象进而得到Environment
对象,并使用此对象检查环境中是否存在名为magic
的环境属性,这里属性的值是什么无所谓。如果属性满足则条件满足,bean
就能被创建出来,否则,就忽略。如果考虑的因素更多,matches
方法则可能需要使用ConditionContext
和AnnotatedTypeMetadata
对象来做出决策。其中
ConditionContext
是一个接口,大致如下:
public interface ConditionContext{
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
使用此接口可以做到如下几点:
借助
getRegistry()
返回的BeanDefinitionRegistry
检查bean
定义借助
getBeanFactory()
返回的ConfigurableListableBeanFactory
检查bean
是否存在,甚至检查bean
的属性借助
getEnvironment()
返回的Environment
检查环境变量是否存在以及它的值是什么读取并探查
getResourceLoader()
返回的ResourceLoader
所加载的资源借助
getClassLoader()
返回的ClassLoader
加载并检查类是否存在而
AnnotatedTypeMetadata
则能够让我们检查带有@Bean
注解的方法上还有什么其他的注解,也是一个接口:
public class AnnotatedTypeMetadata{
boolean isAnnotated(String annotationType);
Map getAnnotationAttributes(String annotationType);
Map getAnnotationAttributes(String annotationType, boolean classValueAsString);
MultiValueMap getAllAnnotationAttributes(String annotationType);
MultiValueMap getAllAnnotationAttributes(String annotationType, boolean classValueAsString);
}
说明:借助isAnnotated()
方法能够判断带有@Bean
注解的方法是不是还有其他特定的注解;借助其他方法,能够检查@Bean
注解方法上其他注解的属性。
从Spring 4
开始,@Profile
注解进行了重构,使其基于@Conditional
和Condition
实现:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
说明:可以看到@Profile
本身也使用了@Conditional
注解,并且引用ProfileCondition
作为Condition
实现,实现中考虑到了ConditionContext
和AnnotatedTypeMetadata
中的多个因素:
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
}
return true;
}
}
说明:首先是得到了@Profile
注解的所有属性。借助该信息,会明确地检查value
属性,该属性包含了bean
的profile
名称,然后通过Environment
来检查[借助acceptsProfiles()
方法]该profile
是否处于激活状态。就是比较环境中的value
值和profile
中的value
值是不是一致的。
三、处理自动装配的歧义性
在自动装配时,如果仅有一个bean
匹配所需的结果时,自动装配才是有效的。如果不仅有一个bean
能够匹配结果的话,这种歧义性会阻碍Spring
自动装配属性、构造器参数或方法参数。举例说明:
@Autowired
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
说明:上例中Dessert
是一个接口,有如下三个实现:
@Component
public class Cake implements Dessert{...}
@Component
public class Cookies implements Dessert{...}
@Component
public class IceCream implements Dessert{...}
说明:此时如果要自动装配setDessert
方法,那么有三个可以匹配的bean
,这样就会造成歧义。下面看如何解决这种歧义。
3.1 标示首选的 bean
对于上面三个可选的bean
,我们可以标识一个为首选的bean
,这样就不会出现歧义了:
@Component
@Primary
public class Cake implements IceCream {...}
当然也可以使用XML
方式配置:
说明:但是如果我们配置多个首选,那么又会出现歧义。就解决歧义性的问题,限定符是一种更为强大的机制。
3.2 限定自动装配的 bean
之前的@Primary
只能标识一个优选方案,但是并不能解决歧义性问题。而限定符能够在所有可选的bean
上进行缩小分为的操作,最终能够达到只有一个bean
满足所有要求。如果依然存在歧义性,那么可以继续使用更多的限定符来缩小范围。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
说明:这是使用限定符最简单的例子。为@Qualifier
注解所设置的参数就是想要注入的bean
的ID
。但是要注意:这个"iceCream"
要和实际bean
的ID
一致。但是如果重构了IceCream
类,将其重命名为Gelato
的,同时有是使用默认ID
,那么就会出现问题。于是我们可以创建自定义的限定符来解决此问题。
3.2.1 创建自定义的限定符
我们可以为bean
设置自己的限定符,而不是依赖于将bean ID
作为限定符:
@Component
@Qualifier("cold")
public class IceCream implements Dessert{...}
说明:这里我们还是使用@Qualifier
注解来为bean
创建了一个自定义的限定符"cold"
,而且不依赖bean
的类名或ID
,于是方法上可以这样使用:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
说明:更为重要的是通过Java
配置显示定义bean
的时候,@Qualifier
可以和@Bean
一起使用:
@Bean
@Qualifier("cold")
public Dessert IceCream{
return new IceCream();
}
说明:在使用自定义限定符的时候,最佳实践是为bean
选择特征性或描述性的术语。
3.2.2 使用自定义的限定符注解
如果此时我们有引入了一个新的Dessert bean
:
@Component
@Qualifier("cold")
public class Popsicle implements Dessert{...}
此时就有两个实现了Dessert
接口的bean
,而且使用相同的自定义限定符,这样会显然会造成歧义,于是我们可以再加上一层限定:
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert{...}
于是此时我们可以这样定义方法setDessert
:
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
说明:
低版本的
Java
不与许在同一个条目上重复出现相同类型的多个注解,但是Java8
允许,只要这个注解本身定义的时候带有@Repeatable
注解就可以,不过Spring
的@Qualifier
注解并没有在定义时添加@Repeatable
注解。仅仅使用@Qualifier
并没有办法将可选的bean
缩小到仅有一个可选的bean
。这里我们可以使用自定义的限定符注解解决。这里所需要做的就是创建一个注解,它本身要使用
@Qualifier
注解来标注:
@Target({ElementType.ANNOTATION_TYPE.CONSTRUCTOR, ElementType.FIELD,
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS.RUNTIME)
@Qualifier
public @interface Cold {}
这样便定义了一个注解,可以这样使用:
@Component
@Cold
public class IceCream implements Dessert{...}
说明:通过声明自定义的限定符注解,可以同时使用多个限定符,不会再有其他问题。同时,相对于使用原始的@Qualifier
并借助String
类型来指定限定符,自定义的注解也更为类型安全。