Spring 3.1 M1:引入 @Profile

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

引言

在我之前的文章中,我宣布了 Spring 3.1 M1 的发布,并讨论了使用 Spring <beans/> XML 配置容器时应用的新特性:bean 定义 profile。今天,我们将介绍新的 @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.xml 和 com.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 的 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 和 standalone @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 包,一次性检测所有类。这与基于通配符加载 Spring 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 类,此时我们的任务都已完成!我们已将配置从 Spring <beans/> XML 移植到 @Configuration 类,并使用 AnnotationConfigApplicationContext 直接从这些类启动了容器。

进一步改进 @Configuration 类结构

我们的应用程序一切正常,JUnit 条也是绿色的,但仍有改进的空间。回想一下 DataSource bean 如何通过 @Autowired 注入到 TransferServiceConfig 中?这工作得很好,但并不完全清楚这个 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 直接 @Autowired 注入到 TransferServiceConfig 中一样,我们也可以注入 @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 定义 profile 特性在 XML 和 @Configuration 风格中都得到全面支持。无论你喜欢哪种风格,我们都希望你会发现 profile 有用。请持续提供反馈,因为它将直接影响即将发布的 3.1 M2。在下一篇文章中,我们将更深入地研究 Spring 的新Environment 抽象,以及它如何帮助管理应用程序中的配置属性。敬请关注!

获取 Spring 新闻通讯

订阅 Spring 新闻通讯保持联系

订阅

抢先一步

VMware 提供培训和认证,助你快速提升。

了解更多

获取支持

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

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部