在学习声明式事务之前,我们先回忆一下什么是事务:【什么是事务】。既然是事务,就免不了要操作数据库,Spring提供了JdbcTemplate,可以快捷的操作数据库。

一、JdbcTemplate

JdbcTemplate是Spring提供的进行数据访问的工具,想要使用JdbcTemplate,首先需要导入以下三个Jar包:

  1. spring-jdbc-5.2.6.RELEASE.jar
  2. spring-orm-5.2.6.RELEASE.jar // 对象关系映射
  3. spring-tx-5.2.6.RELEASE.jar // 事务

Jar包导入之后,怎么使用JdbcTemplate来操作数据库呢?

访问数据库,首先需要知道数据源,我们可以通过new的方式获取dataSource和JdbcTemplate,然后使用JdbcTemplate中的增删改查方法即可。

 public void test() throws SQLException, PropertyVetoException {
     JdbcTemplate jdbcTemplate = new JdbcTemplate();
     ComboPooledDataSource dataSource = new ComboPooledDataSource();
     dataSource.setUser("");
     dataSource.setPassword("");
     dataSource.setJdbcUrl("");
     dataSource.setDriverClass("");

     jdbcTemplate.setDataSource(dataSource);
 }

另外一种方式,就是在Spring容器中注册dataSource和JdbcTemplate,我们要使用JdbcTemplate的时候,直接去Spring容器中取,之后再使用JdbcTemplate中的增删改查方法来操作数据库。

<!--将数据库的连接信息抽取到jdbc.properties文件中,然后引用外部属性文件(需要使用context名称空间)-->
<!--加载外部配置文件 classpath:固定写法,表示引用类路径下的一个资源-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="testDB1" class="com.mchange.v2.c3p0.ComboPooledDataSource">
  <!--${key}:动态取出配置文件中某个key对应的值
            如果在配置文件中直接配置username,获取不到数据库的连接,因为username是Spring的key中的一个关键字
            所以我们一般加一个前缀:jdbc.username来加以区分
   -->
  <property name="user" value="${jdbc.username}"></property>
  <property name="password" value="${jdbc.password}"></property>
  <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
  <property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
  <property name="dataSource" ref="testDB1"></property>
</bean>

JdbcTemplate使用示例:

public Double queryBookPrice(String bookId){
    ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
    JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class, "jdbcTemplate");

    String sql = "SELECT t.price from tb_book t WHERE t.book_id = ?";
    return jdbcTemplate.queryForObject(sql,Double.class,bookId);
}

public void updateBookStock(String bookId,int number){
    ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
    JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class, "jdbcTemplate");

    String sql = "UPDATE tb_book_stock T SET T.stock = T.stock - ? WHERE T.book_id = ?";
    jdbcTemplate.update(sql,number,bookId);
}

JdbcTemplate知道怎么用就行,不做重点研究。

二、声明式事务概述

什么是编程式事务?什么是声明式事务?

编程式事务:顾名思义,编程式事务就是通过编码的方式实现事务控制,需要在程序中显式地调用事务管理的相关方法。
声明式事务:不需要编写复杂的事务控制代码,只需要声明一个方法是事务方法,然后Spring就会自动进行事务控制。

为什么声明了事务方法,Spring就可以进行事务控制了呢?

先来看一下如果使用原生的JDBC进行事务管理,我们是怎样处理的。

public void test(){   
    try{
        // 1. 获取连接
        // 2. 设置非自动提交
        // 3. 目标代码执行
        // 4. 提交
    }catch(Exception e){
        // 4. 回滚
    }finally{
        // 5. 关闭连接,释放资源
    }
}

这种编程式事务将事务管理的代码嵌入到业务方法中来控制事物的提交和回滚,每一个需要事务控制的地方,我们都需要写一遍事务控制的代码,很容易造成代码的冗余。

然后我们再来回忆一下Aop中的环绕通知的编写方式。

public static Object logAround() throws Throwable {

    try {
        // 这个相当于@Before
        System.out.println("[环绕前置]" + name + "开始执行,参数是" + Arrays.asList(args));

        // 这个方法就相当于method.invoke(obj,args),返回的值就是调用的目标方法的返回值
        proceed = pjp.proceed(args);

        // 这个相当于@AfterReturning
        System.out.println("[返回通知]" + name + "执行正常结束,结果是" + proceed);
    } catch (Exception e) {
        throw new RuntimeException(e);
        // 这个相当于@AfterThrowing
        System.out.println("[异常通知]" + name + "执行异常,异常信息是" + e);
    } finally {
        // 这个相当于@After
        System.out.println("[环绕后置]" + name + "最终结束");
    }
}

