Spring Framework 3.1 M1 已发布

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

Spring 3.1 的第一个里程碑版本刚刚发布[1],本文将启动一系列帖子,我将和其他团队成员一起介绍每个主要功能。即使在第一个里程碑版本中,也有很多内容值得讨论!

  • Bean 定义配置文件
  • 通过 Spring 新的 Environment 抽象统一属性管理
  • 增强基于 Java 的配置,使用 @Feature 方法
  • 扩展的 MVC 命名空间支持和基于 Java 的配置等效项
  • RestTemplate API 的流式支持和新的拦截模型
  • 全面的缓存支持
  • 新的 c: XML 命名空间,用于简洁地配置构造函数注入

今天我将介绍第一项——我们称之为*bean 定义 profiles* 的新功能。我们最常收到的请求之一是提供一个核心容器机制,允许在不同环境中注册不同的 bean。“环境”一词对不同的用户可能意味着不同的东西,但一个典型的场景可能是仅在将应用程序部署到性能环境时注册监控基础结构,或者为客户 A 与客户 B 的部署注册自定义 bean 实现。也许最常见的情况是在开发中针对独立数据源,而在 QA 或生产环境中从 JNDI 中查找相同的数据源。Bean 定义 profiles 代表了一种通用方法来满足此类用例,我们将在下面的示例中探讨后者用例。

通过示例动手实践

我开发了一个小的示例来配合这篇博文,您不妨现在花点时间来看看它(如果不看也没关系;您不需要代码也能继续阅读下面的内容)。只需按照 https://github.com/cbeams/spring-3.1-profiles-xml 的 README 中的说明操作即可。如果您不熟悉 Git,README 中也有 SVN 访问的说明。

理解应用程序

首先,让我们来看一个 JUnit 测试用例,它演示了在我们银行应用程序中,两个账户之间的转账应该如何工作。

src/test/com/bank/config/xml/IntegrationTests.java


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

我们稍后会讨论创建 Spring 容器的细节,但首先请注意,我们的目标很简单——将 10.00 美元从账户“A123”转到账户“C456”。单元测试只是断言两个账户的初始余额,执行转账,然后断言最终余额反映了这种变化。

典型的 XML 配置

Bean 定义 profiles 功能在 Spring XML 和使用 Spring @Configuration 类配置容器时都得到了同等程度的支持,但今天的博文将涵盖 XML 方法,因为这是大多数用户都熟悉的方式。

暂时忘掉 Bean 定义 profiles,在 Spring XML 中配置此应用程序,人们通常会这样做,如下面的代码片段所示。让我们假设我们正处于开发过程的早期,并且倾向于使用独立的数据源。为了方便起见,我们将使用 HSQLDB,当然,您也可以想象使用您选择的任何数据源进行配置。

src/main/com/bank/config/xml/transfer-service-config.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>

上面唯一可能不熟悉的项目可能是使用了 Spring 的 jdbc: 命名空间。它是在 Spring 3.0 中引入的,其中的元素允许轻松配置常用嵌入式数据库类型。这里只是为了方便使用。

考虑到此配置,我们现在可以完成上面开始编写的单元测试。

src/test/com/bank/config/xml/IntegrationTests.java


public class IntegrationTests {
	@Test
	public void transferTenDollars() throws InsufficientFundsException {

		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
		ctx.load("classpath:/com/bank/config/xml/transfer-service-config.xml");
		ctx.refresh();

		TransferService transferService = ctx.getBean(TransferService.class);
		AccountRepository accountRepository = ctx.getBean(AccountRepository.class);

		// perform transfer and issue assertions as above ...
	}
}

旁注:我们使用 Spring 的 GenericXmlApplicationContext 应用程序上下文来加载 XML 配置。许多 Spring 用户会更熟悉 ClassPathXmlApplicationContext,它同样有效。然而,总的来说,GenericXmlApplicationContext 是一个更灵活的选择,应该被普遍推荐。

