Spring事务控制

一:事务介绍

1:什么是事务

  事务管理是程序开发中必不可少的技术,用来保证数据的完整性和一致性,它们被当作一个单独的工作单元。这些动作要么全部完成,要不就完全不起作用。就例如我们在完成转账操作的时候,转出用户的金额减少、转入用户的金额增加,这就完成了一个完整的操作,提交事务然后完成转账,但是如果有一方操作失败则全部失败,回滚数据。这就是事务,保证一组操作要么全部成功,要么全部失败。

2:事务四大属性

① 原子性(atomicity):
  事务是一个原子操作,有一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用
② 一致性(consistency):
  一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中
③ 隔离性(isolation):
  可能有许多事务会同时处理相同的数据,因此每个事物都应该与其他事务隔离开来,防止数据损坏
④ 持久性(durability):
  一旦事务完成被提交,它对数据库中的数据操作是永久性的,反之发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果被写到持久化存储器中

3:事务并发问题

一:脏读    
   介绍:一个事务读到了另一个事务的未提交的数据
      事务A对表中的一个数据更新了,但是还未来得及提交,因为事务A 中途是要对这个数据进行各种操作后才可最终提交的;
  恰巧这时候事务B来了(此时事务A更新了数据还没提交),事务B在读取表中的数据时会读到事务A未提交的数据,为了安全起见,
  事务A未提交最终数据按道理说其它事务是无法读取到事务A未提交的数据的 二:幻读 介绍:一个事务读到了另一个事务已经提交的insert的数据导致多次查询结果不一致;幻读只出现在insert和delete的时候   事务A正在查询当前表中的总记录数,此时事务A还没结束;恰巧事务B对表中增加了几条数据,并快速的提交了事务;此时事务A会查询到 事务B提交后的全部记录数总和;如原本10条数据,事务B增加了5条提交了事务,原本事务A只能查到10条,但是事务A却读到了事务B提交的
   5条数据,总共15条 三:不可重复读 介绍:一个事务读到了另一个事务已经提交的update的数据导致多次查询结果不一致;不可重复读出现在update的时候   事务A在查询表中的某个字段值为100,此时事务A并没有关闭(提交);恰巧事务B更改了事务A之前查询的数据,更改到了200并提交
   了事务,这时候事务A再次查询那个字段数据发现变成了200,这叫不可重复读,之前的100再也读不到了
四:串性化
    
指的是强制事务串行执行,其可以避免“幻象读”的问题出现,这是最严格的隔离级别了。因为串行化需要在发生竞争的数据上加锁,
  所以并发性能不高,只有在对数据一致性要求非常高且并发度不高的情况下才会考虑使用这种隔离级别。此策略解决了上面3种问题
补充: Oracle解决了脏读问题 MySQL解决了脏读和幻读问题

4:事务隔离级别

第一级别:Read Uncommitted(读取未提交内容)
    [ 脏读、幻读、不可重复读都可能发生 ]
        所有事务都可以看到其他未提交事务的执行结果,避免这种事情发生,一般不用此事务级别
第二级别:Read Committed(读取提交内容)
    [ 避免了脏读,但幻读、不可重复读都可能发生 ]
      这是大多数数据库系统的默认隔离级别如Oracle(除MySQL);它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
第三级别:Repeatable Read(可重读)
    [ 避免了脏读、幻读,但不可重复读可能发生 ]
      这是MySQL的默认事务隔离级别;它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
第四级别:Serializable(串行化)
    [ 最高级别,避免了上面一切问题 ]
      此设置不支持多线程,效率太慢

补充:
    查询MySQL事务隔离级别:select @@tx_isolation
    设置MySQL事务隔离级别:set session transaction isolation level 事务级别;

二:Spring中的事务管理分类

  作为企业级的应用程序框架,Spring在不同的事务管理API之上定义了一个抽象层。而开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring在分类上即支持编程式事务管理也支持声明式事务。

