Spring Framework之Transaction Management

Posted qq_23473123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Framework之Transaction Management相关的知识,希望对你有一定的参考价值。

前言

原文链接,本文主要翻译官方文档,同时可能也会对笔者认为不够清楚的地方加上说明、示例、链接等。

Spring Framework事务管理介绍

全面事务支持是使用Spring Framework最令人信服的理由之一。Spring Framework为管理事务提供了一致性的抽象,具有以下优点:

  • 跨不同事务API的一致性编程模型,例如Java事务API(JTA)、JDBC、Hibernate、Java持久化API(JPA)、和Java数据对象(JDO)。
  • 支持声明式事务管理
  • 用于编程事务管理的API比复杂的事务API(如JTA)更简单。
  • 与Spring的数据库访问抽象的完美集成。

以下部分描述了Spring Framework的事务增值和技术。本章还讨论了最佳实践、应用服务器集成和常见问题的解决方案。)

Spring Framework的事务支持模型的优点

传统上,Java EE开发者对事务管理有两个选择:全局或者本地事务,两者都有很大的局限性。在接下来两节中回顾全局和本地事务管理,紧接着讨论Spring Framework的事务管理如何解决全局和本地事务模型的局限性。

全局事务

全局事务是你能够使用多数据源,通常是关系数据库和消息队列。应用服务管理全局事务通过JTA。这是个使用其他很麻烦的API(部分是由于它的异常模型)。此外,JTA UserTransaction通常需要来源于JNDI(java Naming and Directory Interface,java命名和目录接口),意味着你也需要使用JNDI为了使用JTA。显然使用全局事务将限制应用代码的一些潜在的重用,因为JTA通常仅在application server环境中可用(就是本服务中,不能对多服务进行事务控制(如seata))。

之前,使用全局事务的首选方式是通过EJB CMT(Container Managed Transaction):CMT是声明式管理的一种形式(区别于程序化事务管理),EJB CMT取消了事务相关的JNDI查找的需要,尽管使用EJB本身当然需要使用JNDI。它消除了大部分但是不是全部需要编写Java代码来控制事务。CMT明显的缺点是和JTA和application server相关联。此外,它需要选择EJB中或至少在事务性EJB外观之后实现业务逻辑才可用。一般而言EJB的负面影响如此之大,以至于这不是一个吸引人的提议,尤其是在面对声明式事务管理的引人注目的替代方案时。

本地事务

本地事务是特定于资源的,例如与JDBC连接相关联的事务。本地事务也许更容易去使用,但有更大缺点:它们不能垮在多个事务资源使用。例如。代码使用JDBC连接管理的代码不能使用在全局JTA事务中。因为application server不处理事务管理,它不可能帮助确定横跨多个事务源的正确性。(它是值得注意的是大部分应用使用都是单事务资源)另一个去缺点是本地事务是对编程模型具有侵入性的。

Spring Framework的一致性编程模型

spring解决了全局事务和本地事务的缺点。它使用应用程序开发人员能够在任何环境中使用一个一致性编程模型。你编写一次代码,它可以在不同的环境中不同的事务策略收益。Spring Framework提供声明式和编程式事务管理。大部分用户更加喜欢声明式事务管理,这在大多数情况下是推荐的。

通过编程式事务管理,开发者使用Spring Frame事务抽象,它可以在任何事务管理基础架构上。使用更受欢迎的声明模式开发者通常写很少或者不写代码去使用事务管理,因此不依赖Spring Framework事务API和其他一些事务API。

理解Spring Framework事务的抽象

spring 事务抽像的关键在于事务策略的概念。事务策略是被定义由org.springframework.transaction.PlatformTransactionManager接口

public interface PlatformTransactionManager {