可以发现,环绕通知和最原始的编程式事务思路基本一致,并且事务管理代码是固定的,都是第一步获取连接,第二步设置非自动提交,第三步目标代码执行,第四步如果目标代码执行成功,提交事务;执行失败,回滚事务,最后一步关闭连接,释放资源。所以我们将事务管理代码的每一步作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring Aop框架实现声明式事务管理。

如果我们要自己写这个切面方法还是很复杂的,要考虑各种因素。所以Spring已经给我们提供了很多事务控制的切面,这些切面也叫事务管理器。根据不同的持久化层框架,Spring提供了不同的事务管理器,这些事务管理器就可以在目标方法前后进进行事务控制。

4. 声明式事务 - 图1

这些事务管理器要如何使用呢?(如何利用这些事务管理器为方法添加事务)

我们拿DataSourceTranscationManager举例:

  1. 配置事务管理器,让其进行事务控制,因为事务管理器是一个切面,所以需要导入面向切面编程的几个包 ```java — 基础版 spring-aop-5.2.6.RELEASE.jar spring-aspects-5.2.6.RELEASE.jar

— 增强版 com.springsource.org.aspectj.weaver-1.7.2.RELEASE.jar aopalliance-1.0-sources.jar cglib-nodep-javadoc-2.2.jar


2. 开启基于注解的事务控制模式
```xml
<!-- 数据源 -->
<bean id="testDB1" class="com.mchange.v2.c3p0.ComboPooledDataSource">
  <property name="user" value="${jdbc.username}"></property>
  <property name="password" value="${jdbc.password}"></property>
  <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
  <property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>

<!-- 1. 配置事务管理器让其进行事务控制:要导入面向切面编程的几个包-->
<bean id="sourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="testDB1"></property>
</bean>

<!-- 2. 开启基于注解的事务控制模式:依赖tx名称空间 -->
<tx:annotation-driven transaction-manager="sourceTransactionManager"/>
  1. 给事务方法加注解

    @Transactional
    public void checkOut(String userId, String bookId, int number) {
    
     // 减去图书库存
     bookDao.updateBookStock(bookId, number);
    
     // 减去用户余额
     Double bookPrice = bookDao.queryBookPrice(bookId);
     bookDao.updateUserBalance(userId, bookPrice * number);
    }
    

    三、Transactional

给事务方法加@Transactional注解,就代表声明了这个方法是事务方法,之后Spring就可以进行事务控制。但是@Transactional并不是表面这么简单,它还有很多的属性需要我们了解。

4. 声明式事务 - 图2

isolation:事务的隔离级别

先回忆一下什么是事务的隔离级别:【事务的隔离级别】

propagation:事务的传播行为

rollbackFor:让原本不回滚的异常(编译时异常)进行事务回滚
rollbackForClassName:让原本不回滚的异常进行事务回滚,写全类名
noRollbackFor:让原本回滚的异常(运行时异常)不进行事务回滚
noRollbackForClassName:让原本回滚的异常不进行事务回滚,写全类名

为什么需要这几个属性?

如下图,这个方法已经声明了事务,并且方法抛出了FileNotFound的异常,但是数据库的值依然被修改了,事务并没有回滚。这是为什么呢?

4. 声明式事务 - 图3

异常分类:
运行时异常(非检查异常):可以不用处理,默认都回滚
编译时异常(受检查异常):要么try-catch,要么在方法上声明throws,默认不回滚

所以上图中事务没有回滚是因为FileNotFound是一个编译时异常,事务默认发生运行时异常会回滚,发生编译时异常不会回滚。

有了这几个属性之后,可以让原来不回滚的异常进行事务回滚,让原本回滚的异常不进行事务回滚。

@Transactional(rollbackFor={FileNotFoundException.class})
public void checkOut(String userId, String bookId, int number) throws InterruptedException, FileNotFoundException {

    // 加上这个异常回滚之后,遇到FileNotFoundException异常,事务就会回滚
}

readOnly:设置事务为只读事务,默认值为false

@Transactional(readOnly = true)
public void checkOut(String userId, String bookId, int number) {

    // 当方法中全是对数据库的查询操作时,可以设置readOnly = true,可以跳过事务控制的其他操作,加快查询速度
}

如果方法中有增删改操作,但是事务设置readOnly = true,就会报错
4. 声明式事务 - 图4

timeout:事务超出指定执行时长后自动终止并回滚,以秒为单位

@Transactional(timeout = 3)
public void checkOut(String userId, String bookId, int number) {

   // 如果这个方法执行超过3秒,事务就会终止并回滚
}

4. 声明式事务 - 图5