1:声明式事务管理

  Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
  声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于注解的方式),便可以将事务规则应用到业务逻辑中。因为事务管理本身就是一个典型的横切逻辑,正是 AOP 的用武之地。Spring 开发团队也意识到了这一点,为声明式事务提供了简单而强大的支持。

2:编程式事务管理

   所谓编程式事务指的是通过编码方式实现事务,允许用户在代码中精确定义事务的边界。即类似于JDBC编程实现事务管理。管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

三:Spring事务管理接口

Spring 事务管理为我们提供了三个高层抽象的接口,分别是PlatformTransactionManagerTransactionDefinitionTransactionStatus

1:PlatformTransactionManager事务管理器

  Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,Spring框架并不直接管理事务,而是通过这个接口为不同的持久层框架提供了不同的PlatformTransactionManager接口实现类,也就是将事务管理的职责委托给Hibernate或者iBatis等持久化框架的事务来实现。在没接触到Spring事务管理器之前我们都是自己定义事务管理器,里面定义各种事务方法,并且还要保证事务的Connection和Mybatis的Connection一致;现在使用Spring事务管理器,我们就可以不用自己写事务类等一系列操作了

注:PlatformTransactionManager只是事务管理器接口,针对不同的数据库操作框架应该使用不同的事务管理器子类
①:org.springframework.jdbc.datasource.DataSourceTransactionManager:

  使用JDBC或者iBatis进行持久化数据时使用 ②:org.springframework.orm.hibernate5.HibernateTransactionManager:
  使用hibernate5版本进行持久化数据时使用 ③:org.springframework.orm.jpa.JpaTransactionManager:
  使用JPA进行持久化数据时使用 ④:org.springframework.jdo.JdoTransactionManager:
  当持久化机制是jdo时使用 ⑤:org.springframework.transaction.jta.JtaTransactionManager:
  使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用
//接口源码
public
interface PlatformTransactionManager extends TransactionManager { //事务管理器通过TransactionDefinition,获得“事务状态”,从而管理事务 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; //根据状态提交 void commit(TransactionStatus var1) throws TransactionException; //根据状态回滚 void rollback(TransactionStatus var1) throws TransactionException; }

2:TransactionDefinition定义事务基本属性

  org.springframework.transaction.TransactionDefinition接口用于定义一个事务,它定义了Spring事务管理的五大属性:隔离级别传播行为是否只读事务超时回滚规则

 ①:隔离级别

  什么是事务的隔离级别?我们知道,隔离性是事务的四大特性之一,表示多个并发事务之间的数据要相互隔离,隔离级别就是用来描述并发事务之间隔离程度的大小
在并发事务之间如果不考虑隔离性,会引发如下安全性问题:
脏读 :一个事务读到了另一个事务的未提交的数据
不可重复读 :一个事务读到了另一个事务已经提交的 update 的数据导致多次查询结果不一致
幻读 :一个事务读到了另一个事务已经提交的 insert 的数据导致多次查询结果不一致

在 Spring 事务管理中,为我们定义了如下的隔离级别:
①:ISOLATION_DEFAULT:
    使用数据库默认的隔离级别
②:ISOLATION_READ_UNCOMMITTED:
    最低的隔离级别,允许读取已改变而没有提交的数据,可能会导致脏读、幻读或不可重复读
③:ISOLATION_READ_COMMITTED:
    允许读取事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
④:ISOLATION_REPEATABLE_READ:
    对同一字段的多次读取结果都是一致的,除非数据事务本身改变,可以阻止脏读和不可重复读,但幻读仍有可能发生
⑤:ISOLATION_SERIALIZABLE:
    最高的隔离级别,完全服从ACID的隔离级别,确保不发生脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

②:传播行为