    TransactionStatus getTransaction(
            TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

这是主要的服务提供者接口(SPI),尽管它可以在你的应用代码中以编程的方式被使用。因为PlatformTransactionManager是一个接口,它可以根据需要轻松模拟或存根。它不依赖于像JNDI之类的查找策略。PlatformTransactionManager的实现是被定义像任何一些其他对象在Spring Framework IOC容器中。即使你在使用JTA时,仅此好处就使Spring Framework事务成为有价值的抽象。事务代码可以比直接使用JTA更加容易进行测试。

再次和Spring的哲学保持一致,unchecked TransactionException是可以被抛出由任何PlatformTransactionManager接口的方法。事务基本功能失败几乎总是致命的。在极少情况下,应用代码实际上可以从一个事务的失败中恢复,应用开发者仍然可以选择捕获和处理TransactionException。重点是开发者没有强制这么做。

getTransaction(…)方法根据TransactionDefinition参数返回一个TransactionStatus对象。被返回的TransactionStatus可能代表一个新的事务或者代表一个存在的事务如果一个正在匹配的事务存在于调用栈中。这后面一种情况的含义是,与java EE事务中的上下文一样,一个TransactionStatus是被收集在一个执行线程中。

TransactionDefinition接口指定:

  • Isolation(隔离):此事务和其他事务的工作隔离的程度。例如,可以读到其他事务未提交的写?
  • Propagation(传播):通常,在一个事务范围内全部代码是在该事务中运行。但是,当一个事务上下文已经存在,你可以被执行的事务方法选择指定行为。例如,代码可以持续运行在当前事务中;或者存在的事务可以暂停,一个新的事务被创建。Spring提供所有类似于EJB CMT。要了解Spring中事务传播的语义,请参阅第 16.5.7 节,“事务传播”
  • Timeout:在超时之前事务可以运行多久,超时之后回被底层事务框架自动回滚。
  • Read-only状态:当你只读不修改数据的时候你可以使用Read-only事务。在某些情况下,只读事务可能是一种有用的优化,例如当你使用Hibernate时。

TransactionStatus接口为了事务代码提供了一个简单的方式去控制事务执行和查询的状态。这些概念应该都是熟悉的,因为所有事务API都是通用的:

public interface TransactionStatus extends SavepointManager {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

}

而不管你在spring中选择使用声明式事务和编程式进行事务管理,定义一个正确的PlatformTransactionManager实现是绝对重要的,你通常通过依赖注入来定义这个实现。

PlatformTransactionManager实现通常需要了解他们工作的环境:JDBC、JTA、Hibernate等等,接下来的例子展示你如何定义一个本地PlatformTransactionManager实现。(这个例子适用于普通的JDBC)。

你定义一个JDBC DataSource

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

有关PlatformTransactionManager bean定义将引用DataSource定义,如下:

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

如果你在一个Java EE容器中使用JTA,然后你可以通过JNDI使用一个DateSource容器,并结合Spring的JtaTransactionManager。JTA和JNDI查找版本如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd">

    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>

    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />

    <!-- other <bean/> definitions here -->

</beans>

JtaTransactionManager不需要知道DataSource,一些具体来源,因为它使用容器全局事务管理。

你可以轻松的使用Hibernate本地事务,就如下面的例子。在这种情况下,你需要定义一个Hibernate LocalSessionFactoryBean,在你的代码中将使用包含Hibernate Session实例。

DataSourcebean定义将是和定义本地JDBC类似的,前面已经展示,因此是不被展示在下面的例子中。

在例子中txManager bean属于HibernateTransactionManager类型。和DateSourceTransactionManager一样需要一个DataSource引用,HibernateTransactionManager需要引用SessionFactory。

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

如果你使用Hibernate和Java EE容器管理JTA事务,你应该简单使用相同的JtaTransactionManager就像之前的JTA的JDBC例子。

<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

资源和事务的同步

你现在应该清楚如何创建不同的事务管理和它们是如何链接需要同步事务的关联资源(例如DataSourceTransactionManager同步关联一个JDBC DataSource,HibernateTransactionManager关联一个Hibernate SessionFactory等等)。这节描述应该使用要一个持久性AP例如JDBC、Hibernate或者JDO,确保这些资源是被创建、复用和适当地清理。本节还讨论了如何通过相关的PlatformTransactionManager触发(可选)事务同步

高级同步方法

首选方法是使用Spring基于持久化集成APIs的高级模版或者使用带有transaction-aware factory beans或者代理管理本地ORM APIs。这些transaction-aware解决方案在内部处理resource创建、重用、清理、可选事务资源同步和异常映射。因此用户数据访问不必处理这些任务,而可以专注于集中非模版的持久化逻辑代码。一般情况下,你可以使用本地ORM API或者通过使用JdbcTemplate使用一个JDBC访问模版方法。这些处理是被详细介绍在本参考文档的随后章节。

低级同步方法

诸如DataSourceUtils(对于JDBC),EntityManagerFactoryUtils(对于JPA)、SessionFactoryUtils(对于Hibernate)、PersistenceManagerFactoryUtils(对于JDO)等存在低级别的类。当你想要使用应用代码直接处理本地持久化APIs的resource类,你使用这些类来确保获得合适的Spring Framework-managed实例,事务(可选)同步,过程中发生的异常时正确的映射到一致性API中。

例如,在使用JDBC的情况下,替换传统的在DataSource上调用getConnection()方法的JDBC方法,而是使用Spring的org.springframework.jdbc.datasource.DataSourceUtils如下:

Connection conn = DataSourceUtils.getConnection(dataSource);

如果一个存在中的事务存在一个与之同步的连接,则返回该实例。否则方法调用触发创建新的连接,该连接(可选)同步到任何现有事务,并可供该事务中的后续重用。如前所述,一些SQL Exception是包含在Spring Framework中的CannotGetJdbcConnectionException,是Spring Framework未经检查的DataAccessExceptiion之一。这种方法比SQLException中获得更多的信息,并确保方便的跨不同数据库甚至跨不同持久性技术。

这种方法也可以在没有Spring transation management(事务同步是可选的)生效,因此无论是否使用Spring transaction management你都可以使用它。

当然,一旦你使用了Spring的JDBC支持、JPA支持或者Hibernate支持,你通常不使用DataSourceUtils或其他帮助类,因为通过Spring 抽象比直接使用相关API会更快乐。如果你使用Spring JdbcTemplate(内部使用DataSourceUtils)或jdbc.object包去简化你使用JDBC,正确的连接发生在幕后,你无需编写任何特殊代码。

TransactionAwareDatasourceProxy

在最底层存在TransactionAwareDataSourceProxy类。这是一个对目标DataSource的代理,包装目标DataSource并添加Spring-managed transactions的awareness。在这个方面,它是类似于一个由Java EE服务器提供的事务性JNDI DataSource。

你应该几乎不需要使用此类,除非必须调用现有代码并传递一个标准的JDBC DataSource接口实现。在这种情况下,这个代码可能是可用的,但参与Spring managed transaction。它是更好使用上面提到的更高级别抽象方法去写你的新代码。

声明式事务管理

大多数Spring Framework使用者声明事务管理。这个选择对应用代码有最小的入侵,因此最符合非入侵轻量容器观念。

Spring Framework的声明式管理是通过Spring aspect-oriented programming(AOP)成为可能,尽管由于事务切面代码随着Spring Framework发布来到并且可以以样板的方式使用,AOP概念通常有效使用不需要必须被理解。

Spring Framework的事务管理是类似于EJB CMT,因为你可以指定transaction behavior(或缺少它)指定到单个方法级别。如果有必要可以在上下文中调用setRoolbackOnly()。两种事务管理的不同如下:

  • 与绑定到JTA的EJB CMT不同,Spring Framework的声明式事务工作的任何环境中。它可以通过简单调整配置文件和JTA transaction或者使用JDBC、JPA、Hibernate或者JDO的本地事务一起工作。
  • 你可以在任何类中应用Spring Framework声明式事务管理,不像EJB仅在一些特殊的类中。
  • Spring Framework通过声明式回滚规则,没有EJB的特性。回滚是支持编程式或者声明式的。
  • spring Framework使用能够定制事务的行为,由使用AOP。例如你可以在事务回滚的情况下插入定制行为。你可以添加任何advice以及事务的advice。在使用EJB CMT时你除了setRollbackOnly()之外不能影响容器的事务管理。
  • Spring Framework不支持跨远程调用事务上下文传播,高端应用服务也是如此。如果你需要这个功能,我们建议你使用EJB。然而,在使用此类功能之前需要仔细考虑,因为通常情况下,人们不希望事务跨远程调用。

回滚概念很重要的:他们确保你去指定哪一个异常(和throwables)应该造成自动回滚。你应该在配置中指定这个声明而不是在Java代码中。所以,虽然你仍然可以在TransactionStatus对象上调用setRollbackOnly()去回滚当前事务,大部分情况下你可以指定一个规则:MyApplicationException必须总是回滚。这个选项的明显优势是业务对象不依赖于事务底层。例如,他们通常不需要import Spring transction APIs或者其他Spring APIs。

虽然EJB容器默认行为是在一个system exception(通常是运行时异常)自动的回滚这事务,但是EJB CMT不在一个application exception(即除java.rmi.RemoteException之外的检查异常)自动回滚。当对于声明式事务管理的Spring默认行为遵循EJB约定(仅在unchecked异常中自动回滚),但定制行为通常是有用的。

理解Spring Framework的声明式事务实现

它是不够的仅告诉你简单的去使用注解@Transactional去注解你的类、在你的configuraction上添加@EnabeTransactionManagement,然后期望你去理解它是如何工作的。本节解释在和事务相关问题时Spring Framework声明式事务底层是如何工作的。

关于Spring Framework的声明式事务,需要掌握的最重要的概念是这种通过AOP代理是支持的,并且事务的advice是被驱动由元数据(当前的XML-或者annotation-based)。AOP和事务的元数据组合产生了一个AOP代理(由TransactionInterceptor和一个适合的PlatformTransactionManager实现去驱动事务around method)。

从概念上讲,在事务代理上调用方法看起来像这样…

声明式事务实现示例

考虑到以下接口和它的伴随实现。这个例子使用Foo和Bar类作为占位符,以便你可以集中精力在事务的使用而不是在特定的领域模型。对于例子的目的而言,DefaultFooService类在每一个方法体的实现中抛出UnsupportOperationException示例是很好的;它让你可以看看事务被创建和回滚在返回UnsupportOperationException实例中。

// the service interface that we want to make transactional

package x.y.service;

public interface FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);

}
// an implementation of the above interface

