Spring JDBC-Spring对事务管理的支持

概述

Spring 为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象。也就是说,不管选择 Spring JDBC、Hibernate 、JPA 还是 iBatis,Spring 都让我们可以用统一的编程模型进行事务管理。

类似 Spring DAO 为不同的持久化技术实现提供了模板类一样,Spring 事务管理也提供了事务模板类 TransactionTemplate。 通过 TransactionTemplate 并配合使用事务回调 TransactionCallback 指定具体的持久化操作,就可以 通过编程的方式实现事务管理,而无须关注资源获取、复用、释放、事务同步和异步处理等操作。

Spring 事务管理的亮点在于声明式事务管理,Spring 允许通过声明的方式,在 IoC 配置中指定事务的边界和事务属性,Spring 会自动在指定的事务边界上应用事务属性。

事务管理关键抽象

在Spring事务管理SPI(Service Provider Interface)的抽象层主要包括3个接口,分别是 PlatformTransactionManager、TransactionDefinition 和 TransactionStatus。 都在 org.springframework.transaction 包中

  • TransactionDefinition 用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务属性可以通过XML配置或注解描述提供,也可以通过手工编程的方式设置。

  • PlatformTransactionManager 根据 TransactionDefinition 提供的事务属性配置信息,创建事务,并用 TransactionStatus 描述这个激活事务的状态。

Spring事务管理的实现类

spring将事务管理委托底层具体的持久化实现框架去完成,因此针对不同的框架 spring 有的不同的接口实现类.

事务说明
org.springframework.orm.jpa.JpaTransactionManager使用JPA进行持久化时,使用该事务管理器
org.springframework.orm.hibernateX.HibernateTransactionManager使用HibernateX版本时使用该事务管理器
org.springframework.jdbc.datasource.DataSourceTransactionManager使用SpringJDBC或MyBatis等基于DataSource数据源的持久化技术时,使用该事务管理器
org.springframework.orm.jdo.JdoTransactionManager使用JDO进行持久化时,使用该事务管理器
org.springframework.transaction.jta.JtaTransactionManager具有多个数据源的全局事务使用该事务管理器(不管采用何种持久化技术)

要实现事务管理,首先要在Spring中配置好相应的事务管理器,为事务管理器指定数据资源及一些其他事务管理控制属性。

下面介绍一下几个常见的事务管理器的配置

Spring JDBC 和 MybBatis 的事务管理器的配置

Spring JDBC 和 MybBatis 都是基于数据源的 Connection 访问数据库,所有都可以使用 DataSourceTransactionManager, 配置如下

<!--引用外部的Properties文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>

<!--配置一个数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close"
          p:driverClassName="${jdbc.driverClassName}"
          p:url="${jdbc.url}"
          p:username="${jdbc.username}"
          p:password="${jdbc.password}"/>

<!--基于数据源的事务管理器,通过属性引用数据源-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
      p:dataSource-ref="dataSource"/>

JPA的事务管理器的配置

要配置一个 JPA 事务管理器,必须现提供一个 DataSource,然后配置一个 EntityManagerFactory,最后才配置 JpaTransationManager.

.......

<!--通过dataSource-ref指定一个数据源-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
          p:dataSource-ref="dataSource"/>
    ......
</bean>

<!--指定实体管理器-->

<bean id="transactionManger" class="org.springframework.orm.jpa.JpaTransactionManager"
          p:entityManagerFacotry-ref="entityManagerFactory"/>

Hibernate的事务管理器的配置

Spring4.0 已经取消了对 Hibernate3.6 之前的版本支持,并全面支持 Hibernate5.0. 因此,只为 Hibernate3.6+ 提供事务管理器。

以Hibernate4.0为例

....
<!--通过dataSource-ref引用数据源 和 Hibernate配置文件 及其他属性-->
<bean id="sessionFactory"
     class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"
     p:dataSource-ref="dataSource"
     p:mappingResources="classpath:Artisan.hbm.xml">

   <property name="hibernateProperties">
     <props>
       <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
       <prop key="hibernate.show_sql">true</prop>
       <prop key="hibernate.generate_statistics">true</prop>
     </props>
   </property>
</bean>

<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"
      p:sessionFactory-ref="sessionFactory"/>

JTA 的事务管理器的配置

如果希望在 JavaEE 容器中使用 JTA,则将通过 JNDI 和 Spring 的 JtaTransactionManager 获取一个容器的 DataSource。

