Bean definition profiles是3.1版本引入的新特性。
背景
它提供了一种机制:当客户端请求某一bean时,容器可以根据不同的环境注册并返回不同的bean。“environment”对不同的用户也说意义也不太一样,把应用部署在性能环境中测试时,注册的就是监控信息;或者客户A和客户B部署时各自有自己的实现。最常见场景是:在开发阶段使用单独的数据源,在生产环境可能用JNDI查找相同的数据源。Bean definition profiles提供了满足这些场景的通用机制。
简单的业务案例
用junit演示了银行系统中两个账户之间的转账功能
public class IntegrationTests { @Test public void transferTenDollars() throws InsufficientFundsException { ApplicationContext ctx = // instantiate the spring container TransferService transferService = ctx.getBean(TransferService.class); AccountRepository accountRepository = ctx.getBean(AccountRepository.class); assertThat(accountRepository.findById("A123").getBalance(), equalTo(100.00)); assertThat(accountRepository.findById("C456").getBalance(), equalTo(0.00)); transferService.transfer(10.00, "A123", "C456"); assertThat(accountRepository.findById("A123").getBalance(), equalTo(90.00)); assertThat(accountRepository.findById("C456").getBalance(), equalTo(10.00)); } }
以上代码片段中,稍后将介绍容器的初始化,首先注意到我们的目标很简单--从账户A123转10元钱到账户C456。这个测试断言两个账户的初始余额、执行转账、断言转账后余额。
常见的XML配置
bean definition profiles支持注解和XML两种配置,先以大家都熟悉的XML配置演,。为了方便起见在测试中使用HSQLDB数据库,配置如下
<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="..."> <bean id="transferService" class="com.bank.service.internal.DefaultTransferService"> <constructor-arg ref="accountRepository"/> <constructor-arg ref="feePolicy"/> </bean> <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository"> <constructor-arg ref="dataSource"/> </bean> <bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans>
jdbc:命名空间在3.0中引入的,支持内嵌数据库。根据以上代码可以想到容器初始化如下
public class IntegrationTests { @Test public void transferTenDollars() throws InsufficientFundsException { GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); ctx.load("xxx.xml"); ctx.refresh(); TransferService transferService = ctx.getBean(TransferService.class); AccountRepository accountRepository = ctx.getBean(AccountRepository.class); // perform transfer and issue assertions as above ... } }
以上片段中,使用GenericXmlApplicationContext 载入bean文件,也可以使用ClassPathXmlApplicationContext。
这个测试将成功运行,现在我们关注下如果把环境切换到生产环境。常见的场景是我们使用TOMCAT作开发用weblogic或websphere作生产环境,我们必须查找应用服务器上配置的JNDI数据源。为了得到数据源,我们必须使用JNDI查找。spring提供了这种支持常用<jee:jndi-lookup/>,那上面的spring配置中数据源配置做如下修改
<beans ...> <bean id="transferService" ... /> <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository"> <constructor-arg ref="dataSource"/> </bean> <bean id="feePolicy" ... /> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans>
以上配置没什么问题,在不同的环境下我们经常的解决方案是准备几份配置,开发、测试、生成环境。在部署到生产环境时使用一些方法将生成环境配置覆盖掉之前的配置;或者结合系统环境变量和包含占位符${placeholder}的<import/>语句来解决路径问题。对spring容器来讲这并非最优方案。
理解Bean definition profiles
了解以上案例特定于环境的配置,我们将对某一环境下注册这个环境的bean definition。也可以说注册特定场景A下的bean definitions,或注册场景B下的bean definition.
spring 3.1中,<beans/>解决了这个问题,我们将把配置文件分为3份并注意*-datasource.xml中的profile="..."配置。
src/main/com/bank/config/xml/transfer-service-config.xml
<beans ...> <bean id="transferService" ... /> <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository"> <constructor-arg ref="dataSource"/> </bean> <bean id="feePolicy" ... /> </beans>
src/main/com/bank/config/xml/standalone-datasource-config.xml
<beans profile="dev"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans>
src/main/com/bank/config/xml/jndi-datasource-config.xml
<beans profile="production"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans>
修改测试代码
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); ctx.load("classpath:/com/bank/config/xml/*-config.xml"); ctx.refresh();
以上配置并不完全,虽然我们配置了profile当运行测试时会报错NoSuchBeanDefinitionException,因为容器找不到"datasource"。原因是我们还没有激活profile。
引入Environment
Environment架构是spring 3.1中引入的,具体内容别的章节再介绍。总之它包含激活profile必须信息。当ApplicationContext 载入bean配置文件时,只要配置了ApplicationContext <beans profile="...">中的profile,都会在Environment中查找与之一致的信息,如果找不到该<beans />根本不会被解析和注册。
激活profiles有几种方式,首先用ApplicationContext API编程式的注册
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); ctx.getEnvironment().setActiveProfiles("dev"); ctx.load("classpath:/com/bank/config/xml/*-config.xml"); ctx.refresh();
运行测试,容器将对classpath:/com/bank/config/xml/*-config.xml匹配的文件作如下思考
- transfer-service-config.xml 没有指定profile属性,总被解析
- standalone-datasource-config.xml 指定profile="dev" "dev" profile被激活,所以被解析
- jndi-datasource-config.xml 指定profile="production" "production" profile 没被激活,所以不被解析
当我们切换到生产环境使用JNDI查找时得激活production profile.在单元测试中编程式的指定是可以的,在war包被创建或者应用准备部署时这种做法是不可行的。出于这种原因,profiles可以使用spring.profiles.active或spring.profiles.default属性被声明式的激活。这些属性可以通过系统环境变量、JVM系统属性、web.xml中servlet上下文的参数、甚至JNDI中的一个实体。例如在web.xml配置如下
servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>spring.profiles.active</param-name> <param-value>production</param-value> </init-param> </servlet>
profiles 的指定可以有多个,编程式的API支持可变参数列表
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
声明式指定时也可以有多个用','分割,例如在JVM系统属性中指定
-Dspring.profiles.active="profile1,profile2"
Bean definition files中可以指定多个候选者
<beans profile="profile1,profile2"> ... </beans>
内置<beans/>
现在的profile="dev" 和profile="production" 都是在顶级的<beans/>中指定的。现在有配置三个文件,为了合并成一个文件spring3.1开始支持内置的<beans/>,之前的三个文件可以合并成一个
<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="..."> <bean id="transferService" class="com.bank.service.internal.DefaultTransferService"> <constructor-arg ref="accountRepository"/> <constructor-arg ref="feePolicy"/> </bean> <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository"> <constructor-arg ref="dataSource"/> </bean> <bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/> <beans profile="dev"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans> <beans profile="production"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans> </beans>
为了避免混乱内置<beans profile/>必须在文件最后指定,这点spring-beans-3.1.xsd中有要求
支持@Profile
之前的案例全部换成注解风格的,首先看看之前的xml配置
<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="..."> <bean id="transferService" class="com.bank.service.internal.DefaultTransferService"> <constructor-arg ref="accountRepository"/> <constructor-arg ref="feePolicy"/> </bean> <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository"> <constructor-arg ref="dataSource"/> </bean> <bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans>
src/main/com/bank/config/code/TransferServiceConfig.java
@Configuration public class TransferServiceConfig { @Bean public TransferService transferService() { return new DefaultTransferService(accountRepository(), feePolicy()); } @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource()); } @Bean public FeePolicy feePolicy() { return new ZeroFeePolicy(); } @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } }
EmbeddedDatabaseBuilder是<jdbc:embedded-database/>底层组件,@Bean方法非常方便
现在我们开始基于@Configuration的单元测试
src/test/com/bank/config/code/IntegrationTests.java
public class IntegrationTests { @Test public void transferTenDollars() throws InsufficientFundsException { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(TransferServiceConfig.class); ctx.refresh(); TransferService transferService = ctx.getBean(TransferService.class); AccountRepository accountRepository = ctx.getBean(AccountRepository.class); assertThat(accountRepository.findById("A123").getBalance(), equalTo(100.00)); assertThat(accountRepository.findById("C456").getBalance(), equalTo(0.00)); transferService.transfer(10.00, "A123", "C456"); assertThat(accountRepository.findById("A123").getBalance(), equalTo(90.00)); assertThat(accountRepository.findById("C456").getBalance(), equalTo(10.00)); } }
以上片段中,AnnotationConfigApplicationContext 允许注册 @Configuration 和基于@Component的注解。没有xml简直太方便了。但是遇到了同样的问题:当部署到生产环境时standalone datasource是不合适的,我们需要从JNDI上查找。
把内嵌的和基于JNDI数据源配置分开
src/main/com/bank/config/code/StandaloneDataConfig.java
@Configuration @Profile("dev") public class StandaloneDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } }
src/main/com/bank/config/code/JndiDataConfig.java
@Configuration @Profile("production") public class JndiDataConfig { @Bean public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } }
src/main/com/bank/config/code/TransferServiceConfig.java
@Configuration public class TransferServiceConfig { @Autowired DataSource dataSource; //standlone or jndi @Bean public TransferService transferService() { return new DefaultTransferService(accountRepository(), feePolicy()); } @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } @Bean public FeePolicy feePolicy() { return new ZeroFeePolicy(); } }
开始测试
src/test/com/bank/config/code/IntegrationTests.java
public class IntegrationTests { @Test public void transferTenDollars() throws InsufficientFundsException { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setActiveProfiles("dev"); ctx.register(TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); ctx.refresh(); // proceed with assertions as above ... } }
以上片段列出了配置class,AnnotationConfigApplicationContext 支持通配符就像xml中支持**/*-config.xml
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setActiveProfiles("dev"); ctx.scan("com.bank.config.code"); // find and register all @Configuration classes within ctx.refresh();
进一步改善@Configuration结构
通过@Autowired 在TransferServiceConfig中装配一个数据源,但是这个数据远来自何方?不是很清晰,可以让StandaloneDataConfig 和JndiDataConfig实现如下接口
interface DataConfig { DataSource dataSource(); }
@Configuration public class StandaloneDataConfig implements DataConfig { ... }
@Configuration public class JndiDataConfig implements DataConfig { ... }
将@Autowired dataSource 换成@Autowired DataConfig
src/main/com/bank/config/code/TransferServiceConfig.java
@Configuration public class TransferServiceConfig { @Autowired DataConfig dataConfig; // ... @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataConfig.dataSource()); } // ... }
按 CTRL-T,很容易知道数据源来自哪里
自定义@Profile
可以用@Profile定义自己的注解,然后和其他的注解一起使用 package com.bank.annotation; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Profile("dev") pubilc @interface Dev { } @Dev @Component public class MyDevService { ... } @Dev @Configuration public class StandaloneDataConfig { ... }
profile特性实现点
基于xml的实现 org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader中
protected void doRegisterBeanDefinitions(Element root) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { Assert.state(this.environment != null, "environment property must not be null"); String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); if (!this.environment.acceptsProfiles(specifiedProfiles)) {//关键部分 return; } } // any nested <beans> elements will cause recursion in this method. In // order to propagate and preserve <beans> default-* attributes correctly, // keep track of the current (parent) delegate, which may be null. Create // the new (child) delegate with a reference to the parent for fallback purposes, // then ultimately reset this.delegate back to its original (parent) reference. // this behavior emulates a stack of delegates without actually necessitating one. BeanDefinitionParserDelegate parent = this.delegate; this.delegate = createHelper(readerContext, root, parent); preProcessXml(root); parseBeanDefinitions(root, this.delegate); postProcessXml(root); this.delegate = parent; }
基于注解实现
在org.springframework.context.annotation.AnnotatedBeanDefinitionReader中
public void registerBean(Class<?> annotatedClass, String name, Class<? extends Annotation>... qualifiers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass); AnnotationMetadata metadata = abd.getMetadata(); if (ProfileHelper.isProfileAnnotationPresent(metadata)) { if (!this.environment.acceptsProfiles(ProfileHelper.getCandidateProfiles(metadata))) { //关键部分 return; } } ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); if (qualifiers != null) { for (Class<? extends Annotation> qualifier : qualifiers) { if (Primary.class.equals(qualifier)) { abd.setPrimary(true); } else if (Lazy.class.equals(qualifier)) { abd.setLazyInit(true); } else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } } } BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); }