package x.y.service;

public class DefaultFooService implements FooService {

    public Foo getFoo(String fooName) {
        throw new UnsupportedOperationException();
    }

    public Foo getFoo(String fooName, String barName) {
        throw new UnsupportedOperationException();
    }

    public void insertFoo(Foo foo) {
        throw new UnsupportedOperationException();
    }

    public void updateFoo(Foo foo) {
        throw new UnsupportedOperationException();
    }

}

假设FooService接口前面两个方法getFoo(String)和getFoo(String,String)必须在可以读写的事务上下文中执行。下面的配置将在下面几段中详细解释。

<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name="get*" read-only="true"/>
            <!-- other methods use the default transaction settings (see below) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- ensure that the above transactional advice runs for any execution
        of an operation defined by the FooService interface -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>

    <!-- don't forget the DataSource -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>
    <!-- similarly, don't forget the PlatformTransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>

检查前面的配置。你想让使一个fooService对象具有事务性。应用事务语义是被封装在<tx:advice/>定义中。这个<tx:advice/>定义读作"…所有以‘get’开头的方法是被执行在可读事务上下文中并且所有其他方法是被执行在默认的事务语义中"。<tx:advice/>标签的transaction-manager属性是被设置成用来推动事务PlatformTransactionManager属性的名字,在本例中是txManager bean。

