在无完整 Java EE 环境下配置 Spring 和 JTA

工程 | Josh Long | 2011年8月15日 | ...

Spring 通过其PlatformTransactionManager接口及其实现层次结构,为事务管理提供了丰富的支持。Spring 的事务支持为众多 API 的事务语义提供了一致的接口。广义上,事务可以分为两类:本地事务和全局事务。本地事务仅影响一个事务资源。大多数情况下,这些资源都有其自身的事务 API,即使事务的概念没有明确地体现出来。通常它以会话的概念体现出来,这是一个工作单元,具有分界 API 来告诉资源何时应该提交缓冲的工作。全局事务是指跨越一个或多个事务资源的事务,并将它们都加入到单个事务中。

JMS 和 JDBC API 中的一些常见本地事务示例。在 JMS 中,用户可以创建一个事务性会话,发送和接收消息,并在消息处理完成后,调用Session.commit()来告诉服务器可以完成工作。在数据库领域,JDBC Connection默认情况下会自动提交查询。对于一次性语句来说,这很好,但通常最好将几个相关的语句收集到一个批处理中,然后提交所有语句或不提交任何语句。在 JDBC 中,您可以通过首先将ConnectionsetAutoCommit()方法设置为 false,然后在批处理结束时显式调用Connection.commit()来实现这一点。这两个 API 以及许多其他 API 都提供了事务工作单元的概念,可以根据客户端的意愿提交、完成、刷新或以其他方式使其永久化。这些 API 大相径庭,但概念相同。

全局事务则完全不同。如果您想让多个资源参与事务,则应随时使用它们。这有时是必需的:也许您想发送 JMS 消息并写入数据库?或者,也许您想使用 JPA 的两个不同的持久上下文?在全局事务设置中,第三方事务监视器将多个事务资源加入到事务中,为提交做好准备——在此阶段,资源通常执行相当于预提交的操作——然后最终提交每个资源。这些步骤是大多数全局事务实现的基础,称为两阶段提交 (2PC)。如果一个提交失败(由于在为提交做好准备时不存在的原因,例如网络中断),则事务监视器会要求每个资源撤消或回滚上次事务。

全局事务明显比常规本地事务更复杂,因为它们必须根据定义包含一个第三方代理,其唯一功能是在多个事务资源之间仲裁事务状态。事务监视器还必须知道如何与每个事务资源通信。由于此代理还必须保持状态——毕竟,不能信任各个事务资源知道其他资源正在做什么——它具有持久性要求和同步成本以保持一致的事务日志,非常类似于数据库。当发生灾难性事件并且事务监视器关闭时,它必须能够启动并重播进行中的事务,以确保所有资源在任何中断事务方面都处于一致状态。为了与事务资源通信,事务监视器必须使用与事务资源的通用协议进行通信。通常,此协议称为 XA。

在企业 Java 世界中,向应用程序添加 XA 的典型方法是使用 JTA。Java 事务 API (JTA) 是一个规范,它描述了用户的标准全局事务监视器 API。您可以使用 JTA API 以标准方式处理支持它的资源类型的全局事务——通常只是 JDBC 和 JMS。Java EE 应用程序服务器开箱即用地支持 JTA,并且您可以使用 JTA 的第三方独立实现来避免被困在 Java EE 应用程序服务器上。

要使用 JTA,您需要做出非常具体的决定才能在您的事务代码中使用它,因为该 API 与 JDBC 公开的事务 API 大相径庭,而 JDBC 的 API 又与 JMS 的 API 大相径庭。

经常会看到针对 JTA API 编写的旧代码,即使只涉及一个资源,因为至少它不必完全重写就能利用 XA(如果稍后需要)。这通常是一个令人遗憾但可以理解的决定。以这种方式编写的代码不幸的是也常常与容器绑定;除非 JNDI 和所有服务器机制正在运行以引导事务监视器和 JNDI 服务器,否则它将无法工作。

幸运的是,可以使用 Spring 优雅地避免这种复杂性。

Spring 的事务支持

解决这种糟糕现状的第一步是 Spring PlatformTransactionManager API 和 Spring 框架中相关的事务管理 API。Spring 框架和周围的项目提供了丰富的PlatformTransactionManager实现选择,支持 JDBC、JMS、Gemfire、AMQP、Hibernate、JPA、JDO、iBatis 等中的本地事务。
Spring Transaction Class Hierarchy

Spring 框架还提供TransactionTemplate,它与PlatformTransactionManager实现一起工作,可用于自动将工作单元包含在事务中,因此您甚至不需要了解PlatformTransactionManager API 本身,并且PlatformTransactionManager的使用契约得到满足:事务将被正确启动、执行、准备和提交,并且——如果在处理过程中抛出异常,事务将回滚。