运行此测试时,进度条将是绿色的。我们的简单应用程序由容器连接,我们检索并使用了其中的一些 bean——这里没什么特别的。当考虑到此应用程序将如何部署到 QA 或生产环境时,事情就变得有趣了。例如,使用 Spring 的企业为了易用性而针对 Tomcat 开发 Web 应用程序,然后在生产环境中将这些应用程序部署到 WebSphere,这是一个常见场景。应用程序的数据源极有可能已注册到生产应用程序服务器的 JNDI 目录中。这意味着,为了获取数据源,我们必须执行 JNDI 查找。当然,Spring 为此提供了强大的支持,一种流行的方法是通过 Spring 的 <jee:jndi-lookup/> 元素。上面配置文件的生产版本可能看起来像这样

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" ... />

	<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

当然,这种配置工作得非常好。问题是如何根据当前环境在这两种变体之间切换。随着时间的推移,Spring 用户已经想出了许多方法来实现这一点,通常依赖于系统环境变量和 XML <import/> 语句的组合,这些语句包含 ${placeholder} 令牌,根据环境变量的值解析到正确的配置文件路径。虽然这些和其他解决方案可以奏效,但它们 hardly 称得上是我们所说的容器提供的“一流”解决方案。

引入 Bean 定义 profiles

如果我们泛化上面关于环境特定 Bean 定义的用例,我们会发现需要在某些上下文中注册某些 Bean 定义,而在其他上下文中不注册。你可以说你希望在情况 A 中注册一组 Bean 定义的特定 *profile*,而在情况 B 中注册另一组不同的 profile。

在 Spring 3.1 中,<beans/> XML 文档现在包含了这个新概念。我们可以将配置分解为以下三个文件。请注意 *-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();

但这还不够。在这些更改后运行单元测试时,我们会看到一个 NoSuchBeanDefinitionException 被抛出,因为容器找不到名为“dataSource”的 Spring bean。原因在于,虽然我们明确定义了两个 Bean 定义 profile——“dev”和“production”,但我们还没有 *激活* 它们中的任何一个。

引入 Environment

Spring 3.1 新增了一个Environment 的概念。这个抽象已经集成到容器的各个部分,我们在接下来的几天博客文章中会多次看到它。对于我们在这里的目的,重要的是要理解 Environment 包含有关当前活动 profile(如果有)的信息。当上面的 Spring ApplicationContext 加载我们的三个 Bean 定义文件时,它会密切关注它们中的 <beans profile="..."> 属性。如果存在并且设置为当前未激活的 profile 名称,则整个文件将被跳过——不会解析或注册任何 Bean 定义。

激活 profile 有多种方法,但最直接的方法是通过 ApplicationContext API 以编程方式进行。


	GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
	ctx.getEnvironment().setActiveProfiles("dev");
	ctx.load("classpath:/com/bank/config/xml/*-config.xml");
	ctx.refresh();

此时,运行我们的单元测试将得到绿色的进度条。让我们分析一下容器在加载与 *-config.xml 匹配的三个文件时的思考过程。

  • transfer-service-config.xml 完全没有指定 profile 属性,因此它始终被解析。
  • standalone-datasource-config.xml 指定了 profile="dev" 且“dev” profile 当前已激活,因此被解析。
  • jndi-datasource-config.xml 指定了 profile="production" 但“production” profile 当前未激活,因此被跳过。

结果是只注册了一个名为“dataSource”的 bean,它满足了“accountRepository” bean 的依赖注入需求。一切又恢复正常了。

那么,在实际生产中如何切换到 JNDI 查找呢?当然,有必要激活“production” profile。对于单元测试而言,以编程方式进行是可以的,如上所示,但一旦 WAR 文件被创建并且应用程序准备好部署,这种方法就不切实际了。因此,profiles 也可以通过spring.profiles.active 属性*声明性地*激活,该属性可以通过系统环境变量、JVM 系统属性、web.xml 中的 servlet 上下文参数,甚至 JNDI 条目 [2] 来指定。例如,您可以如下配置 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 不是“非此即彼”的选项;可以同时激活多个 profiles。以编程方式,只需向 setActiveProfiles() 方法提供多个 profile 名称,该方法接受 String... 可变参数。


	ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

