Spring Framework 3.1 M1 发布

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

Spring 3.1 的第一个里程碑版本刚刚发布 [1],本文将开启一系列文章,我和其他团队成员将逐一讲解每个主要特性。即使在第一个里程碑版本中,已经有很多值得讨论的内容!

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

今天我将介绍第一项——我们称之为bean definition profiles 的新特性。我们最频繁的需求之一是提供一个核心容器机制,允许在不同的环境中注册不同的 bean。“环境”对于不同的用户可能意味着不同的事物,但典型的场景可能是在部署应用程序到性能环境时仅注册监控基础设施,或者为客户 A 与客户 B 的部署注册定制的 bean 实现。也许最常见的用例之一是在开发环境中使用独立的 datasource,而在 QA 或生产环境中从 JNDI 查找相同的 datasource。Bean definition profiles 提供了一种通用的方式来满足这类用例,我们将在下面的示例中探讨后一种用例。

动手尝试示例

我为本文开发了一个小示例,你可能想花点时间看看它(如果不看也没关系;阅读下面的内容不需要代码)。只需按照 README 的说明操作即可,地址是 https://github.com/cbeams/spring-3.1-profiles-xml。如果你不熟悉 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 容器,但首先请注意,我们的目标很简单——从账户“A123”向账户“C456”转账 10.00 美元。单元测试简单地断言了两个账户的初始余额,执行转账,然后断言最终余额反映了变化。

典型的 XML 配置

在 Spring XML 中,bean definition profiles 特性得到了很好的支持,就像在使用 Spring @Configuration 类配置容器时一样好,但在今天的文章中,我们将介绍 XML 方法,因为这是大多数用户所熟悉的。

暂时不考虑 bean definition profiles,要在 Spring XML 中配置此应用程序,传统上会执行如下代码片段。我们假设正处于开发的早期阶段,并且首选使用独立的 datasource。为了方便起见,我们将使用 HSQLDB,但你当然可以想象配置你选择的 datasource。

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 中。应用程序的 datasource 极有可能在生产应用程序服务器的 JNDI 目录中注册。这意味着为了获取 datasource,我们必须执行 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} 标记,这些标记根据环境变量的值解析到正确的配置文件路径。虽然这些以及其他解决方案都可以奏效,但它们远非我们所谓的由容器提供的“一流”解决方案。

引入 bean definition 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 definition profiles——“dev”和“production”,但我们还没有激活其中任何一个。

引入 Environment

3.1 版本的新特性是 Spring 的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 文件创建并准备部署时,这种方法就不切实际了。因此,也可以通过 spring.profiles.active 属性声明式地激活 profile,此属性可以通过系统环境变量、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>

请注意,profile 不是“非此即彼”的建议;可以同时激活多个 profile。通过编程方式,只需向 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 definition 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 definition profiles 开发的,但嵌套的 <beans/> 元素通常很有用。想象一下,给定文件中有一部分 bean 应该标记为 lazy-init="true"。你可以声明一个嵌套的 <beans default-lazy-init="true"/> 元素,而不是标记每个 bean,其中的所有 bean 都将继承该默认值。文件中其他地方定义的 bean 将保持 lazy-init="false" 的正常默认值。这适用于 <beans/> 元素的所有 default-* 属性,例如 default-lazy-initdefault-init-methoddefault-destroy-method 等。

注意事项

考虑使用 bean definition profiles 时,有几点需要注意。

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

两个 profile 之间注册的 bean 集应该更相似而不是更不同。例如,如果你的开发和 QA 环境中的 bean 与生产环境中的 bean 有很大差异,问题就变成了:你是否测试了所有应该测试的内容?通常,bean 在开发/QA 分割之外不应有太大差异。例外情况可能是在性能环境中条件性地引入监控切面。

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

总结

目前就到这里;我鼓励你查看示例应用程序并动手尝试。例如,尝试按原样运行单元测试,然后尝试将 spring.profiles.active 系统属性设置为“production”并查看会发生什么。

本系列的下一篇文章中,我们将探讨在使用 @Configuration 类而不是 XML 配置容器时如何使用 bean definition profiles;新的 @Profile 注解将在这里提供帮助。

脚注

[1] 里程碑构建版本发布到 http://maven.springframework.org/milestone。有关如何从这个仓库拉取的详细信息,请参阅示例中的 pom.xmlbuild.gradle 文件。

[2] 技术上,spring.profiles.active 可以指定在注册到 ApplicationContextEnvironment 的任何 PropertySource 对象中。我们将在后续的博客文章中介绍属性源的概念。

获取 Spring 新闻通讯

订阅 Spring 新闻通讯以保持联系

订阅

抢占先机

VMware 提供培训和认证,助力你飞速发展。

了解更多

获得支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举办的活动

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

查看全部