   Spring事务传播机制规定了事务方法和事务方法发生嵌套调用时事务如何进行传播,即协调已经有事务标识的方法之间的发生调用时的事务上下文的规则

Spring定义了七种传播行为,这里以方法A和方法B发生嵌套调用时如何传播事务为例说明:
①:PROPAGATION_REQUIRED:(propagation_required)
    A如果有事务,B将使用该事务;如果A没有事务,B将创建一个新的事务
②:PROPAGATION_SUPPORTS:(propagation_supports)
    A如果有事务,B将使用该事务;如果A没有事务,B将以非事务执行
③:PROPAGATION_MANDATORY:(propagation_mandatory)
    A如果有事务,B将使用该事务;如果A没有事务,B将抛异常
④:PROPAGATION_REQUIRES_NEW:(propagation_requires_new)
    A如果有事务,将A的事务挂起,B创建一个新的事务;如果A没有事务,B创建一个新的事务
⑤:PROPAGATION_NOT_SUPPORTED:(propagation_not_supported)
    A如果有事务,将A的事务挂起,B将以非事务执行;如果A没有事务,B将以非事务执行
⑥:PROPAGATION_NEVER:(propagation_never)
    A如果有事务,B将抛异常;A如果没有事务,B将以非事务执行
⑦:PROPAGATION_NESTED:(propagation_nested)
    A和B底层采用保存点机制,形成嵌套事务

③:是否只读

  如果将事务设置为只读,表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务

④:事务超时

  事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。在 TransactionDefinition 中以 int 的值来表示超时时间,默认值是-1,其单位是秒

⑤:回滚操作

  回滚规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚

 ⑥:源码展示

public interface TransactionDefinition {
   //事务传播行为类型:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。
    int PROPAGATION_REQUIRED = 0;
    //事务传播行为类型:支持当前事务,如果当前没有事务,就以非事务方式执行。 
    int PROPAGATION_SUPPORTS = 1;
    //事务传播行为类型:当前如果有事务,Spring就会使用该事务;否则会抛出异常
    int PROPAGATION_MANDATORY = 2;
    //事务传播行为类型:新建事务,如果当前存在事务,把当前事务挂起。 
    int PROPAGATION_REQUIRES_NEW = 3;
    //事务传播行为类型:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 
    int PROPAGATION_NOT_SUPPORTED = 4;
    //事务传播行为类型:即使当前有事务,Spring也会在非事务环境下执行。如果当前有事务,则抛出异常
    int PROPAGATION_NEVER = 5;
    //事务传播行为类型:如果当前存在事务,则在嵌套事务内执行。
    int PROPAGATION_NESTED = 6;

   //隔离级别:默认的隔离级别(对mysql数据库来说就是ISOLATION_ READ_COMMITTED,可以重复读)
    int ISOLATION_DEFAULT = -1;
    //隔离级别:读未提交(最低)
    int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
    //隔离级别:读提交
    int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
    //隔离级别:可重复度
    int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
    //隔离级别:序列化操作(最高)
    int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;

    //默认事务的超时时间
    int TIMEOUT_DEFAULT = -1;

    //获取事务的传播行为
    int getPropagationBehavior();
    //获取事务的隔离级别
    int getIsolationLevel();
    //获取超时时间
    int getTimeout();
    
    //是否只读
    boolean isReadOnly();
    //事务名称
    String getName();
}
TransactionDefinition源码

3:TransactionStatus接口源码

   org.springframework.transaction.TransactionStatus接口用来记录事务的状态,该接口定义了一组方法,用来获取或判断事务的相应状态信息

public interface TransactionStatus extends SavepointManager, Flushable {
    boolean isNewTransaction();// 是否是新的事物
    boolean hasSavepoint();// 是否有恢复点
    void setRollbackOnly();// 设置为只回滚
    boolean isRollbackOnly();// 是否为只回滚
    void flush();// 刷新
    boolean isCompleted();// 是否已完成
}
//注:在高版本如5.2.6会发现没这么多方法,其实是被封装到父类了

四:Spring事务管理实现方式

