系列文章索引:

  1. Spring事务Transactional和动态代理(一)-JDK代理实现
  2. Spring事务Transactional和动态代理(二)-cglib动态代理
  3. Spring事务Transactional和动态代理(三)-事务失效的场景

一. Spring事务分类

Spring 提供了两种事务管理方式:声明式事务管理和编程式事务管理。

1.1编程式事务

在 Spring 出现以前,编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中显式调用 beginTransaction()、commit()、rollback() 等事务管理相关的方法,这就是编程式事务管理。

简单地说,编程式事务就是在代码中显式调用开启事务、提交事务、回滚事务的相关方法。

1.2声明式事务

Spring 的声明式事务管理是建立在 Spring AOP 机制之上的,其本质是对目标方法前后进行拦截,并在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。而Spring 声明式事务可以采用 基于 XML 配置基于注解 两种方式实现

简单地说,声明式事务是编程式事务 + AOP 技术包装,使用注解进行扫包,指定范围进行事务管理。

本文内容是使用SpringBoot的开发的“基于注解”申明式事务管理,示例代码:https://github.com/qizhelongdeyang/SpringDemo

二. @Transacational实现机制

在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,如下图中所示调用者 Caller 并不是直接调用的目标类上的目标方法(Target Method),而是

调用的代理类(AOP Proxy)。

根据 @Transactional 的属性配置信息,这个代理对象(AOP Proxy)决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截。在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务

三. @Transacational失效

在开发过程中,可能会遇到使用 @Transactional 进行事务管理时出现失效的情况,本文中代码请移步https://github.com/qizhelongdeyang/SpringDemo查看,其中建了两张表table1和table2都只有一个主键字段,示例都是基于两张表的插入来验证的,由表id的唯一性能来抛出异常。如下mapper:

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("table1")
public class Table1Entity implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
} @Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("table2")
public class Table2Entity implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
} public interface Table1Mapper extends BaseMapper<Table1Entity> {
} public interface Table2Mapper extends BaseMapper<Table2Entity> {
}

3.1 底层数据库引擎不支持事务

并非所有的数据库引擎都支持事务操作,如在MySQL下面,InnoDB是支持事务的,但是MyISAM是不支持事务的。在Spring事务操作中,如果底层表的创建是基于MyISAM引擎创建,那么事务@Transactional 就会失效

3.2 标注修饰无效

因为Spring AOP有两种实现方式:JDK(Spring事务Transactional和动态代理(一)-JDK代理实现)和cglib( Spring事务Transactional和动态代理(二)-cglib动态代理),所以在标注修饰失效的时候也有两种不能情况,如下:

1) 接口JDK动态代理

Spring AOP对于接口-实现类这种方式是基于JDK动态代理的方式实现的。这种方式除了实现自接口的非static方法,其他方法均无效。

由于接口定义的方法是public的,java要求实现类所实现接口的方法必须是public的(不能是protected,private等),同时不能使用static的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施AOP增强,也即不能进行Spring事务增强

如下代码:

public interface IJdkService {
//非静态方法
public void jdkPublic(Integer id1,Integer id2); //接口中的静态方法必须有body
public static void jdkStaticMethod(Integer id1,Integer id2){
System.out.println("static method in interface");
}
} @Service
public class JdkServiceImpl implements IJdkService {
@Autowired
private Table1Mapper table1Mapper; @Autowired
private Table2Mapper table2Mapper; @Transactional(rollbackFor = Exception.class)
@Override
public void jdkPublic(Integer id1, Integer id2) {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity); Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
} //@Override 编译错误,方法不会覆写父类的方法
@Transactional(rollbackFor = Exception.class)
public static void jdkStaticMethod(Integer id1,Integer id2){
System.out.println("static method in implation");
}
}

上面代码中jdkPublic事务可以正常回滚,

而IJdkService中定义的jdkStaticMethod属于静态方法,调用不能通过@Autowired注入的方式调用,只能通过IJdkService.jdkStaticMethod调用,所以定义到实现类中的事务方法根本就不会被调用。

1) cglib动态代理

对于普通@Service注解的类(未实现接口)并通过 @Autowired直接注入类的方式,是通过cglib动态代理实现的。

cglib字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行AOP增强植入的,由于使用final,static,private修饰符的方法都不能被子类复写,所以这些方法将不能被实施的AOP增强。即除了public的非final的实例方法,其他方法均无效。

如下定义了@Service注解的CglibTranService,并使用@Autowired注入,测试事务能够回滚

@Service
public class CglibTranService {
@Autowired
private Table1Mapper table1Mapper; @Autowired
private Table2Mapper table2Mapper; @Transactional(rollbackFor = Exception.class)
public void testTran(Integer id1, Integer id2) {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity); Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}
}

对于使用final修饰大的方法无法回滚事务的原因是:所注入的table1Mapper和table2Mapper会为null(为空的原因在系列文章后面会有分析),所以到table1Mapper.insert这行代码会抛出NullPointerException

