Spring Framework 3.1 M1 发布

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

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

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

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

动手操作示例

我开发了一个小型示例来配合这篇文章,您可能现在想花点时间查看一下(如果不是,别担心;您不需要代码就能继续阅读)。只需按照 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 容器,但首先请注意我们的目标很简单——从账户“A123”向账户“C456”转账 10.00 美元。单元测试只是断言这两个账户的初始余额,执行转账,然后断言最终余额反映了变化。

典型的 XML 配置

bean 定义配置文件功能在 Spring XML 中和在使用 Spring @Configuration 类配置容器时都得到同等良好的支持,但在今天的文章中,我们将介绍 XML 方法,因为大多数用户都熟悉它。

暂时忘记 bean 定义配置文件,要使用 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} 令牌,这些令牌根据环境变量的值解析为正确的配置文件路径。虽然这些和其他解决方案可以正常工作,但我们很难称之为容器提供的“一流”解决方案。

引入 bean 定义配置文件

如果我们概括上面环境特定 bean 定义的示例用例,最终需要在某些上下文中注册某些 bean 定义,而在其他上下文中则不注册。您可以说您希望在情况 A 中注册某些 bean 定义的配置文件,而在情况 B 中注册不同的配置文件。

在 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 定义配置文件——“dev”和“production”,但我们尚未激活其中任何一个。

引入 Environment

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

激活配置文件可以通过多种方式完成,但最直接的方法是通过编程方式针对 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”配置文件当前处于活动状态,因此对其进行解析
  • jndi-datasource-config.xml 指定 profile="production",但“production”配置文件当前未处于活动状态,因此跳过它。

结果是正好注册了一个名为“dataSource”的 bean,它满足“accountRepository”bean 的依赖注入需求。一切再次正常工作。

那么如何在实际生产中切换到 JNDI 查找呢?当然,有必要激活“production”配置文件。出于单元测试的目的,通过编程方式执行此操作是可以的,但一旦 WAR 文件创建完毕且应用程序准备就绪以进行部署,此方法将不切实际。出于这个原因,配置文件也可以通过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>

请注意,配置文件不是“非此即彼”的命题;可以同时激活多个配置文件。通过编程方式,只需向 setActiveProfiles() 方法提供多个配置文件名称即可,该方法接受 String... 可变参数


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

声明方式,spring.profiles.active 可以接受以逗号分隔的配置文件名称列表

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

bean 定义文件可以类似地标记为多个配置文件的候选文件


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

这允许采用灵活的方法来分解应用程序,切分和切块在哪些情况下注册哪些 bean。

使其更简单:引入嵌套的 <beans/> 元素

到目前为止,bean 定义配置文件已为我们提供了一种方便的机制来确定根据应用程序的部署上下文注册哪些 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 定义配置文件的服务中开发的,但嵌套的 <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 定义配置文件时,有一些需要注意的事项。

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

在两个配置文件之间注册的 Bean 集合应该更相似而不是差异很大。 例如,如果您在开发和 QA 环境中的 Bean 集合与生产环境中的 Bean 集合有根本的不同,那么问题就变成了:您是否正在测试所有应该测试的内容? 作为经验法则,Bean 之间的差异不应超出开发/QA 分割。一个例外可能是条件地在性能环境中引入监控方面。

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

摘要

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

在本系列的下一篇文章中,我们将了解如何在使用 @Configuration 类而不是 XML 配置容器时使用 Bean 定义配置文件;新的 @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 社区中所有即将举行的活动。

查看全部