Spring从入门到精通—Spring事务配置使用实例

1、Spring事务控制概述

       Spring支持编程式事务管理声明式事务管理两种数据库事务管理方式。

  • 编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager,通过硬编码的方式来管理数据库的事物。对于编程式事务管理,Spring推荐使用TransactionTemplate。
  • 声明式事务管理是建立在AOP(面向切面编程)之上的。其本质是对将要执行的SQL与语句进行拦截,然后在目标SQL开始之前创建或者加入一个事务,在执行完目标SQL之后根据执行情况提交或者回滚事务。声明式事务管理有两种常用的使用方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用。

       显然声明式事务管理要优于编程式事务管理,这也正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

       说了这么多,不如来点实际的,下面我们就来看看Spring推荐的声明式事物如何在我们的项目中使用。我将以注解和XML两种配置方式来一一为大家介绍。

2、搭建实验环境

首先新建一个Maven项目,导入需要的依赖:

<dependency>
    <!--单元测试-->
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.12</version>
	<scope>test</scope>
</dependency>
 <!--数据库驱动的依赖包-->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.11</version>
</dependency>

<!--Spring核心-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-core</artifactId>
	<version>${spring.version}</version>
	<exclusions>
		<exclusion>
			<groupId>commons-logging</groupId>
			<artifactId>commons-logging</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<!--Spring IOC容器-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-context</artifactId>
	<version>${spring.version}</version>
</dependency>
<!--Spring Bean工厂-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-beans</artifactId>
	<version>${spring.version}</version>
</dependency>
<!--SpEL(Spring表达式)-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-expression</artifactId>
	<version>${spring.version}</version>
</dependency>
<!--Spring面向切面编程-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aop</artifactId>
	<version>${spring.version}</version>
</dependency>
<!--提供对AspectJ的支持,以便可以方便的将面向切面的功能集成进IDE中-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>${spring.version}</version>
</dependency>
<!--aspectj的runtime包(必须)-->
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<version>1.9.1</version>
</dependency>
<!--aspectjweaver是aspectj的织入包(必须)-->
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
	<version>1.9.1</version>
</dependency>
<dependency>
	<groupId>cglib</groupId>
	<artifactId>cglib</artifactId>
	<version>3.1</version>
</dependency>
<!--Spring jdbc-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>${spring.version}</version>
</dependency>
<!--Spring 事物-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-tx</artifactId>
	<version>${spring.version}</version>
</dependency>
<!--Spring 映射-->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-orm</artifactId>
	<version>${spring.version}</version>
</dependency>

创建数据库和表:新建tx数据库,并在tx数据库中新建三张表account、book、book_stock。具体的数据库脚本如下:

/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`tx` /*!40100 DEFAULT CHARACTER SET gb2312 */;

USE `tx`;

/*Table structure for table `account` */

DROP TABLE IF EXISTS `account`;