声明性地,spring.profiles.active 可以接受一个逗号分隔的 profile 名称列表。

	-Dspring.profiles.active="profile1,profile2"

Bean 定义文件可以以类似的方式标记为多个 profile 的候选。


	<beans profile="profile1,profile2">
		...
	</beans>

这提供了一种灵活的方法来分解您的应用程序,根据不同的情况选择注册哪些 bean。

让它更简单:引入嵌套的 <beans/> 元素

到目前为止,Bean 定义 profiles 为我们提供了一种方便的机制来确定哪些 bean 根据应用程序的部署上下文被注册,但它也有一个缺点:上面我们有一个 Spring XML 配置文件,现在我们有了三个。这种分割是必要的,以便区分 profile="dev"profile="production" 的 bean,因为 profile 属性是在 <beans> 元素级别指定的。

使用 Spring 3.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>

spring-beans-3.1.xsd 已更新以允许这种嵌套,但仅限于允许这些元素作为文件中的最后一个元素。这应该有助于提供灵活性,而不会增加 XML 文件的混乱。虽然此增强是为了服务于 Bean 定义 profiles 而开发的,但嵌套的 <beans/> 元素通常很有用。想象一下,您有一个文件中的一组 bean 应该被标记为 lazy-init="true"。您不必标记每个 bean,而是可以声明一个嵌套的 <beans default-lazy-init="true"/> 元素,其中所有 bean 都将继承该默认值。文件中定义的 bean 将保持正常的 lazy-init="false" 默认值。这适用于 <beans/> 元素的所有 default-* 属性,例如 default-lazy-initdefault-init-methoddefault-destroy-method 等。

注意事项

在考虑使用 Bean 定义 profiles 时,有几件事需要注意。

如果更简单的方法就能完成工作,请不要使用 profiles。 如果 profiles 之间唯一改变的是属性值,Spring 现有的 PropertyPlaceholderConfigurer / <context:property-placeholder/> 可能就是您所需要的。

两个 profile 之间注册的 bean 集合可能应该是相似的,而不是不同的。 例如,如果您在 dev 和 QA 中拥有一套与生产环境截然不同的 bean,那么问题就变成了:*您是否测试了所有应该测试的内容?*。作为经验法则,bean 的差异不应超过 dev/QA 的划分。一个例外可能是有条件地在性能环境中引入监控方面。

注意不要将“过多的东西”打包到生产环境中。 如果在开发过程中存在某些 bean 和类库,但在生产环境中是不必要或不想要的,那么您就有可能将所有这些打包并部署到生产环境中。这是浪费(为什么要把所有东西都拖到 WAR 中,如果它不需要的话),而且也可能不安全。请记住,profiles 的激活方式是通过属性。例如,如果您的完全不安全的“无操作密码加密器” bean 定义和类都存在于生产环境中,而要启用它只需要意外激活“dev” profile,那么危险就显而易见了。有几种方法可以缓解此风险:自定义您的构建系统,将不必要的或不需要的类从生产部署归档中排除,或者使用原生的 Java SecurityManager API 来阻止访问 spring.profiles.active 系统环境变量和/或 JVM 系统属性。这样做意味着,即使 Spring 尝试读取这些值,它也无法读取,然后会像它们从未被设置过一样继续。

总结

以上就是本期内容;我鼓励您查看示例应用程序并进行尝试。例如,尝试按原样运行单元测试,然后尝试将 spring.profiles.active 系统属性设置为“production”并观察会发生什么。

在下一篇系列博文中,我们将探讨如何在不使用 XML 而使用 @Configuration 类配置容器时使用 Bean 定义 profiles;新的 @Profile 注解将在此处帮助我们。

脚注

[1] Milestone 构建已发布到 http://maven.springframework.org/milestone。有关如何从该存储库拉取的详细信息,请参阅示例中的 pom.xmlbuild.gradle 文件。

[2] 严格来说,spring.profiles.active 可以在注册到 ApplicationContextEnvironment 的任何 PropertySource 对象中指定。我们将在后续的博客文章中讨论属性源的概念。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

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

了解更多

即将举行的活动

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

查看所有