这个<aop:config/>定义确保transactional advice被定义由txAdvice bean会在程序的适当点上执行。首先定义一个匹配任何一个FooService接口方法的切点(名为fooServiceOperation)。然后使用一个advisor在切入点关联txAdvice。fooServiceoperation的执行结果表明,txAdvice将被执行。

<aop:pointcut/>元素中被定义的表达式是一个面向切面切点表达式;spring的切面表达式更多的详情查看第十章,spring的面向切面编程

一个常用需要是是使整个服务层事务。最好的方式是对切点表达式做一个简单吧的改变去匹配任何一个方法在你的服务层中。如下:

<aop:config>
    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>

现在我们已经分析了配置,你可能会问自己,“好吧…但是这些配置实际上做了啥呢?”。

上面的配置将是被使用去创建一个事务的代理围绕着这个从fooService bean定义生成的对象。这个代理将被配置使用transactional advice,以便在代理上调用适当的方法时,一个事务是开始的、暂停的、标记为只可读的等等,依赖于与该方法关联的事务配置。考虑以下程序测试驱动上面的配置。

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);
        FooService fooService = (FooService) ctx.getBean("fooService");
        fooService.insertFoo (new Foo());
    }
}

输出如下

<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors

<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]

<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) --以上是关于Spring Framework之Transaction Management的主要内容,如果未能解决你的问题,请参考以下文章

Spring Framework之Transaction Management

Spring Framework之Transaction Management

Spring Framework之Transaction Management

Spring Framework之Transaction Management

[Java安全]Java反序列化之spring-tx(Spring Framework 4.2.4)

[Java安全]Java反序列化之spring-tx(Spring Framework 4.2.4)