Spring 3.1 M1: 引入 @Profile

工程 | Chris Beams | 2011 年 2 月 14 日 | ...

介绍

在我 之前关于 Spring 3.1 M1 的发布公告的帖子 中,我讨论了在使用 Spring <beans/> XML 配置容器时应用的新*bean 定义 profiles* 功能。今天我们将介绍新的 @Profile 注解,并了解如何在不使用 XML 而使用 @Configuration 类时应用此相同功能。在此过程中,我们将涵盖一些设计 @Configuration 类的最佳实践。

回顾 @Configuration

对于不熟悉 @Configuration 类的读者,您可以将其视为 Spring <beans/> XML 文件的纯 Java 等效项。我们之前已经在 博客 讨论过 这个特性集,并且参考文档 对此进行了很好的阐述。如果您需要介绍或复习,可以重新阅读这些资源。

正如我们将在本文及后续文章中看到的,Spring 3.1 中的 @Configuration 方法得到了极大的关注,目的是使其更加完善,并成为那些希望不使用 XML 配置应用程序的用户的首选方案。今天的文章将介绍其中一项增强功能:新的 @Profile 注解。

与上一篇文章一样,我准备了一个简短的示例,您可以按照示例进行操作并自己尝试。您可以在 https://github.com/cbeams/spring-3.1-profiles-java 找到它,所有设置细节都在 README 文件中。此示例包含上一篇文章中介绍的基于 XML 的配置,以及 @Configuration 类,分别位于 com.bank.config.xmlcom.bank.config.code 包中。IntegrationTests JUnit 测试用例已针对每个包进行了复制;这应该有助于您比较和对比引导容器的两种样式。

从 XML 到 @Configuration

让我们开始吧!我们的任务很简单:将之前显示的基于 XML 的应用程序移植到 @Configuration 风格。我们在上一篇文章中从如下的 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>

将其移植到 @Configuration 类非常直接

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 是最初在 XML 中使用的 <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,这很好,但此时我们的应用程序存在与第一篇文章中相同的问题:当应用程序部署到生产环境时,独立的 DataSource 将不适用。需要从 JNDI 中查找。

这不成问题。让我们将嵌入式和 JNDI-based DataSource 分别提取到它们各自专用的 @Configuration 类中

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");
	}

}

此时,我们已在各自带有 @Profile 注解的 @Configuration 类中声明了两个不同的 DataSource bean。与 XML 一样,这些类以及其中的 @Bean 方法将根据当前活动的 Spring profile 被跳过或处理。但是,在我们看到这一点之前,我们首先需要完成重构。我们已经分离了两个可能的 DataSource bean,但在 TransferServiceConfig 中——特别是它的 accountRepository() 方法——如何引用它们呢?我们有几个选项,两者都始于理解 @Configuration 类是 @Autowired 注入的候选对象。这是因为,归根结底,@Configuration 对象在容器中被管理为“另一个 Spring bean”。我们来看一下

src/main/com/bank/config/code/TransferServiceConfig.java


@Configuration
public class TransferServiceConfig {

	@Autowired DataSource dataSource;

	@Bean
	public TransferService transferService() {
		return new DefaultTransferService(accountRepository(), feePolicy());
	}

	@Bean
	public AccountRepository accountRepository() {
		return new JdbcAccountRepository(dataSource);
	}

	@Bean
	public FeePolicy feePolicy() {
		return new ZeroFeePolicy();
	}

}

通过使用上面的 @Autowired 注解,我们要求 Spring 容器为我们注入类型为 DataSource 的 bean,无论它在哪里声明——在 XML 中、在 @Configuration 类中,还是其他地方。然后在 accountRepository() 方法中,直接引用注入的 dataSource 字段。这是实现 @Configuration 类之间模块化的一种方式,并且在概念上与声明在不同 XML 文件中的两个 <bean> 元素之间的 ref 风格引用不无相似之处。

重构的最后一步是更新单元测试,不仅要引导 TransferServiceConfig,还要引导我们 DataSource bean 的 JNDI 和独立 @Configuration 变体

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 ...
	}
}

