# spring-mybatis-transaction **Repository Path**: guangjava/spring-mybatis-transaction ## Basic Information - **Project Name**: spring-mybatis-transaction - **Description**: Spring中的事务代码源码分析 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: origin - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-09-08 - **Last Updated**: 2025-01-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Spring的AOP 本篇章中所有的代码都将会放置到git仓库中去,并且会做一个简要的说明。 ### 一、个人理解描述 Spring中所谓的AOP就是在不修改源码的情况下,来进行增强。**所谓的增强其实就是在方法执行前后添加一些额外操作。** > 所谓的增强,就是我们如何来对方法(以类中的方法为基本单位)处理。处理方法有五种:前置增强、后置增强等等 > > 但是最为常用的还是利用环绕通知来进行增强,习惯于手动控制,更加精细化操作。 简单利用画图说明一下上面的描述: ![image-20240120172332635](pic/image-20240120172332635.png) 以这里的CourseController和UserController为例,希望在CourseService和UserService类中的每个都开始事务操作,而且还不在修改CourseService和UserService类中源码的情况下来进行操作,不破坏原来的代码的完整性。 那么首先来写个简单的Demo来体验一下SpringAOP的强大之处。 ### 二、案例演示 * 希望只针对UserServiceImpl类中的save无参方法来做日志打印处理。 因为工作中需要,需要利用到注解+xml的方法,所以下面没有提供纯注解的方式来进行操作。 在UserService接口中提供了三个方法来模拟实际开发过程中接口中的方法: ```java public interface UserService { void save(); void save(String name); void save(String name, Integer age); } ``` UserServiceImpl是对接口UserService的实现: ```java @Service("userService") public class UserServiceImpl implements UserService { @Override public void save() { System.out.println("UserService.save()"); try { int randomMs = (int) (Math.random()*1000); Thread.sleep(randomMs); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void save(String name) { System.out.println("UserService.save(String)"); } @Override public void save(String name, Integer age) { System.out.println("UserService.save(String, Integer)"); } } ``` 上面两步操作中是最为简单的!下面就要来为我们的save无参方法来进行增强。 所谓的增强,就是调用save无参方法的时候,打印我们的日志。 那么来写一个增强类即可: ```java @Component public class MyAdvice { /** * 环绕通知方法。调用者调用时,Spring会执行这个环绕通知方法 * @param pjp 由Spring传递进来的切入点对象(目标方法、目标对象、方法实参等等封装成的对象) * @return */ public Object aroundMethod(ProceedingJoinPoint pjp){ Object result = null; try { System.out.println("调用业务方法之前,我希望看下是否已经走了动态代理"); long start = System.currentTimeMillis(); //自己调用目标对象,得到返回值 // 固定写法:调用业务方法-----所以上面的逻辑可以称之为在调用方法之前操作 result = pjp.proceed(pjp.getArgs()); // 下面的逻辑可以表示成调用业务逻辑方法之后的操作 System.out.println("调用业务方法之后,我希望看下业务方法执行之后的结果"); long end = System.currentTimeMillis(); System.out.println("当前业务方法调用过程中花费的是时间有:" + (end - start) + "毫秒"); } catch (Throwable throwable) { System.out.println("当前方法抛出异常,对应的异常是:"+throwable); } finally { System.out.println("无论业务方法执行过程中出现了怎样的问题,那么这行代码最终都要来执行"); } // 返回最终结果 return result; } } ``` 因为利用到的是注解+xml的方式,所以再写一个xml来配置扫描类和动态代理生成过程: ```xml ``` 那么来写一段代码测试: ```java public class XmlAopTest { @Test public void test(){ ApplicationContext app = new ClassPathXmlApplicationContext("classpath:applicationContext.xml"); UserService userService = app.getBean("userService", UserService.class); userService.save(); System.out.println("---------------------"); userService.save("tom"); System.out.println("---------------------"); userService.save("tom", 20); } } ``` 打印结果如下所示: ```tex 调用业务方法之前,我希望看下是否已经走了动态代理 UserService.save() 调用业务方法之后,我希望看下业务方法执行之后的结果 当前业务方法调用过程中花费的是时间有:838毫秒 无论业务方法执行过程中出现了怎样的问题,那么这行代码最终都要来执行 --------------------- UserService.save(String) --------------------- UserService.save(String, Integer) ``` 从这里可以看到,尽管利用userService调用了三个无参方法,但是只有save无参方法进行了增强。那么到底是如何做到的呢? 下面我们先介绍概念,然后从概念来进行入手,着手分析,然后再进行分析。 ### 三、AOP相关的概念 #### 3.1、AOP相关概念 * 目标对象(Target):要代理的/要增强的目标对象。 * 代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象 * 连接点(JoinPoint):能够被拦截到的点,在Spring里指的是方法 目标类里,所有能够进行增强的方法,都是连接点 * **切入点(PointCut)**:要对哪些连接点进行拦截的定义 已经增强的连接点,叫切入点 * **通知/增强(Advice)**:拦截到连接点之后要做的事情 对目标对象的方法,进行功能增强的代码 * **切面(Aspect)**:是切入点和通知的结合 * 织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程。Spring采用动态代理技术织入,而AspectJ采用编译期织入和装载期织入 那么画图来描述一下,我觉得更为稳妥,如下所示: ![image-20240120172358495](pic/image-20240120172358495.png) 以例子来进行说明: * 目标对象:就是courseservice所表示的单例对象; * 代理对象:就是需要对courseservice对应的单例对象来进行代理的对象; * 所谓的连接点:以上面的courseservice来举例,可以认为是courseservice类中的所有方法; * 所谓的切入点:就是我们需要筛选courseservice类中的个别方法来作为特殊的点(如何筛选,那么就需要我们手写point表达式来进行选择); * 增强:所谓的增强就是要对原来的方法来做何种操作; * 切面:切入点+增强; #### 3.2、AOP开发前要明确的事项 ##### 我们要做的事情: * 编写核心业务代码(Target目标类的目标方法) * 编写通知类,通知类中有通知方法(Advice增强功能方法) * 在配置文件中,配置织入关系,即将哪些通知与哪些切入点 结合,形成切面 ##### Spring的AOP做的事情: * 生成动态代理的过程(把通知织入到切入点的过程),是由Spring来实现的 * Spring会监控切入点方法的执行,一旦发现切入点方法执行,使用代理机制动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。 #### 小结 * AOP相关的概念/术语 * 目标类Target:要对哪个类进行增强 * 代理对象Proxy:对目标类增强后的那个代理对象 * 连接点JoinPoint:目标类里可增强的方法 * 切入点PointCut:要增强的方法 * 通知Advice:功能增强的代码 * 切面Aspect:切入点 + 通知 * 织入Weaving:把切入点 和 通知 进行结合,生成代理对象的过程 * 使用AOP,我们要做的事情: * 编写目标类,自己的业务代码 * 编写通知类 * 配置切面 * 使用AOP,Spring做的事情 * 根据我们配置的切面,进行织入生成代理对象 ### 四、基于XML的AOP #### 快速入门 ##### 1) 需求描述 * 有目标类`UserServiceImpl`,有通知类`MyAdvice` * 使用XML方式AOP,对目标类`UserServiceImpl`的方法进行增强 ##### 2) 步骤分析 1. 创建maven项目,导入AOP相关的依赖坐标 2. 创建目标类(要增强的类,内部有切入点),创建通知类(内部有增强的方法代码) 3. 修改配置文件: 1. 把目标类和通知类都配置成为bean对象 2. 配置切入点和通知方法(增强方法)的织入关系:配置切面 4. 测试代码 ##### 3) 入门实现 ###### 1. 创建maven项目,导入坐标 ```xml org.springframework spring-context 5.0.2.RELEASE org.aspectj aspectjweaver 1.8.9 org.springframework spring-test 5.0.2.RELEASE junit junit 4.12 ``` ###### 2. 创建目标类和通知类 * 目标类:`com.guang.aop.UserServiceImpl` ```java public class UserService{ void save(); } ``` ```java public class UserServiceImpl { public void save(){ System.out.println("UserServiceImpl.save......"); } } ``` * 通知类:`com.guang.aop.MyAdvice` ```java public class MyAdvice { public void before(){ System.out.println("前置通知..."); } } ``` ###### 3. 修改配置文件 1. 把目标类和通知类都配置到Spring配置文件中 2. 配置切入和通知方法(增强方法)的织入关系 ```xml ``` > 注意:在xml中增加了aop的名称空间如下: > > ![image-20240120172416065](pic/image-20240120172416065.png) ###### 4. 测试代码 ```java @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class AopTest { @Autowired private UserService userService; @Test public void testQuickStart(){ userService.save(); } } ``` ##### 4) 步骤小结 1. 导入jar包:`spring-context, aspectjweaver` 2. 编写目标类、编写通知类 3. 配置切面 ```xml ``` #### AOP详解 ##### 1) 切点表达式的写法 ###### 语法: ``` execution([权限修饰符] 返回值类型 包名.类名.方法名(参数列表)) ``` * 修饰符:可以省略 * 返回值类型: * 可以指定类型。比如`String` (如果类型有歧义,就写全限定类名,比如:`java.util.Date`) * `*`,表示任意字符。比如`Str*`,或者`*` * 包名: * 可以写`.`:表示当前包下的类或者子包。比如`com.guang.service` * 可以写`..`:表示当前包里所有后代类、后代包。比如`com..service` * `*`:表示任意字符。比如:`com.gua*`, `com.*` * 类名: * 可以指定类名。比如:`UserServiceImpl` * `*` 表示任意字符。比如:`*ServiceImpl`,`*` * 方法名: * 可以指定方法名 * `*` 表示任意字符。比如:`save*`,`*` * 参数列表: * 可以指定类型。比如:`String,Integer`表示第一个参数是String,第二个参数是Integer类型 * `*`表示任意字符。比如: * `String, *` 表示第一个参数是String,第二个参数是任意类型 * `Str*, Integer`表示第一个参数类型Str开头,第二个参数是Integer类型 * 可以使用`..`表示任意个数、任意类型的参数 ###### 示例 ``` execution(public void com.guang.dao.impl.UserDao.save()) execution(void com.guang.dao.impl.UserDao.*(..)) execution(* com.guang.dao.impl.*.*(..)) execution(* com.guang.dao..*.*(..)) execution(* *..*.*(..)) --不建议使用 ``` ##### 2) 通知的种类 ###### 通知的语法 ```xml ``` ###### 通知的类型 | 名称 | 标签 | 说明 | | -------- | ----------------------- | -------------------------------------------- | | 前置通知 | `` | 通知方法在切入点方法之前执行 | | 后置通知 | `` | 在切入点方法正常执行之后,执行通知方法 | | 异常通知 | `` | 在切入点方法抛出异常时,执行通知方法 | | 最终通知 | `` | 无论切入点方法是否有异常,最终都执行通知方法 | | 环绕通知 | `` | 通知方法在切入点方法之前、之后都执行 | ###### 通知示例 > 注意:通知方法的名称随意,我们这里是为了方便理解,才起名称为:before, after等等 * 前置通知 * 通知方法定义`MyAdvice`的`before`方法: ```java public void before(){ System.out.println("前置通知"); } ``` * xml配置 ```xml ``` * 后置通知 * 通知方法定义 ```java public void afterReturning(){ System.out.println("后置通知"); } ``` * xml配置 ```xml ``` * 环绕通知 * 通知方法定义 ```java /** * @param pjp ProceedingJoinPoint:正在执行的切入点方法对象 * @return 切入点方法的返回值 */ public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕:前置通知..."); Object[] args = pjp.getArgs(); //切入点方法执行 Object proceed = pjp.proceed(args); System.out.println("环绕:后置通知..."); return proceed; } ``` * xml配置 ```xml ``` * 异常抛出通知 * 通知方法定义 ```java public void afterThrowing(){ System.out.println("抛出异常通知"); } ``` * xml配置 ```xml ``` * 最终通知 * 通知方法定义 ```java public void after(){ System.out.println("最终通知"); } ``` * xml配置 ```xml ``` ##### 4) 小结 * 需要我们编写的内容: * 编写目标类,编写通知类 * 配置切面 ```xml ``` * 注意环绕通知的方法 ```java public Object aroundMethod(ProceedingJoinPoint pjp){ Object reuslt = null; try{ //写前置通知代码 //调用目标对象的方法 result = pjp.proceed(pjp.getArgs()); //写后置通知代码 }catch(Throwable t){ //写异常通知代码 }finally{ //写最终通知代码 } } ``` ### 五、扩展了解通知中获取切入点对象 #### 通知中获取切入点对象 #### 介绍 如果想要在通知方法中,获取切入点对象。可以在通知方法里直接增加以下参数: * Spring提供的运行时连接点/切入点对象: | 类名 | 介绍 | | -------------------------------------- | ------------------------------------------------------------ | | `org.aspectj.lang.JoinPoint` | 切入点对象,
用于前置、后置、异常、最终通知,作为通知方法的形参 | | `org.aspectj.lang.ProceedingJoinPoint` | 切入点对象,是`JoinPoint`的子接口
用于环绕通知,作为通知方法的参数 | * `org.aspectj.lang.JoinPoint`的常用方法 | 返回值 | 方法名 | 说明 | | ----------------------- | ----------------- | ------------------------------------------------------------ | | ` java.lang.Object[]` | `getArgs()` | 连接点的实参值. | | ` Signature` | `getSignature()` | 连接点方法签名 | | ` java.lang.Object` | `getTarget()` | Returns the target object. | | ` java.lang.Object` | `getThis()` | Returns the currently executing object. | | ` java.lang.String` | `toLongString()` | Returns an extended string representation of the join point. | | ` java.lang.String` | `toShortString()` | Returns an abbreviated string representation of the join point. | | ` java.lang.String` | `toString()` | | | ` JoinPoint.StaticPart` | `getStaticPart()` | Returns an object that encapsulates the static parts of this join point. | | ` java.lang.String` | `getKind()` | Returns a String representing the kind of join point. | * `ProceedingJoinPoint`是`JoinPoint`的子接口,它除了上述方法,还有 | 返回值 | 方法名 | 说明 | | ------------------- | ------------------------ | ------------------------------------------------------ | | `java.lang.Object` | `proceed()` | 执行下一个通知;
如果后边没有通知了,调用目标方法 | | ` java.lang.Object` | `proceed(Object[] args)` | 执行下一个通知;
如果后边没有通知了,调用目标方法 | #### 示例 ```java public class MyAdvice { public void before(JoinPoint jp) { System.out.println("前置:" + jp.getSignature()); } public void afterReturning(JoinPoint jp){ System.out.println("后置:" + jp.getSignature()); } public void afterThrowing(JoinPoint jp){ System.out.println("异常:" + jp.getSignature()); } public void after(JoinPoint jp){ System.out.println("最终:" + jp.getSignature()); } public Object around(ProceedingJoinPoint pjp){ Object result = null; try { System.out.println("==环绕:前置通知=="); //调用对象的方法,返回方法执行结果 result = pjp.proceed(pjp.getArgs()); System.out.println("==环绕:后置通知=="); } catch (Throwable throwable) { System.out.println("==环绕:异常通知=="); throwable.printStackTrace(); } finally { System.out.println("==环绕:最终通知=="); } return result; } } ``` ### 通知中绑定参数 * 不同类型的通知,可以绑定的参数是不同的 #### 前置通知 * 在通知中,可以绑定参数:获取切入点方法的实参 * 通知方法: ```java public void before(JoinPoint jp, Object params){ System.out.println("==前置通知=="); System.out.println("连接点:" + jp.getSignature()); System.out.println("实参:" + params); } ``` * 切入点表达式: ```xml ``` #### 后置通知 * 在通知中,可以绑定参数:获取切入点方法的实参和返回值 * 通知方法: ```java public void afterReturning(JoinPoint jp, Object params, Object result){ System.out.println("==后置通知=="); System.out.println("方法参数:" + params); System.out.println("返回值:" + result); } ``` * 切入点表达式: ```xml ``` #### 异常通知 * 在通知中,可以绑定参数:获取切入点方法的实参,和异常信息对象 * 通知方法: ```java public void afterThrowing(Exception ex, Object params){ System.out.println("==异常通知=="); System.out.println("方法实参:" + params); System.out.println("异常:" + ex); } ``` * 切入点表达式: ```xml ``` #### 最终通知 * 在通知中,可以绑定参数:获取方法的实参 * 通知方法: ```java public void after(Object params){ System.out.println("==最终通知=="); System.out.println("方法实参:" + params); } ``` * 切入点表达式: ```xml ``` ### 六、基于注解的AOP #### 快速入门 ##### 1) 需求描述 * 有目标类`UserServiceImpl`,有通知类`MyAdvice` * 使用注解方式的AOP对目标类`UserServiceImpl`的方法进行增强 ##### 2) 步骤分析 1. 创建maven项目,导入AOP需要的依赖坐标 2. 创建目标类,创建通知类 1. 使用注解`@Component`标注两个类,配置成为bean对象 2. 在通知类中,使用注解配置织入关系 3. 在配置文件中,开启组件扫描和AOP的自动代理(自动装配) 4. 测试 ##### 3) 入门实现 ###### 1. 创建maven项目,导入坐标 * 注意:需要增加AOP的实现包:`aspectjweaver` ```xml org.springframework spring-context 5.0.2.RELEASE org.aspectj aspectjweaver 1.8.9 org.springframework spring-test 5.0.2.RELEASE junit junit 4.12 ``` ###### 2. 创建目标类,创建通知类 1. 使用注解标注两个类,配置成为bean对象 * 实际开发中,使用`@Repository`, `@Service`, `@Controller`注解,按照分层进行配置 2. 在通知类中,使用注解配置织入关系 * 目标类`com.guang.aop.Target` ```java public class UserService{ void save(); } ``` ```java @Service("userService") public class UserServiceImpl { public void save(){ System.out.println("UserServiceImpl.save......"); } } ``` * 通知类`com.guang.aop.MyAdvice` ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; //声明当前类是切面类:把切入点和通知,在这个类里进行织入,当前类就成为了一个切面类 @Aspect @Component("myAdvice") public class MyAdvice { @Before("execution(void com.guang.impl..*.save())") public void before(){ System.out.println("前置通知..."); } @AfterReturning("execution(void com.guang.impl..*.save()))") public void afterReturning(){ System.out.println("后置通知"); } @After("execution(void com.guang.impl..*.save())") public void after(){ System.out.println("最终通知"); } @AfterThrowing("execution(void com.guang.impl..*.save())") public void afterThrowing(){ System.out.println("抛出异常通知"); } /** * @param pjp ProceedingJoinPoint:正在执行的切入点方法对象 * @return 切入点方法的返回值 */ @Around("execution(void com.guang.impl..*.save())") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕:前置通知..."); //切入点方法执行 Object proceed = pjp.proceed(); System.out.println("环绕:后置通知..."); return proceed; } } ``` ###### 4. 开启组件扫描和AOP自动代理 * 在`applicationContext.xml`中 ```xml ``` > 如果要使用纯注解开发,可以使用配置类代替`applicationContext.xml`,配置类如下: > > ```java > @Configuration //标记当前类是:配置类 > @ComponentScan(basePackage="com.guang") //配置注解扫描 > @EnableAspectJAutoProxy //开启AOP自动代理 > public class AppConfig{ > } > ``` ###### 5. 测试 ```java @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class AopTest { @Autowired private UserService userService; @Test public void testQuickStart(){ userService.save() } } ``` ##### 4) 步骤小结 #### AOP详解 ##### 1) 通知的种类 ###### 通知的语法 ```java @通知注解("切入点表达式") ``` ###### 通知的类型 | 名称 | 注解 | 说明 | | -------- | ----------------- | ------------------------------------ | | 前置通知 | `@Before` | 通知方法在切入点方法之前执行 | | 后置通知 | `@AfterRuturning` | 通知方法在切入点方法之后执行 | | 异常通知 | `@AfterThrowing` | 通知方法在抛出异常时执行 | | 最终通知 | `@After` | 通知方法无论是否有异常,最终都执行 | | 环绕通知 | `@Around` | 通知方法在切入点方法之前、之后都执行 | * 注意: * 注解方式配置的通知,执行顺序是:`前置->最终->后置/异常` * 如果想要指定执行的顺序,就使用环绕通知 ##### 2) 切点表达式的抽取 * 同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取; * 抽取方法是: * 在增强类(切面类,即被`@Aspect`标的类)上增加方法,在方法上使用`@Pointcut`注解定义切入点表达式, * 在增强注解中引用切入点表达式所在的方法 * 示例: ```java @Aspect @Component("myAdvice1") public class MyAdvice1 { //定义切入点表达式 @Pointcut("execution(void com.guang.service..*.save())") public void myPointcut(){} //引用切入点表达式 //完整写法:com.guang.aop.MyAdvice.myPointcut() //简单写法:myPointcut(), 引入当前类里定义的表达式,可以省略包类和类名不写 @Before("myPointcut()") public void before(){ System.out.println("前置通知..."); } @AfterReturning("myPointcut()") public void afterReturning(){ System.out.println("后置通知"); } @After("myPointcut()") public void after(){ System.out.println("最终通知"); } @AfterThrowing("myPointcut()") public void afterThrowing(){ System.out.println("抛出异常通知"); } /*@Around("myPointcut()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("前置通知..."); //切入点方法执行 Object proceed = pjp.proceed(); System.out.println("后置通知..."); return proceed; }*/ } ``` ##### 3) 小结 1. 在通知类上加注解`@Aspect`,声明成一个切面 2. 在通知类里方法上加注解`@Before/@AfterReturning/@AfterThrowing/@After/@Around`,配置切入点表达式 3. 在xml里开启aop的自动代理:`` ### 七、纯注解的AOP * 主要是把XML的配置,放到核心配置类上 ```java @Configuration @ComponentScan(basePackages="com.guang")//开启组件扫描 @EnableAspectJAutoProxy //开启AOP的自动代理 public class AppConfig{ } ``` ```java @Aspect @Component("myAdvice2") public class MyAdvice2 { //定义切入点表达式 @Pointcut("execution(void com.guang.service..*.save())") public void myPointcut(){} @Before("myPointcut()") public void before(){ System.out.println("前置通知..."); } @AfterReturning("myPointcut()") public void afterReturning(){ System.out.println("后置通知"); } @After("myPointcut()") public void after(){ System.out.println("最终通知"); } @AfterThrowing("myPointcut()") public void afterThrowing(){ System.out.println("异常通知"); } } ``` ### 八、Spring事务管理 在Spring事务管理中,我推荐使用的是编程式事务,而不是声明式事务。 所谓的编程式事务就是我们手动来控制事务,而声明式事务则是有Spring来帮助我们来实现的。 而事务又是我们日常操作过程中最为常用且常用的,我们不知道Spring事务如何给我们操作的,所以建议不要将事务由Spring来进行处理,而是我们手动的来进行管理!!! #### 编程式事务管理 * 所谓事务管理,即:按照给定的事务规则,来执行提交或回滚操作。其中: * 给定的事务规则:用`TransactionDefinition`表示 * 按照..来执行提交或回滚操作:用`PlatformTransactionManager`来完成 * `TransactionStatus`用于表示一个运行着的事务的状态 对于PlatformTransactionManager和TransactionStatus来说,是固定的套路,但是我们最需要关注的是这里的事务规则,也就是TransactionDefinition,在下面会重点关注和介绍。 #### 关于编程式事务的说明以及API介绍 - 编程式事务管理:通过编写代码的方式实现事务管理 - 编程式事务管理,因事务管理与业务功能耦合性太强,不方便维护,目前已经基本不用,但是与SpringAOP结合起来使用更佳。 > spring 2.0 就已经提供了 xml配置的声明式事务管理的支持 - 以下API仅做介绍了解,用于了解Spring事务相关的API,并回顾事务相关的概念 #### ` PlatformTransactionManager` - 是Spring提供的事务管理器**接口**,它提供了我们常用的操作事务的方法:开启事务、提交事务等 - 注意:`PlatformTransactionManager`是接口类型,不同的dao层技术有不同的实现,例如: - dao层是jdbcTemplate或Mybatis时,实现类是:`DataSourceTransactionManager`。而我们最常用的就是mybatis,所以肯定使用的是DataSourceTransactionManager。 - dao层是Hibernate时,实现类是:`HibernateTransactionManager` | 方法 | 返回值 | 说明 | | ------------------------------------------ | ------------------- | ----------------------- | | `getTransaction(TransactionDefinition td)` | `TransactionStatus` | 开启事务,并得到事务状态 | | `commit(TransactionStatus status)` | | 提交事务 | | `rollback(TransactionStatus status)` | | 回滚事务 | #### `TransactionDefinition` * 事务的定义信息对象,提供了以下常用方法: | 方法 | 参数 | 返回值 | 说明 | | -------------------------- | ---- | --------- | ------------------ | | `getIsolationLevel()` | | `int` | 获取事务的隔离级别 | | `getPropogationBehavior()` | | `int` | 获取事务的传播行为 | | `getTimeout()` | | `int` | 获取超时时间 | | `isReadOnly()` | | `boolean` | 是否只读的事务 | ##### 事务的隔离级别: * `ISOLATION_DEFAULT`:默认事务隔离级别 * MySql默认隔离级别:`repeatable read` * Oracle默认隔离级别:`read committed` * `ISOLATION_READ_UNCOMMITTED`:读未提交--存在脏读、不可重复读、幻读 * `ISOLATION_READ_COMMITTED`:读已提交--存在不可重复读、幻读 * `ISOLATION_REPEATABLE_READ`:重复读--存在幻读 * `ISOLATION_SERIALIZABLE`:串行化--没有并发问题 ##### 事务的传播行为: 用于解决业务方法调用业务方法时,事务的统一性问题的 ![image-20240120172439709](pic/image-20240120172439709.png) 也就是说service中的methodA方法调用methodB方法的时候,对于methodB来说,叫做传播行为,决定使用怎么样的事务来调用methodB方法。 > 以下三个,是要当前事务的 * `PROPAGATION_REQUIRED`:**需要有事务。默认** * 如果有事务,就使用这个事务 * 如果没有事务,就创建事务。 * `PROPAGATION_SUPPORTS`:支持事务 * 如果有事务,就使用当前事务, * 如果没有事务,就以非事务方式执行(没有事务) * `PROPAGATION_MANDATORY`:强制的 * 如果有事务,就使用当前事务 * 如果没有事务,就抛异常 > 以下三个,是不要当前事务的 * `PROPAGATION_REQUIRES_NEW`:新建的 * 如果有事务,就把事务挂起,再新建事务 * 如果没有事务,新建事务 * `PROPAGATION_NOT_SUPPORTED`:不支持的 * 如果有事务,就把事务挂起,以非事务方式执行 * 如果没有事务,就以非事务方式执行 * `PROPAGATION_NEVER`:非事务的 * 如果有事务,就抛异常 * 如果没有事务,就以非事务方式执行 > 最后一个,是特殊的 * `PROPAGATION_NESTED`:嵌套的 * 如果有事务,就在事务里再嵌套一个事务执行 * 如果没有事务,就是类似`REQUIRED`的操作 ##### 事务运行的超时时间: 超时后事务自动回滚 * 默认值-1,表示没有超时限制 * 如果有,可以以秒为单位进行设置 ##### 是否只读: * 如果设置为只读,那么方法只能查询,不能增删改 * 通常是查询方法设置为只读 #### `TransactionStatus` * 提供了查询事务具体运行状态的方法,常用方法如下: | 方法 | 返回值 | 说明 | | -------------------- | --------- | ---------------------- | | `hasSavePoint()` | `boolean` | 事务是否有回滚点 | | `isCompleted()` | `boolean` | 事务是否已经完成 | | `isNewTransaction()` | `boolean` | 是否是新事务 | | `isRollbackOnly()` | `boolean` | 事务是否是要回滚的状态 | #### 小结 * PlatformTransactionManager接口: * 如果dao层用的是Mybatis、JdbcTemplate:用DataSourceTransactionManager * 如果dao层用的是Hibernate:用HibernateTransactionManager * 事务定义信息: * 事务的隔离级别:通常使用默认`ISOLATION_DEFAULT` * 事务的传播行为:通常使用默认`PROPAGATION_REQUIRED` * 事务的超时时间:如果事务执行超时,会回滚。单位是秒。值为-1表示永不超时 * 事务是否是只读:如果只读,事务里只能执行查询操作,不能增删改 * 事务状态接口 * 事务是否有回滚点 * 事务是否已经完成 * 是否是新事务 * 事务是否是要回滚的状态 #### 示例代码一 下面也是来给出例子来进行说明展示: 给出实体类: ```java public class Account { private Integer id; private String name; private Float money; // 省略get/set方法 } ``` 给出业务逻辑代码: ```java public interface AccountService { void transfer(String from, String to, Float money); } ``` 具体实现类中的代码: ```java @Service("accountService") public class AccountServiceImpl implements AccountService { // 引入事务操作 @Autowired private AccountDao accountDao; @Autowired private TransactionDefinition txDefinition; @Autowired private PlatformTransactionManager txManager; @Override public void transfer(String from, String to, Float money) { //开启事务,得到事务状态 TransactionStatus txStatus = txManager.getTransaction(txDefinition); try { //操作dao Account fromAccount = accountDao.findByName(from); Account toAccount = accountDao.findByName(to); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountDao.edit(fromAccount); accountDao.edit(toAccount); //提交事务 txManager.commit(txStatus); } catch (Exception e) { //回滚事务 txManager.rollback(txStatus); // 出现异常,进行补偿机制 } } } ``` 那么给出dao代码: ```java public interface AccountDao { void edit(Account account) throws SQLException; Account findByName(String name) throws SQLException; } ``` 实现类代码: ```java package com.guang.dao.impl; import com.guang.dao.AccountDao; import com.guang.domain.Account; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.sql.SQLException; import java.util.List; @Repository("accountDao") public class AccountDaoImpl implements AccountDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public void edit(Account account) throws SQLException { jdbcTemplate.update("update account set name=?, money=? where id=?", account.getName(), account.getMoney(), account.getId()); } @Override public Account findByName(String name) throws SQLException { List accounts = jdbcTemplate.query("select * from account where name = ?", new BeanPropertyRowMapper<>(Account.class), name); if (accounts == null || accounts.size() == 0) { return null; }else{ return accounts.get(0); } } } ``` 然后给出配置文件代码 ```xml ``` 从上面来看业务逻辑操作的话,可以看到代码侵入性太高!很明显不适合来使用。 ```java @Service("accountService") public class AccountServiceImpl implements AccountService { @Autowired private TransactionDefinition txDefinition; @Autowired private PlatformTransactionManager txManager; @Override public void transfer(String from, String to, Float money) { //开启事务,得到事务状态 TransactionStatus txStatus = txManager.getTransaction(txDefinition); try { // .... //提交事务 txManager.commit(txStatus); } catch (Exception e) { //回滚事务 txManager.rollback(txStatus); e.printStackTrace(); } } } ``` 但是从这里来看的话,无非就是多用了PlatformTransactionManager和TransactionDefinition而已。 * 那么我的想法是将上面的代码抽取起来写成一个通知类型,然后配置PointCut和Advice组成Advisor。然而在我即将要动手去写的时候,在查看DefaultTransactionDefinition类的时候,我发现有继承类TransactionTemplate,我一看这玩意儿不是我们在项目中经常使用的一个类吗?以前也写过,也遇到过,但是在这里遇到的时候,我才恍然大悟。原来我们一直就是这么用的,但是从来没有去想过背后的原理,所以这也是写这篇博客的收获之一!非常幸运。 那么首先来分析一下我原来的思路(即按照切面类的想法) * 1、针对读写类型来定义事务规则,这里涉及到四个属性,在DefaultTransactionDefinition中存在: * propagationBehavior,默认值是PROPAGATION_REQUIRED,即表示有事务就使用这个事务,没有事务就不用; * isolationLevel,默认值是:ISOLATION_DEFAULT,即表示默认使用数据库的事务。如果使用的是MySQL,那么就用MySQL的RR,可重复读级别; * timeout:默认值是:TIMEOUT_DEFAULT(-1),表示永不超时,超时时间以秒为单位; * readOnly:默认值是:false,表示的是增删改查询使用事务,查询不使用事务; 之前我想的是定义多个事务规则对象,然后配置多个环绕通知方法,在开启事务的时候,根据不同的事务规则对象来获取得到事务。 * 2、注入事务管理器,然后传入事务规则定义,获取得到当前的事务状态; * 3、利用事务管理器通过状态来进行提交或者是回滚事务; 上面可以通过AOP切面类来进行配置,但是看到了TransactionTemplate之后,我发现根本就不需要使用到切面类就可以来进行操作。 那么首先来看下TransactionTemplate类的继承体系: ![image-20240120172454887](pic/image-20240120172454887.png) 分析一下继承体系: * 1、继承了DefaultTransactionDefinition类,所以可以用来定义事务规则信息对象; * 2、实现了InitializingBean接口,那么肯定是要在初始化方法来做操作; * 3、实现了TransactionOperations接口,从接口名称中可以知道这里代表的是事务; 而在TransactionTemplate类中,只有一个属性PlatformTransactionManager,而这是又是我们所需要的事务管理器。 而在InitializingBean接口的初始化方法中做了校验,如下所示: ```java public void afterPropertiesSet() { if (this.transactionManager == null) { throw new IllegalArgumentException("Property 'transactionManager' is required"); } } ``` 因为TransactionTemplate中存在无参方法,没有注入transactionManager对象,说明我们需要手动注入当前容器中的transactionManager对象。那么配置TransactionTemplate的时候可以配置如下所示: ```java ``` 如下所示,配置多个事务定义规则: ```java ``` 那么只需要在使用的时候,通过以下方式注入即可: ```java @Autowired @Qulifier("transactionTemplat1") private TransactionTemplate transactionTemplate @Autowired @Qulifier("transactionTemplat2") private TransactionTemplate transactionTemplate @Autowired @Qulifier("transactionTemplat3") private TransactionTemplate transactionTemplate ``` 通过这种方式来进行注入即可,也是比较方便的。 那么上面说完事务规则定义这块,这块应该有个疑问:在TransactionTemplate从哪里根据事务管理器获取得到事务状态的代码呢? 类似如下的代码: ```java @Service("accountService") public class AccountServiceImpl implements AccountService { @Autowired private TransactionDefinition txDefinition; @Autowired private PlatformTransactionManager txManager; @Override public void transfer(String from, String to, Float money) { //开启事务,得到事务状态 TransactionStatus txStatus = txManager.getTransaction(txDefinition); try { // .... //提交事务 txManager.commit(txStatus); } catch (Exception e) { //回滚事务 txManager.rollback(txStatus); e.printStackTrace(); } } } ``` 来从TransactionTemplate中找,发现就提供了一个方法:execute方法,那么重点就关注一下execute方法即可。 这个方法也是非常的简单,重点是这里的TransactionCallback接口,提前看下: ```java public interface TransactionCallback { T doInTransaction(TransactionStatus status); } ``` 接口中只有一个抽象方法,又被称之为函数式接口。不懂的话接着向下看 ![image-20240120172510173](pic/image-20240120172510173.png) 这里的方法也是非常简单呐,相当于是已经帮助我们做好了。 给出一个使用代码示例: 这里给的是没有返回值的,所以这里给的是Void类型。 ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; @Service public class TransactionalService { @Autowired private TransactionTemplate transactionTemplate; public void performTransactionalOperation() { // 不建议使用下面两行代码!因为如果忘记了设置会默认值,那么后面再次使用的时候默认使用的九种方式 /**nsactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);**/ transactionTemplate.execute(new TransactionCallback() { public Void doInTransaction(TransactionStatus status) { // 在这里执行事务操作 // 可以进行数据库操作、调用其他需要事务支持的方法等 return null; } }); } } ``` 再给出一个有值的代码且可以自己来处理事务状态。不利用事务管理中的回滚方法而已! ```java public Object getObject(String str) { /* * 执行带有返回值的事务管理 */ transactionTemplate.execute(new TransactionCallback() { @Override public Object doInTransaction(TransactionStatus transactionStatus) { try { ... //....... 业务代码 return new Object(); // 注意:这里没有提交代码的逻辑 } catch (Exception e) { // 回滚,这里并不是利用事务管理器进行提交的,这里只是设置了一个标记而已 // 在事务管理进行事务提交的时候会来检查这里的状态而已 transactionStatus.setRollbackOnly(); return null; } } }); } ``` #### 示例代码二 在上面的分析代码中,给出示例代码二: 首先给出配置xml文件: ```java ``` 然后给出代码: ```java package com.guang.service.impl1; import com.guang.dao.AccountDao; import com.guang.domain.Account; import com.guang.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; @Service("accountService") public class AccountServiceImpl1 implements AccountService { @Autowired private AccountDao accountDao; @Autowired private TransactionTemplate transactionTemplate; @Override public void transfer(String from, String to, Float money) { transactionTemplate.execute((status)-> { try { // 来做事务操作 Account fromAccount = accountDao.findByName(from); Account toAccount = accountDao.findByName(to); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountDao.edit(fromAccount); int i = 1 / 0; accountDao.edit(toAccount); } catch ( Exception exception) { System.out.println("执行SQL阶段出现异常!不能够进行提交"); status.setRollbackOnly(); } return null; }); System.out.println("使用事务管理器来执行代码完成"); } } ``` [然后将代码放置到仓库中去](https://gitee.com/guangjava/spring-mybatis-transaction) ## 九、事务失效原因 一般而言失效的情况都是用了声明式事务即 @Transactional 注解,如果使用了这个注解那么在**以下几个情况**下会导致事务失效: 1)rollbackFor 没设置对,比如默认没有任何设置(RuntimeException 或者 Error 才能捕获),则方法内抛出 IOException 则不会回滚,需要配置 @Transactional(rollbackFor = Exception.class)。 2)异常被捕获了,比如代码抛错,但是被 catch 了,仅打了 log 没有抛出异常,这样事务无法正常获取到错误,因此不会回滚。 3)同一个类中方法调用,因此事务是基于动态代理实现的,同类的方法调用不会走代理方法,因此事务自然就失效了。 4)@Transactional 应用在非 public 修饰的方法上,Spring 事务管理器判断非公共方法则不应用事务。 ![image-20241021151712776](./assets/image-20241021151712776.png) 5)@Transactional 应用在 final 和 static 方法上,因为 aop (Spring Boot2.x版本默认是 cglib,Spring 自身默认是 jdk,一般现在用的都是 SpringBoot)默认是 cglib 代理,无法对 final 方法子类化。static 是静态方法,属于类,不属于实例对象,无法被代理! 6)propagation 传播机制配置错误,例如以下的代码 ```java @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void addUserAndAddress(User user,Address address) throws Exception { userMapper.save(user); addAddress(address); } @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void addAddress(Address address) { addressMapper.save(address); } ``` 因为配置了 Propagation.REQUIRES_NEW,是新起了一个事务,即 addAddress 的事务和 addUserAndAddress 其实不是一个事务,因此两个事务之间当然就无法保证数据的一致性了。 7)多线程环境,因为 @Transactional 是基于 ThreadLocal 存储上下文的,多线程情况下每个线程都有自己的上下文,那么之间如何保持事务同步?保持不了,因此事务失效。 8)用的是 MySQL MyISAM,这个引擎本身不支持事务!