CREATE TABLE `account` (
  `username` varchar(50) NOT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312;

/*Data for the table `account` */

insert  into `account`(`username`,`balance`) values ('Jerry',800),('Tom',100000);

/*Table structure for table `book` */

DROP TABLE IF EXISTS `book`;

CREATE TABLE `book` (
  `isbn` varchar(50) NOT NULL,
  `book_name` varchar(100) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  PRIMARY KEY (`isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312;

/*Data for the table `book` */

insert  into `book`(`isbn`,`book_name`,`price`) values ('ISBN-001','book01',100),('ISBN-002','book02',200),('ISBN-003','book03',300),('ISBN-004','book04',400),('ISBN-005','book05',500);

/*Table structure for table `book_stock` */

DROP TABLE IF EXISTS `book_stock`;

CREATE TABLE `book_stock` (
  `isbn` varchar(50) NOT NULL,
  `stock` int(11) DEFAULT NULL,
  PRIMARY KEY (`isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312;

/*Data for the table `book_stock` */

insert  into `book_stock`(`isbn`,`stock`) values ('ISBN-001',1000),('ISBN-002',2000),('ISBN-003',3000),('ISBN-004',4000),('ISBN-005',5000);

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

导入后应该是这样的三张表:

3、基于Annotation的声明式事务管理配置和使用

(1)首先配置数据源,这里使用的阿里的Druid数据连接池

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

<!--配置数据连接池-->
<bean id="DruidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
	<property name="username" value="${jdbc.username}"/>
	<property name="password" value="${jdbc.password}"/>
	<property name="url" value="${jdbc.url}"/>
	<property name="driverClassName" value="${jdbc.driver}"/>

	<!-- 配置初始化大小、最小、最大 -->
	<property name="initialSize" value="${jdbc.initialSize}"/>
	<property name="minIdle" value="${jdbc.minIdle}"/>
	<property name="maxActive" value="${jdbc.maxActive}"/>
	<!-- 配置获取连接等待超时的时间 -->
	<property name="maxWait" value="${jdbc.maxWait}"/>
	<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
	<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
	<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
	<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
	<property name="validationQuery" value="${jdbc.validationQuery}" />
	<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
	<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
	<property name="testOnReturn" value="${jdbc.testOnReturn}" />
	<!-- 打开PSCache,并且指定每个连接上PSCache的大小  如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。-->
	<property name="poolPreparedStatements" value="${jdbc.poolPreparedStatements}" />
	<property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.maxPoolPreparedStatementPerConnectionSize}" />
	<!-- 配置监控统计拦截的filters -->
	<property name="filters" value="${jdbc.filters}" />

</bean>

(2)数据库配置jdbc.properties如下:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/tx?useSSL=false
jdbc.username=root
jdbc.password=95162437
jdbc.initialSize=10
jdbc.minIdle=10
jdbc.maxActive=50
jdbc.maxWait=60000
jdbc.timeBetweenEvictionRunsMillis=60000
jdbc.minEvictableIdleTimeMillis=300000
jdbc.validationQuery=SELECT 'x' FROM DUAL
jdbc.testWhileIdle=true
jdbc.testOnBorrow=false
jdbc.testOnReturn=false
jdbc.poolPreparedStatements=true
jdbc.maxPoolPreparedStatementPerConnectionSize=20
jdbc.filters=wall,stat

(3)配置JdbcTemplate,这里出于学习的目的先使用JdbcTemplate。

<!--配置JdbcTemplate-->
<bean id="JdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <constructor-arg name="dataSource" value="#{DruidDataSource}"></constructor-arg>
</bean>

(4)配置事物管理器

<!--DataSourceTransactionManager适用于使用JDBC来操作数据库的场景-->
<bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" value="#{DruidDataSource}"></property>
</bean>

(5)配置开启注解事物管理

<!--开启注解事物管理-->
<tx:annotation-driven transaction-manager="TransactionManager"/>

完整的ApplicationContext.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:context="http://www.springframework.org/schema/context"
   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/context
	http://www.springframework.org/schema/context/spring-context.xsd
	http://www.springframework.org/schema/aop
	http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">


<context:component-scan base-package="com.xust.iot"/>
<!--引入外部文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>


<!--配置数据连接池-->
<bean id="DruidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
	<property name="username" value="${jdbc.username}"/>
	<property name="password" value="${jdbc.password}"/>
	<property name="url" value="${jdbc.url}"/>
	<property name="driverClassName" value="${jdbc.driver}"/>

	<!-- 配置初始化大小、最小、最大 -->
	<property name="initialSize" value="${jdbc.initialSize}"/>
	<property name="minIdle" value="${jdbc.minIdle}"/>
	<property name="maxActive" value="${jdbc.maxActive}"/>
	<!-- 配置获取连接等待超时的时间 -->
	<property name="maxWait" value="${jdbc.maxWait}"/>
	<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
	<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
	<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
	<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
	<property name="validationQuery" value="${jdbc.validationQuery}" />
	<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
	<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
	<property name="testOnReturn" value="${jdbc.testOnReturn}" />
	<!-- 打开PSCache,并且指定每个连接上PSCache的大小  如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。-->
	<property name="poolPreparedStatements" value="${jdbc.poolPreparedStatements}" />
	<property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.maxPoolPreparedStatementPerConnectionSize}" />
	<!-- 配置监控统计拦截的filters -->
	<property name="filters" value="${jdbc.filters}" />

</bean>

<!--配置JdbcTemplate-->
<bean id="JdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
	<constructor-arg name="dataSource" value="#{DruidDataSource}"></constructor-arg>
</bean>

<!--Spring事务控制-->
<!--1.配置事务管理器,本身事物管理器是需要我们写一个切面类通过AOP编程的方式来实现,但是Spring提供了我们就直接使用Spring提供的事物管理器-->
<bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" value="#{DruidDataSource}"/>
</bean>

<!--2.开启基于注解的事物管理-->
<!--属性transaction-manager就是事物管理器-->
<tx:annotation-driven transaction-manager="TransactionManager"></tx:annotation-driven>

<!--3.在需要事物的地方用@Transactional来告诉Spring这个地方需要进行事物管理-->
</beans>

       完成上面的操作后,我们就可以在需要事物管理的地方使用@Transactional注解标注在类或方法上,以此告诉Spring这里需要事物管理。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

  • @Transactional注解的属性总结
属性 需要的参数的类型 描述
value String 指定使用的事务管理器(必须)
propagation enum:Propagation 事务传播行为设置(可选)
isolation enum: Isolation 事务隔离级别设置(可选)
readOnly boolean 读写或只读事务,默认(false)读写
timeout int 事务超时时间设置,单位:秒
rollbackFor Class[],必须继承自Throwable 指定发生异常后哪些异常要回滚
rollbackForClassName String[],必须继承自Throwable 指定发生异常后哪些异常要回滚的异常类名字数组
noRollbackFor Class[],必须继承自Throwable 指定发生异常后哪些异常不回滚
noRollbackForClassName String[],必须继承自Throwable指定发生异常后哪些异常不回滚的异常类名字数组

使用示例
DAO层,直接使用JdbcTemplate操作数据库,写3个方法分别用于:更新用户余额、更新图书库存、以及查询价格,具体如下:

package com.xust.iot.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoimp {


    @Autowired
    JdbcTemplate jdbcTemplate;

    /**
     * 减库存
     */
    public void updateBookStock(Integer books,String isbn){
       String sql="update book_stock set stock=stock-? where isbn=?";
       jdbcTemplate.update(sql,books,isbn);
    }

    /**
     * 减用户对余额
     */
    public void updateUserBalance(Integer payment,String username){
       String sql="updatess account set balance=balance-? where username=?";
       jdbcTemplate.update(sql,payment,username);
    }

    /**
     * 查询图书的价格
     * @param isbn
     * @return
     */
    public Integer getBookPrice(String isbn){
       String sql="select price from book where isbn=?";
       return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
    }
}

业务层,写一个结账的方法,使用@Transactional告诉Spring要帮我们控制事务

package com.xust.iot.service;

import com.xust.iot.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BookService {

    @Autowired
    BookDao bookDao;


    /**
     * 模拟结账
     * @param username   用户姓名
     * @param isbn   图书编号
     * @param books  用户买了几本书
     */
    @Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
    public void cheockout(String username,String isbn,Integer books){

        //减库存
        bookDao.updateBookStock(books,isbn);

        Integer price=bookDao.getBookPrice(isbn);
        Integer payment=price*books;
        //减账户余额
        bookDao.updateUserBalance(payment,username);

    }
}

4、基于XML的声明式事务管理配置和使用

其他的步骤和使用注解一样,不同点的核心的在下面:

<!--基于XML的事物管理-->
<!--1.配置事务管理器:这个和使用注解时一样-->
<bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" value="#{DruidDataSource}"></property>
</bean>
<!--2.配置事物属性:使用<tx:advice>标签声明事物通知 -->
<tx:advice id="myTransaction" transaction-manager="TransactionManager">
	<!--事物属性-->
	<tx:attributes>
        <tx:method name="get*" propagation="REQUIRED" read-only="true"/>
        <tx:method name="cheockout" propagation="REQUIRED" isolation="REPEATABLE_READ"/>
	</tx:attributes>
</tx:advice>
<!--3.配置事物切入点,把事物切入点和事物属性关联起来-->
<aop:config>
	<aop:pointcut id="pointCut" expression="execution(* com.xust.iot.service.*.*(..))"/>
	<aop:advisor advice-ref="myTransaction" pointcut-ref="pointCut"/>
</aop:config>
  • 测试:在src/main/test包下新建测试类TxTest.java测试
package test;

import com.alibaba.druid.pool.DruidDataSource;
import com.xust.iot.service.BookService;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date;

public class TxTest {

    ApplicationContext ioc=new ClassPathXmlApplicationContext("ApplicationContext.xml");
    private JdbcTemplate jdbcTemplate=ioc.getBean(JdbcTemplate.class);

    @Test
    public void txTest(){
        BookService bookService=ioc.getBean(BookService.class);
        bookService.cheockout("Tom","ISBN-005",3);
        System.out.println("结账成功!");
    }

}

留言区

还能输入500个字符