@Inject private PlatformTransactionManager txManager; 

TransactionTemplate template  = new TransactionTemplate(this.txManager); 
template.execute( new TransactionCallback<Object>(){ 
  public void doInTransaction(TransactionStatus status){ 
   // work done here will be wrapped by a transaction and committed. 
   // the transaction will be rolled back if 
   // status.setRollbackOnly(true) is called or an exception is thrown 
  } 
});

更进一步,Spring 框架通过简单地启用对它的支持,为将方法调用包含在事务中提供了可靠的基于 AOP 的支持。有了此支持,您不再需要TransactionTemplate——对于使用声明性事务管理启用的任何方法,事务管理都会自动发生。如果您使用的是 Spring 的 XML 配置,则可以使用以下配置:

<tx:annotation-driven transaction-manager = “platformTransactionManagerReference” />

如果您使用的是 Spring 3.1,则可以像这样简单地注释您的 Java 配置类:

 
@EnableTransactionManagement
@Configuration 
public class MyConfiguration { 
  ... 

此注释将自动扫描您的 bean 并查找类型为PlatformTransactionManager的 bean,它将使用该 bean。然后,在您的 Java bean 中,只需通过注释声明方法为事务性方法。


@Transactional
public void work() { 
  // the transaction will be rolled back if the 
  // method throws an Exception, otherwise committed
}

JTA

现在,如果您仍然需要使用 JTA,至少您可以自己做出选择。有两种常见场景:在重量级应用程序服务器中使用 JTA(具有与 JavaEE 服务器绑定的所有令人讨厌的缺点),或使用独立的 JTA 实现。

Spring 通过名为JtaTransactionManagerPlatformTransactionManager实现来提供对基于 JTA 的全局事务实现的支持。如果您在 JavaEE 应用程序服务器上使用它,它将自动从 JNDI 查找正确的javax.transaction.UserTransaction引用。此外,它还将尝试在 9 个不同的应用程序服务器中查找特定于容器的javax.transaction.TransactionManager引用,以便进行更高级的用例,例如事务挂起。在幕后,Spring 加载不同的JtaTransactionManager子类以利用不同服务器中可用的特定额外功能,例如:WebLogicJtaTransactionManagerWebSphereUowTransactionManagerOC4JJtaTransactionManager

因此,如果您位于 Java EE 应用程序服务器内部,无法避免,但想使用 Spring 的 JTA 支持,那么您很有可能可以使用以下命名空间配置支持来正确(并自动)创建JtaTransactionManager

<tx:jta-transaction-manager  />

或者,您可以根据需要注册JtaTransactionManager bean 实例,无构造函数参数,如下所示:


@Bean
 public PlatformTransactionManager platformTransactionManager(){ 
    return new JtaTransactionManager();
}

无论哪种方式,在 JavaEE 应用程序服务器中的最终结果都是,由于 Spring,您现在可以使用 JTA 以统一的方式管理您的事务。

使用可嵌入式事务管理器

许多人选择在 Java EE 应用程序服务器之外使用 JTA,原因显而易见:Tomcat 或 Jetty 更轻、更快、更便宜,测试是可能的(并且更容易),业务逻辑通常不在应用程序服务器中等等。在云计算时代,这些原因比以往任何时候都更为重要,在云计算时代,轻量级、可组合的资源是常态,而重量级、单体式应用程序服务器根本无法扩展。

有许多开源和商业的独立 JTA 事务管理器。在开源社区中,您可以选择几种选择,例如Java 开放事务管理器 (JOTM)JBoss TSBitronix 事务管理器 (BTM)Atomikos

在这篇文章中,我们将介绍一种使用全局事务的简单方法。我们将重点介绍 Atomikos 特定的配置,但源代码中还有一个示例演示了使用 Bitronix 的相同配置。

在这种情况下,事务方法是一个简单的基于 JPA 的服务,它必须同时提交 JMS 消息。代码是典型在线零售商购物车中的假设结账方法。代码如下所示:



	@Transactional
	public void checkout(long purchaseId) {
		Purchase purchase = getPurchaseById(purchaseId);

		if (purchase.isFrozen()) 
		  throw new RuntimeException(
			 "you can't check out Purchase(#" + purchase.getId() + ") that's already been checked out!");

		Date purchasedDate = new Date();
		Set<LineItem> lis = purchase.getLineItems();
		for (LineItem lineItem : lis) {
			lineItem.setPurchasedDate(purchasedDate);
			entityManager.merge(lineItem);
		}
		purchase.setFrozen(true);

		this.entityManager.merge(purchase);
		log.debug("saved purchase updates");
		
		this.jmsTemplate.convertAndSend(this.ordersDestinationName, purchase);
		log.debug("sent partner notification");
	}

该方法使用@Transactional注释来告诉 Spring 将其调用包装在事务中。该方法同时使用 JPA(合并实体的更改状态)和 JMS。因此,工作跨越了两个事务资源,必须使它们在这两者之间保持一致:数据库更新操作和 JMS 发送操作都成功,或者两者都回滚。毕竟,您不希望为尚未付款的产品触发使用 JMS 消息的履行周期!

为了测试 JTA 配置是否有效,您只需在最后一行抛出RuntimeException,如下所示:


 if (true) throw new RuntimeException("Monkey wrench!");

然后,在正常操作下,数据库中的购买实体的状态应该已更改为冻结,并且应该已发送 JMS 消息,从而触发履行。在最后一行抛出的异常将回滚这两个更改。Spring 的声明性事务管理将拦截异常并使用配置的JtaTransactionManager自动回滚事务。然后,您可以验证这两个事件从未发生过,并且它们未反映在相应的资源中:不会排队 JMS 消息,并且 JPA 实体的数据库记录不会更改。我为此使用的测试用例是:




package org.springsource.jta.etailer.store.services;

import org.apache.commons.logging.*;
import org.junit.*;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springsource.jta.etailer.store.config.*;
import org.springsource.jta.etailer.store.domain.*;
import javax.inject.Inject;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class,
       classes = {AtomikosJtaConfiguration.class, StoreConfiguration.class})
public class JpaDatabaseCustomerOrderServiceTest {

	private Log log = LogFactory.getLog(getClass().getName());

	@Inject private CustomerOrderService customerOrderService;
	@Inject private CustomerService customerService;
	@Inject private ProductService productService;

	@Test
	public void testAddingProductsToCart() throws Exception {
		Customer customer = customerService.createCustomer("A", "Customer");
		Purchase purchase = customerOrderService.createPurchase(customer.getId());
		Product product1 = productService.createProduct(
                 "Widget1", "a widget that slices (but not dices)", 12.0);
		Product product2 = productService.createProduct(
                 "Widget2", "a widget that dices (but not slices)", 7.5);
		LineItem one = customerOrderService.addProductToPurchase(
                 purchase.getId(), product1.getId());
		LineItem two = customerOrderService.addProductToPurchase(
                 purchase.getId(), product2.getId());
		purchase = customerOrderService.getPurchaseById(purchase.getId());
		assertTrue(purchase.getTotal() == (product1.getPrice() + product2.getPrice()));
		assertEquals(one.getPurchase().getId(), purchase.getId());
		assertEquals(two.getPurchase().getId(), purchase.getId());
		// this is the part that requires XA to work correctly
		customerOrderService.checkout(purchase.getId());
	}
}

测试是一个简单的交易脚本:购物者创建一个帐户,找到她喜欢的商品,将它们作为行项目添加到购物车,然后结账。结账方法将购物车的更改状态保存到数据库,然后发送触发 JMS 消息以通知其他系统新的订单。正是在这个结账方法中,JTA 至关重要。

配置基本服务

我们有两个配置类——JTA 提供程序特定的代码(已设置以正确构造 Spring 的JtaTransactionManager 实例)——以及其余的配置,无论选择哪种PlatformTransactionManager策略,都应该保持静态。

我已经使用 Spring 的模块化 Java 配置将 JTA 提供程序特定的配置类与其余配置分离,因此您可以轻松地在 Atomikos 特定的 JTA 配置和 Bitronix 特定的 JTA 配置之间切换。

让我们看一下StoreConfiguration类——无论您使用哪个事务管理器实现,它都将相同。我只摘录了重要部分,以便您可以看到哪些部分与 JTA 提供程序特定的配置交互。



package org.springsource.jta.etailer.store.config;

import org.hibernate.cfg.ImprovedNamingStrategy;
import org.hibernate.dialect.MySQL5Dialect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.inject.Inject;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import java.util.Properties;

@EnableTransactionManagement
@Configuration
@ComponentScan(value = "org.springsource.jta.etailer.store.services")
public class StoreConfiguration {

	// ... 	

	// this is a reference to a specific Java configuration class for JTA 
	@Inject private AtomikosJtaConfiguration jtaConfiguration ;

	@Bean
	public JmsTemplate jmsTemplate() throws Throwable{
		JmsTemplate jmsTemplate = new JmsTemplate(  jtaConfiguration.connectionFactory() );
		// ... 
	}

