因为很多系统都包含事务,所以Spring提供了相关的Api,属于AOP切面编程的二次封装
那么什么是事务(Transaction)呢?
事务在执行过程中包含哪些过程呢?
对于一个基本的事务有哪些特性呢?
MVC 三层架构逻辑图:
在数据库中建表 t_act2 , 并初始化两条数据用于我们转账【自定义即可】
我们的项目模块中创建 MVC 三层架构的包,因为我们这是一个Jar工程,所以通过测试模拟表现层 【给出目录结构】
修改我们的pom.xml 文件,确定打包方式和配置我们需要的依赖
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!--依赖-->
<dependencies>
<!--spring context-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--德鲁伊连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<!--@Resource注解-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
创建我们的pojo类,对应封装数据库表中的字段 Account.java
package com.powernode.bank.pojo;
/**
* @author Bonbons
* @version 1.0
*/
public class Account {
private String actno;
private Double balance;
public Account() {
}
public Account(String actno, Double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "User{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
}
编写我们持久层的接口 AccountDao 【提供对数据库操作的接口,因为我们只实现转账功能,所以只需要查询和更新操作】
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
/**
* @author Bonbons
* @version 1.0
*/
public interface AccountDao {
// 更新
int update(Account act);
// 查询
Account selectByActno(String actno);
}
编写我们dao层接口的实现类 AccountDaoImpl.java
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
/**
* @author Bonbons
* @version 1.0
*/
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
/*
我们采用Spring提供的JDBCTemplate来完成操作
*/
// 创建JdbcTemplate的对象
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public int update(Account act) {
// 编写sql语句
String sql = "update t_act2 set balance = ? where actno = ?";
// 调用我们的更新方法 >> sql语句、参数列表
int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
return count;
}
@Override
public Account selectByActno(String actno) {
// 编写查询的sql
String sql = "select actno, balance from t_act2 where actno = ?";
// 调用我们查询一条语句的方法
Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
return account;
}
}
为了降低MVC层与层之间的耦合度,我们在每层之间都提供接口,编写我们的业务层接口 AccountService
package com.powernode.bank.service;
/**
* @author Bonbons
* @version 1.0
*/
public interface AccountService {
/**
* 转账业务
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 具体金额
*/
public void transfer(String fromActno, String toActno, Double money);
}
编写我们的接口实现类 AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* @author Bonbons
* @version 1.0
*/
@Service("accountService")
public class AccountServiceImpl implements AccountService {
// 需要我们持久层的对象
@Resource(name = "accountDao")
private AccountDao accountDao;
// 转账业务
@Override
public void transfer(String fromActno, String toActno, Double money) {
// 查询转出账户
Account fromAccount = accountDao.selectByActno(fromActno);
// 判断余额是否充足
if(fromAccount.getBalance() < money){
/*
System.out.println("余额不足");
// return直接结束事务
return;
*/
// 使用抛出异常来代替这部分的代码
throw new RuntimeException("余额不足");
}
// 获取我们接收账户的信息
Account toAccount = accountDao.selectByActno(toActno);
// 在内存中更新余额信息
toAccount.setBalance(toAccount.getBalance() + money);
fromAccount.setBalance(fromAccount.getBalance() - money);
// 将更新后的转入账户对象调用update方法更新到我们的数据库中
int updateCount = 0;
updateCount += accountDao.update(fromAccount);
updateCount += accountDao.update(toAccount);
// 查看我们是否更新成功
// System.out.println(updateCount == 2 ? "转账成功" : "转账失败");
// 此段代码还是采用抛出异常的方式代替
if(updateCount != 2){
throw new RuntimeException("转账失败,请联系银行工作人员!!!");
}
}
}
编写我们的配置文件 spring.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"
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">
<!--context扫描包自动创建Bean的实例-->
<context:component-scan base-package="com.powernode.bank" />
<!--我们自动扫描Bean是针对我们自己写好的类,但是我们JdbcTemplate类不是我们自己写的,所以就需要手动声明-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--这个jdbc模板需要我们配置一个数据源的属性-->
<property name="dataSource" ref="dataSource" />
</bean>
<!--因为我们使用的是德鲁伊的数据源,需要传入数据库的信息,所以还需要手动配一下-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!--连接数据库的四个属性-->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/powernode" />
<property name="username" value="root" />
<property name="password" value="111111" />
</bean>
</beans>
编写我们的测试程序
package com.powernode.bank.test;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.service.impl.AccountServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Bonbons
* @version 1.0
*/
public class BankTransferTest {
// 对我们转账功能的测试
@Test
public void testTransfer(){
// 先解析XML文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
// 获取我们业务类的Bean
AccountService accountService = applicationContext.getBean("accountService", AccountServiceImpl.class);
try {
// 调用我们的转账方法 [因为可能发生异常,所以我们执行事务的这个语句需要添加异常处理]
accountService.transfer("act-001", "act-002", 1000.0);
System.out.println("转账成功!");
}catch (Exception e){
e.printStackTrace();
}
}
}
根据程序运行结果以及数据库表中数据的变化,我们可以得知转账操作成功
我们模拟了一个空指针异常,先看一下测试的运行结果
确实捕获到了空指针异常,接下来看一下数据库表中的信息是否发生了变化
Spring 实现了事务的两种方式:
Spring 专门对事务开发了一套AOP,属于面向切面编程,有一个Spring事务管理器的核心接口 PlatformTransactionManager
针对上述案例存在的问题,我们采用注解式事务演示如何处理
需要我们为配置文件添加内容:tx的命名空间、事务管理器的生命、开启注解事务的功能【我直接给出完整的配置文件】
<?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 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/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--context扫描包自动创建Bean的实例-->
<context:component-scan base-package="com.powernode.bank" />
<!--我们自动扫描Bean是针对我们自己写好的类,但是我们JdbcTemplate类不是我们自己写的,所以就需要手动声明-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--这个jdbc模板需要我们配置一个数据源的属性-->
<property name="dataSource" ref="dataSource" />
</bean>
<!--因为我们使用的是德鲁伊的数据源,需要传入数据库的信息,所以还需要手动配一下-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!--连接数据库的四个属性-->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/powernode" />
<property name="username" value="root" />
<property name="password" value="111111" />
</bean>
<!--配置事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--因为事务管理器需要连接对象,所以我们要配置数据源-->
<property name="dataSource" ref="dataSource" />
</bean>
<!--我们要通过事务管理器的命名空间 tx, 开启事务注解管理器-->
<tx:annotation-driven transaction-manager="txManager" />
</beans>
然后在我们的业务类中添加我们的事务注解 @Transactional
如果将注解添加到了我们的具体的方法上,代表这个方法将是一个完整的事务
如果直接将注解添加到了类上,等同于为这个类中的所有方法都添加了注解因为我们这个业务类只有一个方法,所以我就直接添加到 transfer 方法上了
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Bonbons
* @version 1.0
*/
@Service("accountService")
public class AccountServiceImpl implements AccountService {
// 需要我们持久层的对象
@Resource(name = "accountDao")
private AccountDao accountDao;
// 转账业务
@Override
@Transactional
public void transfer(String fromActno, String toActno, Double money) {
// 查询转出账户
Account fromAccount = accountDao.selectByActno(fromActno);
// 判断余额是否充足
if(fromAccount.getBalance() < money){
/*
System.out.println("余额不足");
// return直接结束事务
return;
*/
// 使用抛出异常来代替这部分的代码
throw new RuntimeException("余额不足");
}
// 获取我们接收账户的信息
Account toAccount = accountDao.selectByActno(toActno);
// 在内存中更新余额信息
toAccount.setBalance(toAccount.getBalance() + money);
fromAccount.setBalance(fromAccount.getBalance() - money);
// 将更新后的转入账户对象调用update方法更新到我们的数据库中
int updateCount = 0;
updateCount += accountDao.update(fromAccount);
// 我们模拟空指针异常
String s = null;
s.length();
updateCount += accountDao.update(toAccount);
// 查看我们是否更新成功
// System.out.println(updateCount == 2 ? "转账成功" : "转账失败");
// 此段代码还是采用抛出异常的方式代替
if(updateCount != 2){
throw new RuntimeException("转账失败,请联系银行工作人员!!!");
}
}
}
重新执行我们的测试程序 >> 发生了异常,但是此次没有更新我们数据库表的信息
我们去掉空指针异常,测试是否可以成功转账 >> 测试成功
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.transaction.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
String timeoutString() default "";
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.transaction.annotation;
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
很明显这是枚举类型,有七个变量,那么这些变量又分别代表什么含义呢?
我的一些理解:
在事务注解中声明传播行为的语法:@Transactional(propagation = Propagation.REQUIRED)
我们结合上面的案例编写程序测试一下这个传播行为究竟是怎么个事儿
为了方便查看程序的执行流程,我们引入Log4J查看日志信息 >> 导入依赖和配置文件
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
这个 Log4j 的核心配置文件推荐放在根目录下,日志级别我们设置为 DEBUG
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<loggers>
<!--
level指定日志级别,从低到高的优先级:
ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
-->
<root level="DEBUG">
<appender-ref ref="spring6log"/>
</root>
</loggers>
<appenders>
<!--输出日志信息到控制台-->
<console name="spring6log" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
</console>
</appenders>
</configuration>
第一步修改,我们为 AccountDao 接口添加新方法 insert() 为了向数据库中添加新账户
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
/**
* @author Bonbons
* @version 1.0
*/
public interface AccountDao {
// 更新
int update(Account act);
// 查询
Account selectByActno(String actno);
// 插入
int insert(Account act);
}
第二步修改,在我们接口的实现类中要实现对应的方法,也就是在 AccountDaoImpl中
@Override
public int insert(Account act) {
String sql = "insert into t_act2 values (?, ?)";
int count = jdbcTemplate.update(sql, act.getActno(), act.getBalance());
return count;
}
第三步修改,在我们银行业务方法中添加一个 save 方法,调用我们持久层对象的 inset 方法
package com.powernode.bank.service;
import com.powernode.bank.pojo.Account;
/**
* @author Bonbons
* @version 1.0
*/
public interface AccountService {
/**
* 转账业务
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 具体金额
*/
public void transfer(String fromActno, String toActno, Double money);
/**
* 保存账户信息
* @param act 账户
*/
void save(Account act);
}
第四步修改,我们需要在业务层接口的实现类AccountServiceImpl中实现我们新加入的方法,为了调用其他的业务我们添加一个接口的实现类
// 先给出的是我们新增的接口实现类 AccountServiceImpl2
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Bonbons
* @version 1.0
*/
@Service("accountService2")
public class AccountServiceImpl2 implements AccountService {
@Override
public void transfer(String fromActno, String toActno, Double money) {
// 不使用transfer方法
}
// 需要创建操作数据库的对象
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
@Transactional(propagation = Propagation.REQUIRES)
public void save(Account act) {
accountDao.insert(act);
}
}
此处给出的是我们修改后的业务接口原实现类,我们只是想研究一下传播行为,所以我们不用管上面的 transfer 方法
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Bonbons
* @version 1.0
*/
@Service("accountService")
public class AccountServiceImpl implements AccountService {
// 需要我们持久层的对象
@Resource(name = "accountDao")
private AccountDao accountDao;
// 转账业务
@Override
@Transactional
public void transfer(String fromActno, String toActno, Double money) {
// 查询转出账户
Account fromAccount = accountDao.selectByActno(fromActno);
// 判断余额是否充足
if(fromAccount.getBalance() < money){
/*
System.out.println("余额不足");
// return直接结束事务
return;
*/
// 使用抛出异常来代替这部分的代码
throw new RuntimeException("余额不足");
}
// 获取我们接收账户的信息
Account toAccount = accountDao.selectByActno(toActno);
// 在内存中更新余额信息
toAccount.setBalance(toAccount.getBalance() + money);
fromAccount.setBalance(fromAccount.getBalance() - money);
// 将更新后的转入账户对象调用update方法更新到我们的数据库中
int updateCount = 0;
updateCount += accountDao.update(fromAccount);
// 我们模拟空指针异常
String s = null;
s.length();
updateCount += accountDao.update(toAccount);
// 查看我们是否更新成功
// System.out.println(updateCount == 2 ? "转账成功" : "转账失败");
// 此段代码还是采用抛出异常的方式代替
if(updateCount != 2){
throw new RuntimeException("转账失败,请联系银行工作人员!!!");
}
}
// 创建我们第二个Service的对象
@Resource(name = "accountService2")
private AccountService accountService;
@Override
@Transactional(propagation = Propagation.REQUIRED) // 默认传播行为就是这个
public void save(Account act) {
// 在本方法中也插入数据
accountDao.insert(act);
// 创建我们待插入的对象
Account account = new Account("act-004", 500.0);
// 调用另一个事务的save方法
accountService.save(account);
}
}
接下来就可以编写我们的测试程序了
// 对我们嵌套调用事务,事务的传播级别为 Required 的测试
@Test
public void testPropagation(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
// 获取我们第一个业务的实例
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
// 创建我们第一个方法要插入的对象
Account account = new Account("act-003", 1000.0);
accountService.save(account);
}
我们在被调用的那个save方法写一个空指针异常
通过测试结果知道,这两个save操作都回滚了【实际上是一个事务】
@Transactional(propagation = Propagation.REQUIRES_NEW)
通过运行结果我们可以得知,这是两个不同的事务,如果将第二个事务异常捕获,并不会影响第一个事务的执行,所以第一个事务正常提交
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.transaction.annotation;
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
【默认为数据库的隔离级别】第一个业务类完成数据查询的操作,还是针对我们的银行账户进行操作 IsolationService1
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Bonbons
* @version 1.0
*/
@Service("i1")
public class IsolationService1 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 事务i1负责查询,事务的隔离级别为读未提交:就是为了演示能读到还没有提交的数据
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void selectById(String actno){
// 利用我们操作数据库的对象,调用对应查询的方法
Account account = accountDao.selectByActno(actno);
// 此处我们直接输出查询到的数据
System.out.println(account);
}
}
第二个业务负责想数据库中插入数据 IsolationService2
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Bonbons
* @version 1.0
*/
@Service("i2")
public class IsolationService2 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 事务i2负责插入数据,因为与读操作无关,我们就无需设置隔离级别
@Transactional
public void save(Account act){
try{
int count = accountDao.insert(act);
// 我们采用让进程睡眠的方式来模拟插入数据了,但还没有提交
Thread.sleep(1000 * 20);
// 输出一下插入数据成功的条数
System.out.println(count);
}catch (Exception e){
e.printStackTrace();
}
}
}
编写我们保存数据的测试程序 testIsolation1
// 插入数据20s后提交
@Test
public void testIsolation1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
// 创建一个我们要插入的数据
Account account = new Account("act-111", 1888.88);
i2.save(account);
}
编写我们的查询数据的测试程序 testIsolation2
// 演示读未提交的事务隔离级别,我们直接查找上面那个插入的数据
@Test
public void testIsolation2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService1 i1 = applicationContext.getBean("i1", IsolationService1.class);
i1.selectById("act-111");
}
操作方法,先运行我们的测试程序1,然后运行我们的测试程序2【在20s之内】
我们可以看到,当插入数据的程序未运行完,我们就已经获取到了这个用户的信息
我们将查询数据的隔离级别更改为读已提交再次测试,看看是否还能读取到数据
通过结果可以看到,此时已经无法获得未提交的数据了
@Transactional(timeout = 10)
【参数默认为-1代表不设时间限制,也可以手动指定为-1】我们依旧使用上面插入银行账户信息的业务 IsoactionService2 的方法
@Transactional(timeout = 10)
public void save(Account act){
try{
int count = accountDao.insert(act);
// 我们采用让进程睡眠的方式来模拟插入数据了,但还没有提交
Thread.sleep(1000 * 20);
// 输出一下插入数据成功的条数
System.out.println(count);
}catch (Exception e){
e.printStackTrace();
}
}
通过数据库表中的信息我们可以知道,插入数据成功了,虽然说这个事务超时了,但是在我们预设的时间10s之后没有待执行的DML【增删改】
为了演示超时后还有未执行的DML语句,我们交换睡眠进程和插入操作语句的位置 【我们把待插入的记录删除】
@Transactional(timeout = 10)
public void save(Account act){
try{
// 我们采用让进程睡眠的方式来模拟插入数据了,但还没有提交
Thread.sleep(1000 * 20);
// 位置交换了 >> 预期是没有插入成功,因为事务发生了回滚
int count = accountDao.insert(act);
// 输出一下插入数据成功的条数
System.out.println(count);
}catch (Exception e){
e.printStackTrace();
}
}
@Transactional(readOnly = true)
@Transactional(rollbackFor = RuntimeException.class)
@Transactional(noRollbackFor = NullPointerException.class)
package com.powernode;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
/**
* @author Bonbons
* @version 1.0
*/
@Configuration // 声明这是一个配置类
@ComponentScan("com.powernode.bank") // 声明我们扫描哪些包[主要就是扫描类上有没有声明Bean的注解然后创建实例]
@EnableTransactionManagement // 代表开启事务支持 >> 就是我们配置文件中的注解事务
public class Spring6Config {
// 因为有些Bean需要我们配置类在哪里,在配置文件中我们直接给出类的全限定类名和我们要传递的属性
// 但是在配置类中,我们只能自己创建Bean然后Spring调用方法获得后纳入到Spring容器中
/*
(1)此处我们手动创建了三个Bean >> 连接池、JDK模板、事务管理
(2)我们想使用事务管理器,不但要创建事务管理器的Bean,还要开启根据注解识别事务的功能[因为此处我们采用的是注解式开发]
*/
@Bean("dataSource")
public DruidDataSource getDataSource(){
// 创建德鲁伊连接池的对象
DruidDataSource druidDataSource = new DruidDataSource();
// 对于数据库连接的四个属性我们通过对象的set方法进行传递
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/powernode");
druidDataSource.setUsername("root");
druidDataSource.setPassword("111111");
// 返回我们的数据库连接池
return druidDataSource;
}
@Bean("jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
// 创建JDK模板类的对象
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// 将数据源传递给模板类
jdbcTemplate.setDataSource(dataSource);
// 返回Bean的实例
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
我们将配置文件替换为配置类之后,再获取Sprng容器的时候,不采用类路径加载的方式,而是采用下面的语法格式
ApplicationContext applicationContext1 = new AnnotationConfigApplicationContext("配置类的全限定类名");
在前面我们的依赖基础上还需要添加 aspect 的依赖,因为我们需要手动配置切片了
<!--aspectj依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>
具体的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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
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/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">
<context:component-scan base-package="com.powernode.bank"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置通知-->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="del*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="transfer*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
</tx:attributes>
</tx:advice>
<!--配置切面-->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.powernode.bank.service..*(..))"/>
<!--切面 = 通知 + 切点-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans>
编写我们的测试程序
@Test
public void testTransferXml(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try {
accountService.transfer("act-001", "act-002", 10000);
System.out.println("转账成功");
} catch (Exception e) {
e.printStackTrace();
}
}