  Spring 事务管理有两种方式:编程式事务管理、声明式事务管理。
  编程式事务管理通过TransactionTemplate手动管理事务,在实际应用中很少使用;
  声明式事务管理有三种实现方式:基于TransactionProxyFactoryBean的方式、基于AspectJ的XML方式、基于注解的方式。
  在声明式事务管理的三种实现方式中,基于TransactionProxyFactoryBean的方式需要为每个进行事务管理的类配置一个TransactionProxyFactoryBean对象进行增强,所以开发中很少使用;基于AspectJ的XML方式一旦在XML文件中配置好后,不需要修改源代码,所以开发中经常使用;基于注解的方式开发较为简单,配置好后只需要在事务类上或方法上添加@Transaction注解即可,所以开发中也经常使用
  我们以学生转账为例来学习这基于AspectJ的XML方式、基于注解的方式不同的实现方式

n:不带任何的事务且带转账普通代码   建表语句

<!--设置maven的代码编译环境 我设置JDK1.9-->
    <properties>
        <maven.compiler.source>1.9</maven.compiler.source>
        <maven.compiler.target>1.9</maven.compiler.target>
        <maven.compiler.compilerVersion>1.9</maven.compiler.compilerVersion>
    </properties>

    <dependencies>
        <!--mysql驱动坐标-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.32</version>
        </dependency>
        <!--c3p0连接池坐标-->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.2</version>
        </dependency>
        <!--junit单元测试坐标-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--Spring-context主要坐标-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--导入Spring的jdbc的操作 注集成mybatis要导入此包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring对mybatis的支持-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.1</version>
        </dependency>
        <!--导入mybatis坐标 注解sql语句需要-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--Spring-test测试坐标-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring对Aop支持-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--支持解析切入点表达式等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--支持AOP相关注解等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--JDK9废弃了一些注解,使用此坐标导入-->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
pom.xml
public class Student {
    private int sid;            //主键id
    private String sname;       //姓名
    private String ssex;        //性别
    private int sage;           //年龄
    private double scredit;     //学分
    private double smoney;      //零花钱
    private String saddress;    //住址
    private String senrol;      //入学时间
    //因为简单的单表CRUD就不涉及到外键
    //private int fid;            //外键 连接家庭表信息学生对家庭,一对一
    //private int tid;            //外键 连接老师信息 学生对老师,一对一
    //创建构造器/get/set/toString就不展示了
}
POJO实体类Student
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 学生业务接口
 * @version: v1.0.0
 */
public interface StudentService {
    //转账
    void transfer(String currentName, String targetName, double money);
}
/***************************************************/

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 学生业务接口实现
 * @version: v1.0.0
 */
@Service("studentService")
public class StudentServiceImpl implements StudentService {

    //创建StudentDao变量并注入
    @Autowired
    private  StudentDao studentDao;

    /**
     * 转账
     * @param currentName 当前学生姓名
     * @param targetName  目标学生姓名
     * @param money       转账金额
     */
    public void transfer(String currentName, String targetName, double money) {
        /*为了演示 我就不做那么细致的数据判断等等操作*/
        //①:查询学生
        Student currentStudent = studentDao.findByName(currentName);
        Student targetStudent = studentDao.findByName(targetName);
        //打印最初结果
        System.out.println("转账前: 当前学生姓名和余额:" + currentStudent.getSname() + "  " + currentStudent.getSmoney());
        System.out.println("转账前: 目标学生姓名和余额:" + targetStudent.getSname() + "  " + targetStudent.getSmoney());
        //②:开始转账
        currentStudent.setSmoney(currentStudent.getSmoney() - money);
        targetStudent.setSmoney(targetStudent.getSmoney() + money);

        //③:调用更新
        studentDao.update(currentStudent);
        //int a=1/0; //开启则会出现异常
        studentDao.update(targetStudent);

        //打印最终结果
        Student currentStudentEnd = studentDao.findByName(currentName);
        Student targetStudentEnd = studentDao.findByName(targetName);
        System.out.println("转账后: 当前学生姓名和余额:" + currentStudentEnd.getSname() + "  " + currentStudentEnd.getSmoney());
        System.out.println("转账后: 目标学生姓名和余额:" + targetStudentEnd.getSname() + "  " + targetStudentEnd.getSmoney());
    }
}
/***************************************************/

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 学生dao
 * @version: v1.0.0
 */
public interface StudentDao {
    //根据姓名查询
    @Select("select * from student where sname=#{name}")
    Student findByName(@Param("name") String name);