	@Bean
	public LocalContainerEntityManagerFactoryBean entityManager () throws Throwable  {
		LocalContainerEntityManagerFactoryBean entityManager = 
		   new LocalContainerEntityManagerFactoryBean();		
		entityManager.setDataSource(jtaConfiguration.dataSource());
		Properties properties = new Properties();
		// ... 
		jtaConfiguration.tailorProperties(properties);
		entityManager.setJpaProperties(properties);
		return entityManager;
	}

	@Bean
	public PlatformTransactionManager platformTransactionManager()  throws Throwable {
		return new JtaTransactionManager( 
                         jtaConfiguration.userTransaction(), jtaConfiguration.transactionManager());
	}
}

配置都是相当标准的——只是您可能为任何 JPA 或 JMS 应用程序配置的常规对象。为了完成其工作,配置类需要访问javax.jms.ConnectionFactoryjavax.sql.DataSourcejavax.transaction.UserTransactionjavax.transaction.TransactionManager。由于满足这些接口的对象的构造特定于每个事务管理器实现,因此这些 bean 的定义位于单独的 Java 配置类中,我们使用字段注入(@Inject private AtomikosJtaConfiguration jtaConfiguration)在StoreConfiguration类的顶部导入这些类。

我们的StoreConfiguration使用@EnableTransactionManagement注解打开自动事务处理。

我们使用 Spring 3.1 的@PropertySource注解(它与环境抽象相关联)来访问services.properties中的键和值。属性文件如下所示

dataSource.url=jdbc:mysql://127.0.0.1/crm
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
dataSource.user=crm
dataSource.password=crm

jms.partnernotifications.destination=orders
jms.broker.url=tcp://127.0.0.1:61616

任何 JTA 配置最终要做的最重要的事情是提供对提供程序特定的UserTransaction的引用,以及对提供程序特定的TransactionManager的引用,后者用于创建PlatformTransactionManager实例,如下所示


	@Bean	
	public PlatformTransactionManager platformTransactionManager() throws Throwable {
	  UserTransaction userTransaction = jtaConfiguration.userTransaction() ;
	  TransactionManager transactionManager = jtaConfiguration.transactionManager() ;
	  return new JtaTransactionManager(  userTransaction, transactionManager );
	}

配置 Atomikos

我们不会查看 Bitronix 和 Atomikos 实现的细节,只查看一个,因为Atomikos 配置的源代码在此处可用,并且Bitronix 配置的源代码在此处可用。让我们剖析 Atomikos 实现,以便我们可以分解重要的参与者。一旦您知道参与者是谁,那么理解任何第三方 JTA 提供程序的配置就很容易了。在运行代码时切换使用哪个配置非常简单。


package org.springsource.jta.etailer.store.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import com.atomikos.icatch.jta.hibernate3.TransactionManagerLookup;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.atomikos.jms.AtomikosConnectionFactoryBean;
import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
import org.apache.activemq.ActiveMQXAConnectionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

import javax.inject.Inject;
import javax.jms.ConnectionFactory;
import javax.sql.DataSource;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import java.util.Properties;

@Configuration
public class AtomikosJtaConfiguration {

	@Inject private Environment environment ;

	public void tailorProperties(Properties properties) {
		properties.setProperty( "hibernate.transaction.manager_lookup_class", 
				 TransactionManagerLookup.class.getName());
	}

	@Bean
	public UserTransaction userTransaction() throws Throwable {
		UserTransactionImp userTransactionImp = new UserTransactionImp();
		userTransactionImp.setTransactionTimeout(1000);
		return userTransactionImp;
	}

	@Bean(initMethod = "init", destroyMethod = "close")
	public TransactionManager transactionManager() throws Throwable {
		UserTransactionManager userTransactionManager = new UserTransactionManager();
		userTransactionManager.setForceShutdown(false);
		return userTransactionManager;
	}

	@Bean(initMethod = "init", destroyMethod = "close")
	public DataSource dataSource() {
		MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
		mysqlXaDataSource.setUrl(this.environment.getProperty("dataSource.url"));
		mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
		mysqlXaDataSource.setPassword(this.environment.getProperty("dataSource.password"));
		mysqlXaDataSource.setUser(this.environment.getProperty ("dataSource.password"));

		AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
		xaDataSource.setXaDataSource(mysqlXaDataSource);
		xaDataSource.setUniqueResourceName("xads");
		return xaDataSource;
	}