而static修饰的方法就会变为类变量,因为JDK的限制,当在static方法中使用table1Mapper和table2Mapper的时候会报编译错误: 无法从静态上下文中引用非静态变量 table1Mapper

3.2 方法自调用

目标类直接调用该类的其他标注了@Transactional 的方法(相当于调用了this.对象方法),事务不会起作用。事务不起作用其根本原因就是未通过代理调用,因为事务是在代理中处理的,没通过代理,也就不会有事务的处理。

首先在table1和table2中都已经出入了1,并有如下示例代码:

@RestController
@RequestMapping(value = "/cglib")
public class CglibTranController {
@Autowired
private CglibTranService cglibTranService;
@PutMapping("/testThis/{id1}/{id2}")
public boolean testThis(@PathVariable("id1") Integer id1, @PathVariable("id2") Integer id2) {
try {
cglibTranService.testTranByThis(id1,id2);
return true;
}catch (Exception ex){
ex.printStackTrace();
return false;
}
}
} @Service
public class CglibTranService {
@Autowired
private Table1Mapper table1Mapper; @Autowired
private Table2Mapper table2Mapper; /**
* 入口方法,这种方式事务会失效
* @param id1
* @param id2
*/
public void testTranByThis(Integer id1, Integer id2) {
//直接调用目标类的方法
testTranByThis_insert(id1,id2);
} @Transactional
public void testTranByThis_insert(Integer id1, Integer id2){
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity); Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}
}

通过curl来调用接口

curl -X PUT "http://localhost:8080/cglib/testThis/2/1"

结果是table1中有1,2两条记录,table2中只有1一条记录。也就是说testTranByThis_insert上面标注@Transactional无效table1Mapper插入成功了,table2Mapper的插入并未导致table1Mapper插入回滚。

那如果必须要在方法内部调用@Transactional注解方法保证事务生效,该怎么做?当然是改为Spring AOP的方式调用

//定义一个ApplicationContext 工具类
@Component
public class SpringContextUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
} public static ApplicationContext getApplicationContext() {
return applicationContext;
} public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
} public static Object getBean(Class c) {
return applicationContext.getBean(c);
}
}

并改造testTranByThis方法如下:

    public void testTranByThis(Integer id1, Integer id2) {
//直接调用目标类的方法
// testTranByThis_insert(id1,id2);
//注解调用
CglibTranService proxy = (CglibTranService)SpringContextUtil.getBean(CglibTranService.class);
proxy.testTranByThis_insert(id1,id2);
}

这样即使是内部调用,但是通过ApplicationContext 获取了Bean,改造后的事务是生效

3.3 多个事务管理器

当一个应用存在多个事务管理器时,如果不指定事务管理器,@Transactional 会按照事务管理器在配置文件中的初始化顺序使用其中一个。

如果存在多个数据源 datasource1 和 datasource2,假设默认使用 datasource1 的事务管理器,当对 datasource2 进行数据操作时就处于非事务环境。

解决办法是,可以通过@Transactional 的 value 属性指定一个事务管理器。在使用多个事务管理器的情况下,事务不生效的原因在本系列后续文章中会有分析

3.4 默认 checked 异常不回滚事务

Spring 默认只为 RuntimeException 异常回滚事务,如果方法往外抛出 checked exception,该方法虽然不会再执行后续操作,但仍会提交已执行的数据操作。这样可能使得只有部分数据提交,造成数据不一致。

要自定义回滚策略,可使用@Transactional 的 noRollbackFor,noRollbackForClassName,rollbackFor,rollbackForClassName 属性

如下代码事务不生效,table1Mapper插入成功。table2Mapper插入失败了,但是异常被捕获了并抛出了IOException,table1Mapper的插入不会回滚

    @Transactional(rollbackFor = RuntimeException.class)
public void testCheckedTran(Integer id1, Integer id2) throws IOException {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity);
try {
Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}catch (Exception ex){
throw new IOException("testCheckedTran");
}
}

不会回滚的原因是check了rollbackFor = RuntimeException.class,但是抛出的是IOException,而IOException并不是RuntimeException的子类,如下的继承关系图

改造以上代码如下可以成功回滚事务,DuplicateKeyException是RuntimeException的子类:

    @Transactional(rollbackFor = RuntimeException.class)
public void testCheckedTran(Integer id1, Integer id2) throws IOException {
Table1Entity table1Entity = new Table1Entity();
table1Entity.setId(id1);
table1Mapper.insert(table1Entity);
try {
Table2Entity table2Entity = new Table2Entity();
table2Entity.setId(id2);
table2Mapper.insert(table2Entity);
}catch (Exception ex){
throw new DuplicateKeyException("testCheckedTran");
}
}