    //修改金额
    @Update("update student set smoney=#{stu.smoney} where sid=#{stu.sid}")
    void update(@Param("stu") Student student);
}
service/dao类编写
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/demo_school
jdbc.username=root
jdbc.password=123
jdbc.properties数据库连接参数
<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd ">

    <!--开启注解扫描-->
    <context:component-scan base-package="cn.xw"/>

    <!--导入jdbc数据库连接必要数据-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置c3p0连接池 DataSource数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--设置Mybatis工厂类bean对象-->
    <bean id="factoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--设置mybatis数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--配置别名-->
        <property name="typeAliasesPackage" value="cn.xw.pojo"/>
        <!--配置mybatis配置文件,如果有别的配置文件可以使用此标签导入-->
        <!--<property name="configuration" value="classpath:SqlMapConfig.xml"/>-->
    </bean>
    <!--配置mapper扫描映射类-->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.xw.dao"/>
    </bean>
</beans>
applicationContext.xml配置 即没有手写事务配置,也没使用Spring事务配置
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    @Autowired
    private StudentService studentService;

    @Test
    public void testTransfer() {
        studentService.transfer("王生安", "李鑫灏", 10);
    }
}
测试

   在写完这个不带事务的代码大家会发现,如果学过AOP,使用AOP手写事务通知类和切面也是可以事务控制,这个我再前一篇文章介绍过AOP的使用,原生写法可以使用普通代码控制、JDK动态代理和Cglib三种方式来控制事务,还有使用AOP来通过前置和后置通知来控制事务。但是下面我将为大家介绍一下Spring为我们提供的专门事务管理。这样我们可以解放我们的双手,再也不用自定义创建事务通知类了

1:基于AspectJ的XML方式完成事务(Spring+Mybatis)

   基于AspectJ的方式其实主要利用了AOP切面的原理,但是Spring为我们封装的事务更强大更灵活,我们就通过更改上面写好的代码来达到事务控制的效果

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
         http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--开启注解扫描-->
    <context:component-scan base-package="cn.xw"/>

    <!--导入jdbc数据库连接必要数据-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置c3p0连接池 DataSource数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--设置Mybatis工厂类bean对象-->
    <bean id="factoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--设置mybatis数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--配置别名-->
        <property name="typeAliasesPackage" value="cn.xw.pojo"/>
        <!--配置mybatis配置文件,如果有别的配置文件可以使用此标签导入-->
        <!--<property name="configuration" value="classpath:SqlMapConfig.xml"/>-->
    </bean>
    <!--配置mapper扫描映射类-->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.xw.dao"/>
    </bean>

    <!--上面代码只要改个约束,其它不动,主要添加下面一些代码-->
    <!-- spring中基于XML的声明式事务控制配置步骤
       1、配置Spring事务管理器
       2、配置事务的通知
               此时我们需要导入事务的约束 tx名称空间和约束,同时也需要aop的
               使用tx:advice标签配置事务通知
                   属性:
                       id:给事务通知起一个唯一标识
                       transaction-manager:给事务通知提供一个事务管理器引用 默认transactionManager
       3、配置AOP中的通用切入点表达式
       4、建立事务通知和切入点表达式的对应关系
       5、配置事务的属性
              是在事务的通知tx:advice标签的内部
    -->
    <!--++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-->

    <!--配置Spring自带的事务管理器  此时我使用的是Mybatis,所以要使用DataSourceTransactionManager实现类-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--配置数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--配置事务属性
               isolation:
                   用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
                        READ_UNCOMMITTED:第一级别:读取未提交内容
                        READ_COMMITTED:第二级别:读取提交内容
                        REPEATABLE_READ:第三级别:可重读
                        SERIALIZABLE:第四级别:串行化
               propagation: [具体请查看上面的隔离级别说明]
                   用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
               read-only:
                   用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
               timeout:
                   用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
               rollback-for:
                   用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。
               no-rollback-for:
                   用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。-->
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
        </tx:attributes>
    </tx:advice>

    <!--配置AOP切入点表达式-->
    <aop:config>
        <aop:pointcut id="pt1" expression="execution(* cn.xw.service.impl.*.*(..))"/>
        <!--我们使用Spring提供的事务通知的话就不用再配置<aop:aspect>来指定啥前/后/环绕啥的通知了-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/>
    </aop:config>