	@Bean(initMethod = "init", destroyMethod = "close")
	public ConnectionFactory connectionFactory() {
		ActiveMQXAConnectionFactory activeMQXAConnectionFactory = new ActiveMQXAConnectionFactory();
		activeMQXAConnectionFactory.setBrokerURL(this.environment.getProperty( "jms.broker.url")  );
		AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
		atomikosConnectionFactoryBean.setUniqueResourceName("xamq");
		atomikosConnectionFactoryBean.setLocalTransactionMode(false);
		atomikosConnectionFactoryBean.setXaConnectionFactory(activeMQXAConnectionFactory);
		return atomikosConnectionFactoryBean;
	}
} 

Atomikos 提供它自己的java.sql.DataSourcejavax.jms.ConnectionFactory包装器,这些包装器可以将任何本机java.sql.DataSourcejavax.jms.ConnectionFactory适配到 JTA(和 XA)感知的包装器。

为了教 Hibernate 如何参与 Atomikos 事务,我们必须设置一个属性——hibernate.transaction.manager_lookup_class——在本例中,是一个名为TransactionManagerLookup的类。对于任何 JTA 实现,您都需要执行此操作。

最后,我们需要提供一个javax.transaction.TransactionManager实现和一个javax.transaction.UserTransaction实现。类顶部的两个 bean 是 Atomikos 对这两个接口的实现,它们用于构造 Spring 的JtaTransactionManager实现,后者是PlatformTransactionManager的实现。

PlatformTransactionManager实例又会被我们的配置类上的@EnableTransactionManagement注解自动拾取,并在调用任何带有@Transactional的方法时用于执行事务。

javax.transaction.UserTransaction实现和javax.transaction.TransactionManager实现的职责相似:UserTransaction 是面向用户的 API,而TransactionManager是面向服务器的 API。所有 JTA 实现都指定一个UserTransaction实现,因为这是 JavaEE 的最低要求。TransactionManager不是必需的,并且并非在每个服务器或 JTA 实现中都可用。

对于熟悉 JTA 的人来说,使用UserTransaction(就像在 JavaEE 中以编程方式控制事务时一样)有一些明显的差距,考虑到 J2EE 最初构思近十年前的过时假设,即没有人会想要在没有 EJB 的情况下进行事务管理,这可能是可以理解的。

问题是,某些操作(例如挂起事务以获得“需要新建”语义)只能在TransactionManager上进行。此接口在 JTA 规范中已标准化,但与UserTransaction不同,它不提供众所周知的 JNDI 位置或其他获取方法。其他一些事情,例如隔离级别的控制或特定于服务器的“事务命名”(用于监控或其他目的),在 JTA 中根本不可能。

TransactionManager 提供高级功能,例如事务挂起和恢复,因此大多数提供程序也支持它。事实上,许多javax.transaction.TransactionManager实现可以在运行时转换为javax.transaction.UserTransaction实现。Spring 知道这一点并且非常聪明。如果您仅使用对 javax.transaction.TransactionManager 的引用来定义 Spring 的JtaTransactionManager实现的实例,它也会尝试在运行时从中强制转换javax.transaction.UserTransaction实例。但是,Atomikos 不会这样做,因此我们明确定义了一个javax.transaction.UserTransaction实例和一个单独的javax.transaction.TransactionManager实例,以便更好地利用javax.transaction.TransactionManager的增强功能。

就是这样!您可以同时拥有和使用 Spring。Bitronix 配置看起来很相似,因为它满足类似的职责。您不必经常调整此代码。您很可能可以简单地重用此处提供的配置,并根据需要调整连接字符串和驱动程序。

总结

在这篇文章中,我们介绍了 Spring 框架中对事务的丰富支持,并且我们介绍了 Spring 独特的能力,它可以与所有类型的 JTA 无缝协作——在嵌入式配置中以及通过现有的应用程序服务器。如果您被迫使用完整的 Java EE 服务器,Spring 对 JTA 的抽象非常简单,并且如果您选择迁移到轻量级容器(如 Apache Tomcat 或 vFabric tc Server),它可以提高代码的可移植性。使用可嵌入事务管理器的此完整示例代码允许您通过简单的配置更改在 Spring bean 中使用相同的业务逻辑。 这篇文章没有介绍 Spring 中常规事务管理支持的细节。要了解使用 Spring 进行基于 XA 的分布式事务管理的最佳方法之一,包括一些关于如何以及何时完全避免它的非常好的见解,请参阅 Dave Syer 博士关于“Spring 中的分布式事务,有和没有 XA。”的文章。

获取 Spring 电子报

与 Spring 电子报保持联系

订阅

领先一步

VMware 提供培训和认证,以加快您的进度。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部