<!--通过jee命名空间获取Java EE应用服务器容器中的数据源-->
<jee:jndi-lookup id="accountDs" jndi-name="java:comp/env/jdbc/account"/>
<jee:jndi-lookup id="orderDs" jndi-name="java:comp/env/jdbc/account"/>

<!--指定JTA事务管理器。-->
<bean id="transactionManager"
  class="org.springframework.transaction.jta.JtaTransactionManager"/>

事务同步管理器

Spring 将 JDBC 的 Connection、Hibernate 的 Session 等访问数据库的连接或者会话对象统称为资源,这些资源在同一时刻是不能多线程共享的。

为了让 DAO、Service 类可能做到 singleton, Spring 的事务同步管理类 org.springframework.transaction.support.TransactionSynchronizationManager 使用 ThreadLocal 为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。

事务同步管理器是 Spring 事务管理的基石,不管用户使用的是编程式事务管理,还是声明式事务管理,都离不开事务同步管理器。

Spring 框架为不同的持久化技术提供了一套从 TransactionSynchronizationManager 中获取对应线程绑定资源的工具类

持久化技术线程绑定资源获取工具
Spring JDBC 或者 MyBatisorg.springframework.jdbc.datasource.DataSourceUtil
HibernateX.0org.springframework.orm.hibernateC.SessionFactoryUtils
JPAorg.springframework.orm.jpa.EntityManagerFactoryUtils
JDOorg.springframework.orm.jdo.PersistenceManagerFactoryUtils

这些工具类都提供了静态的方法,通过这些方法可以获取和当前线程绑定的资源,如

  • DataSourceUtils.getConnection (DataSource dataSource)可以从指定的数据源中获取和当前线程绑定的Connection

  • Hibernate的SessionFactoryUtils.getSession (SessionFactory sessionFactory, boolean allowCreate)则从指定的SessionFactory中获取和当前线程绑定的Session。

当需要脱离模板类,手工操作底层持久技术的原生API时,就需要通过这些工具类获取线程绑定的资源,而不应该直接从DataSource或SessionFactory中获取。因为后者不能获得和本线程相关的资源,因此无法让数据操作参与到本线程相关的事务环境中。

这些工具类还有另外一个重要的用途:将特定异常转换为Spring的DAO异常。

Spring为不同的持久化技术提供了模板类,模板类在内部通过资源获取工具类间接访问TransactionSynchronizationManager中的线程绑定资源。所以,如果Dao使用模板类进行持久化操作,这些Dao就可以配置成singleton。如果不使用模板类,也可直接通过资源获取工具类访问线程相关的资源。

我们来开下TransactionSynchronizationManager的面纱:

TransactionSynchronizationManager 将 Dao、Service 类中影响线程安全的所有“状态”统一抽取到该类中,并用 ThreadLocal 进行替换,从此 Dao(必须基于模板类或资源获取工具类创建的 Dao)和 Service(必须采用 Spring 事务管理机制)摘掉了非线程安全的帽子,完成了脱胎换骨式的身份转变。

事务的传播行为