</beans>
<!--修改完上面的配置,其它代码不用更改即可达到事务控制的效果-->

2:基于注解方式完成事务(Spring+Mybatis)

   注解方式比基于AspectJ的方法要简单的多,可以不用导入aspectjweaver坐标,因为没用到解析切入点表达式,我就根据上面第n小结没有事务的原始代码进行简单的代码改造!

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

    <!--开启注解扫描-->
    <context:component-scan base-package="cn.xw"/>

    <!--导入jdbc数据库连接必要数据-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置c3p0连接池 DataSource数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--设置Mybatis工厂类bean对象-->
    <bean id="factoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--设置mybatis数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--配置别名-->
        <property name="typeAliasesPackage" value="cn.xw.pojo"/>
        <!--配置mybatis配置文件,如果有别的配置文件可以使用此标签导入-->
        <!--<property name="configuration" value="classpath:SqlMapConfig.xml"/>-->
    </bean>
    <!--配置mapper扫描映射类-->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.xw.dao"/>
    </bean>

    <!--其实我们只要配置下面的这2个就可以开启Spring事务了-->
    <!--配置Spring自带的事务管理器  此时我使用的是Mybatis,所以要使用DataSourceTransactionManager实现类-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--配置数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!---开启注解事务管理   内部添加一个事务管理器 默认名为transactionManager-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

我们把xml文件设置为开启注解事务管理器后,我们就可以针对某个要添加事务的方法或类添加事务了,只需要在上面设置一个注解即可开启

######逻辑Service实现类

@Service("studentService")
//配置注解@Transactional,这个注解针对该方法里面的全部方法都是这个配置 主要是限制查询没事务的
@Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
public class StudentServiceImpl implements StudentService {

    //创建StudentDao变量并注入
    @Autowired
    private  StudentDao studentDao;

    //转账方法
    //覆盖开头设置的@Transactional注解,这里因为要操作数据和需要事务支持 就重写配置了一下
    @Transactional(readOnly = false,propagation = Propagation.REQUIRED)
    public void transfer(String currentName, String targetName, double money) {
        //......省略转账逻辑代码
    }
}

注:Transactional注解里面有着各种参数设置,具体和我上面一个xml配置的参数一样

这种缺点是,如果该实现的业务类里面有很多增删改操作都要事务,那么都要重写配置一下事务的支持,不像xml方式,一次配置后面都不需要更改,如果使用纯注解则在主配置文件上添加下面红字注解:

@Configuration
@EnableTransactionManagement
public class SpringTxConfiguration {
//里面配置数据源,配置 JdbcTemplate,配置事务管理器。在之前的步骤已经写过了。
}

三:使用纯注解完成Spring内置事务(Spring+Mybatis)

  在接下来的案例我们使用纯注解完成对学生之间的转账,并且总结纯注解的IOC+AOP知识进行一个总结,在使用Spring内置事务管理器的同时又增加切面通知来打印具体的操作步骤,接下来就和我操作一下吧!