现在,我们所有的 @Configuration 类在引导时都可以被容器访问,并且根据活动的 profile(在本例中为“dev”),@Profile 注解的类及其 bean 将被处理或跳过。顺便提一下,您可以避免在上面列出每个 @Configuration 类,而是告诉 AnnotationConfigApplicationContext 直接扫描整个 .config 包,一次性检测到所有类。这相当于使用通配符(例如 **/*-config.xml)加载 Spring 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 类,此时我们的任务就完成了!我们将配置从 Spring <beans/> XML 移植到了 @Configuration 类,并使用 AnnotationConfigApplicationContext 直接从这些类引导了容器。

进一步改进 @Configuration 类结构

我们的应用程序一切正常,JUnit 条也显示绿色,但仍有改进的空间。还记得 DataSource bean 是如何被 @AutowiredTransferServiceConfig 中的吗?这很有效,但并不清楚 bean *是从哪里*来的。如上所述,它可能来自 XML,也可能来自任何其他 @Configuration 类。我将在下面描述的技术引入了*面向对象配置*,并应有助于实现我们拥有自然 Java 配置的目标——一种可以充分利用 IDE 功能的配置。

如果我们考虑 StandaloneDataConfigJndiDataConfig,它们实际上是同一种类的两种实现,因为它们都声明了一个具有以下签名的方**法**:


		public DataSource dataSource();

似乎唯一缺少的是一个统一这两个类的接口。让我们引入一个——我们稍后会看到原因

src/main/com/bank/config/code/DataConfig.java


interface DataConfig {
	DataSource dataSource();
}

当然,更新两个 @Configuration 类以实现这个新接口


@Configuration
public class StandaloneDataConfig implements DataConfig { ... }

@Configuration
public class JndiDataConfig implements DataConfig { ... }

这给我们带来了什么?就像我们将 DataSource bean 直接 @AutowiredTransferServiceConfig 中一样,*我们还可以注入 @Configuration 实例本身*。让我们看看实际效果

src/main/com/bank/config/code/TransferServiceConfig.java


@Configuration
public class TransferServiceConfig {

	@Autowired DataConfig dataConfig;

	// ...

	@Bean
	public AccountRepository accountRepository() {
		return new JdbcAccountRepository(dataConfig.dataSource());
	}

	// ...
}

这使我们能够通过 IDE 在代码库中进行完全导航。下面的截图显示了在调用 dataConfig.dataSource() 时按 CTRL-T 来获得“快速层次结构”悬停的效果

Quick implementation hierarchy for DataConfig.dataSource()

现在,我们可以很容易地提出“DataSource bean 是在哪里定义的?”这个问题,并且答案会限制在一组实现 DataConfig 的类型中。如果我们试图以一种对 Java 开发人员熟悉且有用的方式做事,这并不坏。

@Profile 的更高级用法

值得快速提及的是,与许多 Spring 注解一样,@Profile 可以用作*元注解*。这意味着您可以定义自己的自定义注解,并用 @Profile 标记它们,Spring 仍然会检测到 @Profile 注解的存在,就好像它直接声明了一样。


package com.bank.annotation;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("dev")
pubilc @interface Dev {
}

这允许我们用新的自定义注解 @Dev 标记我们的 @Component 类,而不是强制使用 Spring 的 @Profile


@Dev @Component
public class MyDevService { ... }

或者,从上面的示例中,用 @Dev 标记我们的 StandaloneDataConfig 也可以


@Dev @Configuration
public class StandaloneDataConfig { ... }

总结

Spring 3.1 的 bean 定义 profiles 功能在 XML 和 @Configuration 样式中都得到了完全支持。无论您偏好哪种样式,希望您会发现 profiles 有用。请继续提供反馈,因为它将直接影响即将发布的 3.1 M2。在下一篇文章中,我们将深入探讨 Spring 的新*Environment*抽象以及它如何帮助管理应用程序中的配置属性。敬请期待!

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有