当我们调用一个基于 Spring 的 Service 接口方法(如 UserService#addUser())时,它将运行于 Spring 管理的事务 环境中,Service 接口方法可能会在内部调用其它的 Service 接口方法以共同完成一个完整的业务操作,因此就会产生服务接口方法嵌套调用的情况, Spring 通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。

事务传播是 Spring 进行事务管理的重要概念,其重要性怎么强调都不为过。但是事务传播行为也是被误解最多的地方,在本文里,我们将详细分析不同事务传播行为的表现形式,掌握它们之间的区别。

Spring 在 TransactionDefinition 接口中规定了7种类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播:

事务传播行为类型说明
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

当使用 PROPAGATION_NESTED 时,底层的数据源必须基于 JDBC 3.0,并且实现者需要支持保存点事务机制。

示例

当服务接口方法分别使用表1中不同的事务传播行为,且这些接口方法又发生相互调用的情况下,大部分组合都是一目了然,容易理解的。但是,也存在一些容易引起误解的组合事务传播方式。

下面,我们通过两个具体的服务接口的组合调用行为来破解这一难点。这两个服务接口分别是 UserService 和 ForumService, UserSerice 有一个a ddCredits() 方法,ForumSerivce#addTopic() 方法调用了 UserSerice#addCredits() 方法,发生关联性服务方法的调用:

@Service
public class ForumService {
    private UserService userService;
    // ①调用其它服务接口的方法
    public void addTopic() {
        // ②被关联调用的业务方法
        userService.addCredits();
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

嵌套调用的事务方法 : 对 Spring 事务传播行为最常见的一个误解是:当服务接口方法发生嵌套调用时,被调用的服务方法只能声明为 PROPAGATION_NESTED。这种观点犯了望文生义的错误,误认为 PROPAGATION_NESTED 是专为方法嵌套准备的。这种误解遗害不 浅,执有这种误解的开发者错误地认为:应尽量不让 Service 类的业务方法发生相互的调用,Service 类只能调用 DAO 层的 DAO 类,以避免产生嵌 套事务。

其实,这种顾虑是完全没有必要的,PROPAGATION_REQUIRED 已经清楚地告诉我们:事务的方法会足够“聪明”地判断上下文是否已经存在一个事务中,如果已经存在,就加入到这个事务中,否则创建一个新的事务。

依照上面的例子,假设我们将 ForumService#addTopic() 和 UserSerice#addCredits() 方法的事务传播行为都设置为 PROPAGATION_REQUIRED,这两个方法将运行于同一个事务中。

将 ForumService#addTopic() 设置为 PROPAGATION_REQUIRED时, UserSerice#addCredits() 设置为 PROPAGATION_REQUIRED、PROPAGATION_SUPPORTS、 PROPAGATION_MANDATORY 时,运行的效果都是一致的(当然,如果单独调用 addCredits() 就另当别论了)。

当 addTopic() 运行在一个事务下(如设置为PROPAGATION_REQUIRED),而addCredits()设置为 PROPAGATION_NESTED 时,如果底层数据源支持保存点,Spring 将为内部的 addCredits() 方法产生的一个内嵌的事务。如果 addCredits() 对应的内嵌事务执行失败,事务将回滚到 addCredits() 方法执行前的点,并不会将整个事务回滚。内嵌事务是内层事务的一 部分,所以只有外层事务提交时,嵌套事务才能一并提交。

嵌套事务不能够提交,它必须通过外层事务来完成提交的动作,外层事务的回滚也会造成内部事务的回滚。

嵌套事务和新事务

PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED也是容易混淆的两个传播行为。PROPAGATION_REQUIRES_NEW 启动一个新的、和外层事务无关的“内部”事务。该事务拥有自己的独立隔离级别和锁,不依赖于外部事务,独立地提交和回滚。当内部事务开始执行时,外部事务 将被挂起,内务事务结束时,外部事务才继续执行。

由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于:

  • PROPAGATION_REQUIRES_NEW 将创建一个全新的事务,它和外层事务没有任何关系,

  • 而 PROPAGATION_NESTED 将创建一个依赖于外层事务的子事务,当外层事务提交或回滚时,子事务也会连带提交和回滚。

以下几个问题值得注意:

  1. 当业务方法被设置为PROPAGATION_MANDATORY时,它就不能被非事务的业务方法调用。

如将ForumService#addTopic ()设置为PROPAGATION_MANDATORY,如果展现层的Action直接调用addTopic()方法,将引发一个异常。正确的情况是: addTopic() 方法必须被另一个带事务的业务方法调用(如 ForumService#otherMethod())。所以 PROPAGATION_MANDATORY的方法一般都是被其它业务方法间接调用的。

  1. 当业务方法被设置为PROPAGATION_NEVER时,它将不能被拥有事务的其它业务方法调用。

假设 UserService#addCredits() 设置为PROPAGATION_NEVER,当 ForumService# addTopic() 拥有一个事务时,addCredits() 方法将抛出异常。所以 PROPAGATION_NEVER 方法一般是被直接调用的。

  1. 当方法被设置为 PROPAGATION_NOT_SUPPORTED 时,外层业务方法的事务会被挂起,当内部方法运行完成后,外层方法的事务重新运行。如果外层方法没有事务,直接运行,不需要做任何其它的事。

在Spring声明式事务管理的配置中,事务传播行为是最容易被误解的配置项,原因在于事务传播行为名称(如 PROPAGATION_NESTED:嵌套式事务)和代码结构的类似性上(业务类方法嵌套调用另一个业务类方法).

编程式的事务管理

在实际的应用中很少通过编程来进行事务管理,但是 Spring 还是为编程式事务管理提供了模板类 TransactionTemplate,以满足一些特殊场合的要求。

TransactionTemplate 是线程安全的,因此可以在多个类中共享 TransactionTemplate 实例进行事务管理。

TransactionTemplate 主要有两个方法:

  • public void setTransactionManager(PlatformTransactionManager transactionManager) 设置事务管理器

  • public <T> T execute(TransactionCallback<T> action) throws TransactionException 在TransactionCallback回调接口中定义需要以事务方式组织的数据访问逻辑

TransactionCallback 接口中仅有一个方法

protected void doInTransaction(TransactionStatus status)

如果操作不需要返回结果,可以使用 TransactionCallback 的子接口 TransactionCallbackWithoutResult。

示例

代码已托管到 Github —> https://github.com/yangshangwei/SpringMaster

POJO

package com.xgj.dao.transaction.programTrans;

import org.springframework.stereotype.Component;

/**
 * 
 * 
 * @ClassName: Artisan
 * 
 * @Description: @Component标注的Bean
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年9月18日 下午5:03:47
 */

@Component
public class Artisan {

    private String userName;
    private String password;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}
package com.xgj.dao.transaction.programTrans;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * 
 * 
 * @ClassName: ProgramTransService
 * 
 * @Description: 在实际应用中,很少通过编程的方式来进行事务管理。
 * 
 * @author: Mr.Yang
 * 
 * @date: 2017年9月21日 下午3:48:10
 */

@Service
public class ProgramTransService {

    private JdbcTemplate jdbcTemplate;
    private TransactionTemplate transactionTemplate;

    // 下面两条SQL在一个事务中,第二条故意写错了表名,会执行失败,第一条已经成功的SQL也会回滚
    private static final String addArtisanSQL = "insert into artisan_user(user_name,password) values(?,?)";
    private static final String deleteOneArtisanSQL = "delete from artisan_user1 where user_name = 'ArtisanBatch0' ";

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 
     * 
     * @Title: setTransactionTemplate
     * 
     * @Description: 通过AOP主动注入transactionTemplate
     * 
     * @param transactionTemplate
     * 
     * @return: void
     */
    @Autowired
    public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }

    public void operArtisanInTrans(final Artisan artisan) {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                // 需要在事务中执行的逻辑
                jdbcTemplate.update(addArtisanSQL, artisan.getUserName(),
                        artisan.getPassword());
                System.out.println("addArtisanSQL  OK ");
                jdbcTemplate.update(deleteOneArtisanSQL);
                System.out.println("deleteOneArtisanSQL  OK ");
            }
        });

    }
}

