领先一步
VMware 提供培训和认证,加速你的进步。
了解更多Spring 通过其 PlatformTransactionManager
接口及实现层次结构提供了丰富的事务管理支持。Spring 的事务支持为许多 API 的事务语义提供了一致的接口。广义上,事务可以分为两类:本地事务和全局事务。本地事务只影响一个事务资源。通常,即使没有明确体现事务的概念,这些资源也有自己的事务 API。通常它以会话的概念出现,即一个具有划分 API 的工作单元,用于告诉资源何时应提交缓冲的工作。全局事务是跨越一个或多个事务资源,并将它们全部纳入一个事务中。
JMS 和 JDBC API 中有一些常见的本地事务示例。在 JMS 中,用户可以创建一个事务性 Session,发送和接收消息,当消息处理完成后,调用 Session.commit()
来告诉服务器可以最终确定工作。在数据库领域,JDBC Connection
默认自动提交查询。这对于一次性语句来说没问题,但通常最好将几个相关的语句收集成一个批处理,然后全部提交或全部回滚。在 JDBC 中,你通过首先将 Connection
的 setAutoCommit()
方法设置为 false,然后在批处理结束时显式调用 Connection.commit()
来实现。这两个 API 以及许多其他 API,都提供了事务性工作单元的概念,这些工作单元可以根据客户端的意愿进行提交、最终确定、刷新或以其他方式永久化。这些 API 差异很大,但概念是相同的。
全局事务完全是另一种不同的东西。当你想让多个资源参与到同一个事务中时,就应该使用它们。这有时是强制要求的:也许你想发送一条 JMS 消息并写入数据库?或者,也许你想使用 JPA 的两个不同的持久化上下文?在全局事务设置中,第三方事务监视器将多个事务资源纳入一个事务中,准备它们进行提交——在此阶段,资源通常执行相当于预提交的操作——最后,提交每个资源。这些步骤是大多数全局事务实现的基础,称为两阶段提交 (2PC)。如果某个提交失败(因为在准备提交时不存在的原因,例如网络中断),那么事务监视器会要求每个资源撤销或回滚上一个事务。
全局事务明显比常规的本地事务复杂得多,因为根据定义,它们必须涉及一个第三方代理,该代理的唯一功能是协调多个事务资源之间的事务状态。事务监视器还必须知道如何与每个事务资源通信。由于此代理还必须保持状态——毕竟,不能信任单独的事务资源了解其他资源正在做什么——它具有持久性要求和同步成本,以保持一致的事务日志,非常像数据库。当发生灾难性事件且事务监视器关闭时,它必须能够启动并重播正在进行的事务,以确保所有资源对于任何中断的事务都处于一致状态。为了与事务资源通信,事务监视器必须与事务资源使用通用协议进行通信。通常,此协议称为 XA 规范。
在企业级 Java 世界中,向应用程序添加 XA 的典型方法是使用 JTA。Java Transaction 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 可以优雅地避免这种复杂性。
PlatformTransactionManager
API 和 Spring 框架中相关的事务管理 API。Spring 框架及相关项目提供了丰富的 PlatformTransactionManager
实现选择,支持 JDBC、JMS、Gemfire、AMQP、Hibernate、JPA、JDO、iBatis 等许多技术中的本地事务。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,并使用它。然后,在你的 Java Bean 中,只需通过注解来声明方法为事务性的。
@Transactional
public void work() {
// the transaction will be rolled back if the
// method throws an Exception, otherwise committed
}
Spring 通过名为 JtaTransactionManager
的 PlatformTransactionManager
实现,提供了对基于 JTA 的全局事务实现的支持。如果你在 JavaEE 应用服务器上使用它,它会自动从 JNDI 中找到正确的 javax.transaction.UserTransaction
引用。此外,它还会在 9 种不同的应用服务器中尝试查找容器特定的 javax.transaction.TransactionManager
引用,用于更高级的用例,如事务挂起。在幕后,Spring 会加载不同的 JtaTransactionManager
子类,以便在可用时利用不同服务器中特定的额外功能,例如:WebLogicJtaTransactionManager
、WebSphereUowTransactionManager
和 OC4JJtaTransactionManager
。
因此,如果你身处 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 Open Transaction Manager (JOTM)、JBoss TS、Bitronix Transaction Manager (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!");
在正常操作下,此时数据库中的 purchase 实体应已将其状态更改为 frozen,并且 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.ConnectionFactory
、javax.sql.DataSource
、javax.transaction.UserTransaction
和 javax.transaction.TransactionManager
。由于构建满足这些接口的对象对于每个事务管理器实现都是特定的,因此这些 Bean 的定义位于一个单独的 Java 配置类中,我们使用字段注入(在 StoreConfiguration
类的顶部使用 @Inject private AtomikosJtaConfiguration jtaConfiguration
)来导入它。
我们的 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://localhost:61616
任何 JTA 配置最终最重要的作用是提供对提供者特定的 UserTransaction
的引用,以及对提供者特定的 TransactionManager
的引用,这些引用用于创建 PlatformTransactionManager
实例,如下所示:
@Bean
public PlatformTransactionManager platformTransactionManager() throws Throwable {
UserTransaction userTransaction = jtaConfiguration.userTransaction() ;
TransactionManager transactionManager = jtaConfiguration.transactionManager() ;
return new JtaTransactionManager( userTransaction, transactionManager );
}
我们不会详细介绍 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.DataSource
和 javax.jms.ConnectionFactory
包装器,可以将任何原生的 java.sql.DataSource
或 javax.jms.ConnectionFactory
适配为支持 JTA(和 XA)的类型。
为了让 Hibernate 参与到 Atomikos 事务中,我们必须设置一个属性——hibernate.transaction.manager_lookup_class
——在本例中,这个类叫做 TransactionManagerLookup
。对于任何 JTA 实现,你都需要这样做。
最后,我们需要提供一个 javax.transaction.TransactionManager
实现和一个 javax.transaction.UserTransaction
实现。类顶部的两个 Bean 是 Atomikos 对这两个接口的实现,它们用于构建 Spring 的 JtaTransactionManager
实现,而 JtaTransactionManager
是 PlatformTransactionManager
的一个实现。
然后,PlatformTransactionManager
实例会被我们配置类上的 @EnableTransactionManagement
注解自动识别,并在任何带有 @Transactional
注解的方法被调用时用于执行事务。
javax.transaction.UserTransaction
实现和 javax.transaction.TransactionManager
实现的职责相似:UserTransaction 是面向用户的 API,而 TransactionManager
是面向服务器的 API。所有 JTA 实现都指定了 UserTransaction
实现,因为它是 JavaEE 最低要求的。TransactionManager
不是强制要求,并且并非所有服务器或 JTA 实现都提供它。
对于熟悉 JTA 的人来说,像在 JavaEE 中以编程方式控制事务那样使用 UserTransaction
,存在一些显著的不足。这或许可以理解,考虑到近十年前 J2EE 首次构思时,人们普遍认为没有人会想在没有 EJB 的情况下进行事务管理,这种假设如今已经过时。
问题在于,某些操作,例如挂起事务(例如为了实现“requires new”语义),只能通过 TransactionManager
来完成。这个接口在 JTA 规范中是标准化的,但与 UserTransaction
不同,它没有提供一个知名的 JNDI 位置或其他获取方式。其他一些事情,例如控制隔离级别或特定于服务器的“事务命名”(用于监控或其他目的),使用 JTA 是完全不可能的。
TransactionManager 提供了诸如事务挂起和恢复等高级功能,因此大多数提供者也支持它。事实上,许多 javax.transaction.TransactionManager
实现可以在运行时被强制转换为 javax.transaction.UserTransaction
实现。Spring 知道这一点,并且处理得非常巧妙。如果你定义 Spring 的 JtaTransactionManager
实现实例时只引用了 javax.transaction.TransactionManager
,它也会在运行时尝试从中获取一个 javax.transaction.UserTransaction
实例。然而,Atomikos 不会这样做,因此我们显式地定义了一个 javax.transaction.UserTransaction
实例,并且为了更好地利用 javax.transaction.TransactionManager
更强大的功能,还定义了一个单独的 javax.transaction.TransactionManager
实例。
就这样!有了 Spring,你可以鱼与熊掌兼得。Bitronix 的配置看起来也很相似,因为它承担着相似的职责。你不需要经常调整这些代码。很可能你可以直接重用这里提供的配置,只需适当调整连接字符串和驱动程序。