<!--设置maven的代码编译环境 我设置JDK1.9-->
    <properties>
        <maven.compiler.source>1.9</maven.compiler.source>
        <maven.compiler.target>1.9</maven.compiler.target>
        <maven.compiler.compilerVersion>1.9</maven.compiler.compilerVersion>
    </properties>

    <dependencies>
        <!--mysql驱动坐标-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.32</version>
        </dependency>
        <!--c3p0连接池坐标-->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.2</version>
        </dependency>
        <!--junit单元测试坐标-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--Spring-context主要坐标-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--导入Spring的jdbc的操作 注集成mybatis要导入此包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring对mybatis的支持-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.1</version>
        </dependency>
        <!--导入mybatis坐标 注解sql语句需要-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--Spring-test测试坐标-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring对Aop支持-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--支持解析切入点表达式等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--支持AOP相关注解等等-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!--JDK9废弃了一些注解,使用此坐标导入-->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
pom.xml
public class Student {
    private int sid;            //主键id
    private String sname;       //姓名
    private String ssex;        //性别
    private int sage;           //年龄
    private double scredit;     //学分
    private double smoney;      //零花钱
    private String saddress;    //住址
    private String senrol;      //入学时间
    //因为简单的单表CRUD就不涉及到外键
    //private int fid;            //外键 连接家庭表信息学生对家庭,一对一
    //private int tid;            //外键 连接老师信息 学生对老师,一对一
    //创建构造器/get/set/toString就不展示了
)
POJO实体类Student
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 学生业务接口
 * @version: v1.0.0
 */
public interface StudentService {
    //转账
    void transfer(String currentName, String targetName, double money);
}
//==================================================

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 学生业务接口实现
 * @version: v1.0.0
 */
@Service("studentService")
public class StudentServiceImpl implements StudentService {

    //创建StudentDao变量并注入
    @Autowired
    private StudentDao studentDao;

    /**
     * 转账
     *
     * @param currentName 当前学生姓名
     * @param targetName  目标学生姓名
     * @param money       转账金额
     */
    @Transactional(transactionManager = "dataSourceTransactionManager", propagation = Propagation.REQUIRED)
    public void transfer(String currentName, String targetName, double money) {
        /*为了演示 我就不做那么细致的数据判断等等操作*/
        //①:查询学生
        Student currentStudent = studentDao.findByName(currentName);
        Student targetStudent = studentDao.findByName(targetName);
        //打印最初结果
        System.out.println("转账前: 当前学生姓名和余额:" + currentStudent.getSname() + "  " + currentStudent.getSmoney());
        System.out.println("转账前: 目标学生姓名和余额:" + targetStudent.getSname() + "  " + targetStudent.getSmoney());
        //②:开始转账
        currentStudent.setSmoney(currentStudent.getSmoney() - money);
        targetStudent.setSmoney(targetStudent.getSmoney() + money);

        //③:调用更新
        studentDao.update(currentStudent);
        //int a=1/0;
        studentDao.update(targetStudent);

        //打印最终结果
        Student currentStudentEnd = studentDao.findByName(currentName);
        Student targetStudentEnd = studentDao.findByName(targetName);
        System.out.println("转账后: 当前学生姓名和余额:" + currentStudentEnd.getSname() + "  " + currentStudentEnd.getSmoney());
        System.out.println("转账后: 目标学生姓名和余额:" + targetStudentEnd.getSname() + "  " + targetStudentEnd.getSmoney());
    }
}
//==================================================

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5
 * @description: 学生dao
 * @version: v1.0.0
 */
public interface StudentDao {
    //根据姓名查询
    @Select("select * from student where sname=#{name}")
    Student findByName(@Param("name") String name);

    //修改金额
    @Update("update student set smoney=#{stu.smoney} where sid=#{stu.sid}")
    void update(@Param("stu") Student student);
}
service/dao类的编写
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/6
 * @description: Spring主配置文件 cn.xw.config.SpringConfig.java
 * @version: v1.0.0
 */
@Configuration                                                      //声明为配置文件
@ComponentScan(value="cn.xw")                                       //扫描组件加入容器
@Import(value={JdbcConfig.class,TransactionManagementConfig.class}) //加载其它自定义配置文件
@EnableAspectJAutoProxy                                             //开启AOP切面通知注解
@EnableTransactionManagement                                        //开启Spring事务管理器注解
public class SpringConfig {
    //Spring主配置一般引用其它class配置类,而子配置类可以不加@Configuration    
}

