`
dylan0514sina.cn
  • 浏览: 94899 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

支持开发、测试、生产环境下bean配置切换的profiles特性

 
阅读更多
介绍
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 没被激活,所以不被解析
结果只有standalone-datasource-config.xml中的datasource被实例化并被注入

当我们切换到生产环境使用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);
	}
  • 大小: 58.1 KB
3
2
分享到:
评论

相关推荐

    SpringBoot中的Profile配置的使用示例源码

    在Spring Boot中,Profile配置是一项强大的特性,它允许我们在不同的环境下使用不同的配置,例如开发、测试和生产环境。本文将深入探讨Spring Boot中Profile的使用,并通过源码示例进行详细解析。 首先,理解...

    我的srping 配置

    **Spring Profiles**:在不同环境下(如开发、测试、生产)使用不同配置时,Spring的Profiles功能非常有用。通过设置环境变量或配置文件,可以轻松切换不同环境的配置。 **AOP(面向切面编程)**:Spring的另一个...

    Spring配置文件

    7. ** Profiles **:Spring支持多环境配置,通过`@Profile`注解可以在特定环境下激活特定的Bean定义。这在开发、测试和生产环境间切换时非常有用。 8. ** 配置元数据合并 **:在大型项目中,配置通常分布在多个文件...

    【翻译】spring配置全书(下)——附PDF完整版下载

    5. **Spring Profiles**:用于在不同环境(如开发、测试、生产)间切换配置,`@Profile`注解可用于标记特定环境下生效的bean。 6. **Spring Cloud Config**:当应用变得庞大,配置管理变得复杂时,Spring Cloud ...

    SpringProfile:显示多个配置文件具有相同bean类型时自动装配的spring bean分辨率

    这个特性在处理多环境部署(如开发、测试、生产等)时尤其有用,可以确保每个环境中bean的配置符合特定环境的需求。下面将详细解释`SpringProfile`的工作原理以及如何在实际应用中使用它。 `@Profile`注解是Spring ...

    SpringBoot 47道面试题和答案.docx

    5. **环境感知配置**:通过-Dspring.profiles.active参数,可以方便地切换不同环境的配置文件,适应开发、测试和生产环境。 6. **DevTools支持热部署**:SpringBoot DevTools允许开发者在代码修改后自动重启应用,...

    springworkroot测试

    Spring Profiles允许开发者为不同的环境(如开发、测试、生产)定义不同的配置,这样就可以轻松地在不同环境中切换。 基于描述中的“测试”,我们可以推断这个“springworkroot”可能包含了一个或多个Spring项目的...

    SpringBoot面试专题及答案

    5. **环境配置**:通过-Dspring.profiles.active参数,可以方便地切换不同环境的配置文件,适应开发、测试和生产环境的变化。 Spring Boot DevTools(开发工具)是提升开发效率的一大利器,它允许开发者在保存代码...

    SpringBoot 38道面试题和答案.docx

    7. 支持基于环境的配置,通过 -Dspring.profiles.active 参数切换环境。 8. 提供 Spring Boot DevTools,实现热部署,无需重启服务器即可应用代码变更。 JavaConfig 是一种纯 Java 的 Spring 容器配置方式,其优势...

    springboot面试题及答案.docx

    - **环境配置**:支持基于环境的配置,如-Dspring.profiles.active={environment},便于在不同环境下切换配置。 2. **JavaConfig** 是Spring社区推出的一种纯Java方式的配置,它消除了对XML配置文件的依赖。Java...

    SpringBoot 35道面试题和答案.docx

    7. 支持基于环境的配置,通过 -Dspring.profiles.active={environment} 参数切换环境配置。 JavaConfig 是 Spring 社区推出的一种纯 Java 的配置方式,它可以替代传统的 XML 配置。JavaConfig 的优势在于: 1. 实现...

    spring-boot-profiles-example:Spring Boot Profiles示例

    在实际开发中,我们经常需要根据不同的部署环境(如开发、测试、生产)来调整应用的配置。Spring Boot通过profiles特性轻松解决了这个问题。 1. **Spring Boot配置文件** Spring Boot允许我们在`src/main/...

    Spring Boot面试专题.pdf

    5. **环境感知配置**:通过 `spring.profiles.active` 属性,可以轻松切换不同环境下的配置,如开发、测试和生产环境。 6. **自动重启**:Spring Boot 提供的 DevTools 模块支持热部署,修改代码后无需手动重启...

    test-configuration.rar

    可以创建不同环境的配置文件,如`application-dev.properties`(开发环境)、`application-test.properties`(测试环境)等,实现环境间的配置切换。在`test-configuration`中,可能会有针对不同环境的配置策略。 5...

    有关springboot的相关问题.docx

    6. 更少的配置需求:通过环境变量管理配置,如`spring.profiles.active`,实现不同环境的快速切换。 7. 自动化依赖注入:使用`@Autowired`注解,Spring Boot 可以自动管理bean的依赖关系。 8. 开发者工具(DevTools...

    spring3.1-framework-reference

    - **BeanDefinition Profiles**:允许在不同的环境或配置下启用或禁用特定的Bean定义。 - **环境抽象**:支持多环境配置,使应用能够在不同的部署环境中轻松切换配置。 - **属性源抽象**:提供了灵活的方式来读取和...

    spring profile 多环境配置管理详解

    Spring Profile 是 Spring 框架中的一个重要特性,它允许开发者为不同的环境(如开发、测试、生产等)创建和管理独立的配置。在多环境配置管理中,Spring Profile 提供了方便的方式来切换不同环境下的配置,确保每个...

Global site tag (gtag.js) - Google Analytics