Spring事务Transactional和动态代理(三)-事务失效的场景的更多相关文章

  1. Spring事务Transactional和动态代理(一)-JDK代理实现

    系列文章索引: Spring事务Transactional和动态代理(一)-JDK代理实现 Spring事务Transactional和动态代理(二)-cglib动态代理 Spring事务Transa ...

  2. Spring事务Transactional和动态代理(二)-cglib动态代理

    系列文章索引: Spring事务Transactional和动态代理(一)-JDK代理实现 Spring事务Transactional和动态代理(二)-cglib动态代理 Spring事务Transa ...

  3. 【Java EE 学习 24 下】【注解在数据库开发中的使用】【反射+注解+动态代理在事务中的应用service层】

    一.使用注解可以解决JavaBean和数据库中表名不一致.字段名不一致.字段数量不一致的问题. 1.Sun公司给jdbc提供的注解 @Table.@Column.@Id.@OneToMany.@One ...

  4. Spring AOP中的动态代理

    0  前言 1  动态代理 1.1 JDK动态代理 1.2 CGLIB动态代理 1.2.1 CGLIB的代理用法 1.2.2 CGLIB的过滤功能 2  Spring AOP中的动态代理机制 2.1  ...

  5. 转:Spring AOP中的动态代理

    原文链接:Spring AOP中的动态代理 0  前言 1  动态代理 1.1 JDK动态代理 1.2 CGLIB动态代理 1.2.1 CGLIB的代理用法 1.2.2 CGLIB的过滤功能 2  S ...

  6. Spring 整合Mybatis Mapper动态代理方法

    先看项目目录结构 很清爽了 最重要的Spring的核心配置文件,看一下 <?xml version="1.0" encoding="UTF-8"?> ...

  7. Spring中的cglib动态代理

    Spring中的cglib动态代理 cglib:Code Generation library, 基于ASM(java字节码操作码)的高性能代码生成包 被许多AOP框架使用 区别于JDK动态代理,cg ...

  8. Spring中的JDK动态代理

    Spring中的JDK动态代理 在JDK1.3以后提供了动态代理的技术,允许开发者在运行期创建接口的代理实例.在Sun刚推出动态代理时,还很难想象它有多大的实际用途,现在动态代理是实现AOP的绝好底层 ...

  9. spring---aop(4)---Spring AOP的CGLIB动态代理

    写在前面 前面介绍了Spring AOP的JDK动态代理的过程,这一篇文章就要介绍下Spring AOP的Cglib代理过程. CGLib全称为Code Generation Library,是一个强 ...

随机推荐

  1. 【shell基础】条件测试

    例1 判断目录是否存在 #!/usr/bin/bash back_dir=/var/mysql_back if ! test -d $back_dir;then mkdir -p $back_dir ...

  2. iOS 一种很方便的构造TarBar

    直接在TarBarController中操作,代码如下: #import "DLTabBarController.h" #import "ViewController.h ...

  3. python学习笔记(15)pymysql数据库操作

    pymysql数据库操作 1.什么是PyMySQL 为了使python连接上数据库,你需要一个驱动,这个驱动是用于与数据库交互的库. PyMySQL : 这是一个使Python连接到MySQL的库,它 ...

  4. iOS火焰动画效果、图文混排框架、StackView效果、偏好设置、底部手势等源码

    iOS精选源码 高性能图文混排框架,构架顺滑的iOS应用. 使用OpenGLE覆盖阿尔法通道视频动画播放器视图. 可选最大日期截至当日日期的日期轮选器ChooseDatePicker 简单轻量的图片浏 ...

  5. KMP算法 详解+模板

    本文大部分摘自szy学长的ppt<string>中的KMP部分. %%%膜拜szy大神orz 1.概述 KMP 算法是用来解决单模匹配问题的一种算法. 如果暴力的进行单模匹配,那么时间复杂 ...

  6. deeplearning.ai 改善深层神经网络 week3 超参数调试、Batch Normalization和程序框架

    这一周的主体是调参. 1. 超参数:No. 1最重要,No. 2其次,No. 3其次次. No. 1学习率α:最重要的参数.在log取值空间随机采样.例如取值范围是[0.001, 1],r = -4* ...

  7. 安装oracle 11g 客户端,检查过程中报物理内存不足的解决

    今早接到同事电话,说安装oracle 11g客户端的时候,在检查先决条件的时候,报错,说内存不足,但是本机的内存是2G,肯定够用:如图: 找了一圈,原来Oracle执行先决条件检查是依赖c$共享,很多 ...

  8. js页面--年份自动增加

    ) { document.write("-" + new Date().getFullYear()); }</script> // 大于2017年自动加上 - 年份

  9. IT技术团队的管理幅度

    一.先科普下 管理幅度,又称管理宽度,是指在一个组织结构中,管理人员所能直接管理或控制的员工数目.这个数目是有限的,当超这个限度时,管理的效率就会随之下降. 二.经历现状 20左右的中小型团队,不算大 ...

  10. iOS(Swift)学习笔记之SwiftyJSON的使用

    本文为原创文章,转载请标明出处 1. 通过CocoaPods安装SwiftyJSON platform :ios, '10.0' target '<Your Target Name>' d ...