//###########################################################

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/6
 * @description: 关于Mybatis创建加入容器和数据源的设置 cn.xw.config.JdbcConfig.java
 * @version: v1.0.0
 */
public class JdbcConfig {

    //创建数据源DataSource并放入IOC容器
    @Bean("dataSource")
    public DataSource createDataSource() {
        ComboPooledDataSource dataSource = null;
        try {
            dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass("com.mysql.jdbc.Driver");
            dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo_school");
            dataSource.setUser("root");
            dataSource.setPassword("123");
        } catch (PropertyVetoException e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    //创建Mybatis工厂类bean对象放入IOC容器
    @Bean("sqlSessionFactory")
    public SqlSessionFactoryBean createSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        //设置数据源
        factoryBean.setDataSource(dataSource);
        //设置扫描的实体类映射 设置别名
        factoryBean.setTypeAliasesPackage("cn.xw.pojo");
        return factoryBean;
    }

    //配置mapper扫描映射类 就是告诉spring哪些dao是和Mybatis一样映射的
    @Bean("mapperScannerConfigurer")
    public MapperScannerConfigurer createMapperScannerConfigurer() {
        MapperScannerConfigurer scannerConfigurer = new MapperScannerConfigurer();
        scannerConfigurer.setBasePackage("cn.xw.dao");
        return scannerConfigurer;
    }
}

//###########################################################

/**
 * @author: xiaofeng
 * @date: Create in 2020/12/6
 * @description: 关于事务管理器的创建放入容器 cn.xw.config.TransactionManagementConfig.java
 * @version: v1.0.0
 */
public class TransactionManagementConfig {

    //创建Spring事务管理器 这里我返回类型是父接口 多态使用
    @Bean("dataSourceTransactionManager")
    public PlatformTransactionManager createPlatformTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        //设置数据源
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}
Spring的主要配置使用类配置
/**
 * @author: xiaofeng
 * @date: Create in $DATE
 * @description: 日志打印描述
 * @version: v1.0.0
 */
@Component("springAopLog")
@Aspect
public class SpringAopLog {

    //AOP通知切面表达式
    @Pointcut(value = "execution(* cn.xw.service.impl.*.*(..))")
    public void pointcut() {
    }

    //@Before("pointcut()")
    public void beforeLog() {
        System.out.println(Thread.currentThread().getName() + " log:方法执行开始");
    }

    //@AfterReturning("pointcut()")
    public void afterReturnLog() {
        System.out.println(Thread.currentThread().getName() + " log:方法执行成功");
    }

    //@AfterThrowing("pointcut()")
    public void afterThrowingLog() {
        System.out.println(Thread.currentThread().getName() + " log:方法执行异常");
    }

    //@After("pointcut()")
    public void afterLog() {
        System.out.println(Thread.currentThread().getName() + " log:完成方法操作");
    }

    @Around("pointcut()")       //注解使用环绕通知
    public Object aroundPrint(ProceedingJoinPoint pp) {
        Object obj = null;
        try {
            beforeLog();
            Object[] args = pp.getArgs();
            obj = pp.proceed(args);
            afterReturnLog();
        } catch (Throwable e) {
            afterThrowingLog();
            throw new RuntimeException("运行错误");
        } finally {
            afterLog();
        }
        return obj;
    }
}
配置日志打印描述SpringAopLog
/**
 * @author: xiaofeng
 * @date: Create in 2020/12/5 15:35
 * @description: 测试类
 * @version: v1.0.0
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class Client {
    @Autowired
    private StudentService studentService;

    @Test
    public void testTransfer() {
        studentService.transfer("王生安", "李鑫灏", 10);
    }
}
测试

总结:总的来说使用AspectJ加XML要方便很多,一次配置后面就一直使用,但是使用纯注解是趋势,但是纯xml肯定不推荐

propagation_required
posted @ 2020-05-12 19:36  蚂蚁小哥  阅读(450)  评论(0编辑  收藏  举报