配置文件

<?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:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 扫描类包,将标注Spring注解的类自动转化Bean,同时完成Bean的注入 -->
    <context:component-scan base-package="com.xgj.dao.transaction.programTrans" />

    <!-- 不使用context命名空间,则需要定义Bean 
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 
        <property name="locations" value="classpath:spring/jdbc.properties" /> 
    </bean> -->

    <!-- 使用context命名空间,同上面的Bean等效.在xml文件中配置数据库的properties文件 -->
    <context:property-placeholder location="classpath:spring/jdbc.properties" />

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

    <!-- 配置Jdbc模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
        p:dataSource-ref="dataSource" />

    <!--基于数据源的事务管理器,通过属性引用数据源-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
      p:dataSource-ref="dataSource"/>

    <!-- 配置transactionTemplate模板 -->    
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"
        p:transactionManager-ref="transactionManager"/>


</beans>

单元测试

package com.xgj.dao.transaction.programTrans;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ProgramTransServiceTest {

    ClassPathXmlApplicationContext ctx = null;

    @Before
    public void initContext() {
        // 启动Spring 容器
        ctx = new ClassPathXmlApplicationContext(
                "classpath:com/xgj/dao/transaction/programTrans/conf_program_transaction.xml");
        System.out.println("initContext successfully");
    }

    @Test
    public void testProgramTransaction() {

        Artisan artisan = ctx.getBean("artisan", Artisan.class);
        artisan.setUserName("trans");
        artisan.setPassword("123");

        ProgramTransService programTransService = ctx.getBean(
                "programTransService", ProgramTransService.class);

        programTransService.operArtisanInTrans(artisan);

        System.out.println("testProgramTransaction successsfully");
    }

    @After
    public void closeContext() {
        if (ctx != null) {
            ctx.close();
        }
        System.out.println("close context successfully");
    }

}

运行结果

第二条因为执行失败,第一条也回滚了,未插入数据, OK。

Spring JDBC-Spring对事务管理的支持