抢先一步
VMware 提供培训和认证,助你快速提升。
了解更多在我之前的文章中,我宣布了 Spring 3.1 M1 的发布,并讨论了使用 Spring <beans/>
XML 配置容器时应用的新特性:bean 定义 profile。今天,我们将介绍新的 @Profile
注解,并看看如何在不使用 XML 的情况下,改用 @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 测试用例都已复制;这应该有助于你比较和对比两种启动容器的方式。
@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 的强大功能。
如果我们考虑 StandaloneDataConfig
和 JndiDataConfig
,它们实际上是同一类型的两个类,因为它们都声明了一个具有以下签名的方法:
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 以获取“快速层次结构”悬停的结果:
现在很容易提出问题:“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
抽象,以及它如何帮助管理应用程序中的配置属性。敬请关注!