Spring AOP
AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。
OOP(Object Oriented Programming)面向对象编程,一种编程思想
AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式。
AOP 作用:在不惊动原始设计的基础上为其进行功能增强,类似于代理模式
举个栗子:
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行万次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
//记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
//计算时间差
Long totalTime = endTime-startTime;
//输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
代码的内容是对于 save 方法中有计算万次执行消耗的时间。
在 App 类中从容器中获取 bookDao 对象后,分别执行其 save , delete , update 和 select 方法后会 有如下的打印结果:
这个案例中其实就使用了Spring的AOP,在不惊动(改动)原有设计(代码)的前提下,想给谁添加功能就给谁添加:对于计算万次执行消耗的时间只有 save 方法有,但是 delete、update 方法没有这个逻辑的代码也有该功能,而 select 方法却没有
这个也就是Spring的理念:
- 无入侵式/无侵入式
对于 AOP 有几个核心概念要介绍下:
- 连接点(JoinPoint): 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- 在 SpringAOP 中,理解为方法的执行
- 切入点(Pointcut): 匹配连接点的式子
- 在 SpringAOP 中,一个切入点可以描述一个具体方法,也可以匹配多个方法
- 一个具体的方法:如 com.XXX.dao 包下的 BookDao 接口中的无形参无返回值的 save 方法
- 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
- 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
- 在 SpringAOP 中,一个切入点可以描述一个具体方法,也可以匹配多个方法
- 通知(Advice): 在切入点处执行的操作,也就是共性功能
- 在 SpringAOP 中,功能最终以方法的形式呈现
- 通知类: 定义通知的类
- 切面(Aspect): 描述通知与切入点的对应关系。
该如何实现?
- 对于上面的案例中 BookServiceImpl 中有save , update , delete和select方法,这些原始方法叫连接点
- BookServiceImpl 的四个方法中,update 和 delete 只有打印没有计算万次执行消耗时间, 但是在运行的时候已经有该功能,那也就是说 update 和 delete 方法都已经被增强,需要增强的方法叫切入点
- 执行 BookServiceImpl 的 update 和 delete 方法的时候都被添加了一个计算万次执行消耗时间 的功能,将这个功能抽取到一个方法中,换句话说就是存放共性功能的方法,叫通知
- 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,对于通知和切入点之间的关系描述叫切面
- 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类叫通知类
入门案例
使用 SpringAOP 的注解方式完成在方法执行前打印出当前系统时间
-
添加依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> </dependencies>
spring-context
中已经导入了spring-aop
,所以不需要再单独导入spring-aop
- 导入 AspectJ 的 jar 包,AspectJ 是 AOP 思想的一个具体实现,Spring 有自己的 AOP 实现,但是相比于 AspectJ 来说比较麻烦,所以直接采用 Spring 整合 ApsectJ 的方式进行 AOP 开发。
-
定义接口与实现类
public interface BookDao { public void save(); public void update(); } @Repository public class BookDaoImpl implements BookDao { public void save() { System.out.println(System.currentTimeMillis()); System.out.println("book dao save ..."); } public void update(){ System.out.println("book dao update ..."); } }
-
定义通知类和通知
public class MyAdvice { public void method(){ System.out.println(System.currentTimeMillis()); } }
-
定义切入点
public class MyAdvice { @Pointcut("execution(void com.XXX.dao.BookDao.update())") private void pt(){} public void method(){ System.out.println(System.currentTimeMillis()); } }
- 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法无实际逻辑
-
制作切面
public class MyAdvice { @Pointcut("execution(void com.XXX.dao.BookDao.update())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
-
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
-
@Before
翻译过来是之前,也就是说通知会在切入点方法执行之前执行
-
-
将通知类配给容器并标识其为切面类
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.XXX.dao.BookDao.update())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
-
开启注解格式 AOP 功能
@Configuration @ComponentScan("com.XXX") @EnableAspectJAutoProxy public class SpringConfig { }
名称 | @EnableAspectJAutoProxy |
---|---|
类型 | 配置类注解 |
位置 | 配置类定义上方 |
作用 | 开启注解格式AOP功能 |
名称 | @Aspect |
---|---|
类型 | 类注解 |
位置 | 切面类定义上方 |
作用 | 设置当前类为AOP切面类 |
名称 | @Pointcut |
---|---|
类型 | 方法注解 |
位置 | 切入点方法定义上方 |
作用 | 设置切入点方法 |
属性 | value:切入点表达式 |
名称 | @Before |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 |
AOP工作流程
由于 AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring 加载 bean 说起
-
Spring 容器启动
- 容器启动就需要去加载 bean
- 被增强的类,如:BookServiceImpl
- 通知类,如:MyAdvice
- 此时对象还没有创建成功
-
读取所有切面配置中的切入点
- 只会读取被使用的切入点,比如:
-
初始化 bean
-
判定要被实例化的 bean 对象对应的类中方法是否匹配到任意切入点
- 匹配失败,创建原始对象,如:UserDao
- 匹配失败说明不需要增强,直接调用原始对象的方法即可
- 匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao
- 匹配成功说明需要对其进行增强
- 对哪个类做增强,这个类对应的对象就叫做目标对象
- 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
- 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
- 匹配失败,创建原始对象,如:UserDao
-
获取 bean 执行方法
- 获取的 bean 是原始对象时,调用方法并执行,完成操作
- 获取的 bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
AOP核心概念
在上面介绍 AOP 的工作流程中,提到了两个核心概念,分别是:
- 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终的工作的
- 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
简单来说,目标对象就是要被增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运 行,只能说它在运行的过程中对于要增强的内容是缺失的。SpringAOP 是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。
AOP配置管理
AOP切入点表达式
语法格式
要明确两个概念:
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
对于切入点的描述,有两种方式:
-
描述方法一:执行 com.XXX.dao 包下的 BookDAO 接口中的无参 update 方法
execution(void com.XXX.dao.BookDao.update())
-
描述方式二:执行 com.XXX.dao.impl 包下的 BookDAOImpl 接口中的无参 update 方法
execution(void com.XXX.dao.impl.BookDaoImpl.update())
因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。
对于切入点表达式的语法为:
- 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.[类/接口名].方法名(参数) 异常)
比如:
execution(public User com.XXX.service.UserService.findById(int))
- execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
- public:访问修饰符,还可以是public,private等,可以省略
- User:返回值,写返回值类型
- com.XXX.service:包名,多级包使用点连接
- UserService:类/接口名称
- findById:方法名
- int:参数,直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以此处的简化是通过通配符
通配符
使用通配符描述切入点,主要的目的就是简化之前的配置
-
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现execution(public * com.XXX.*.UserService.find*(*))
匹配 com.XXX 包下的任意包中的 UserService 类或接口中所有 find 开头的带有一个参数的方法
-
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写execution(public User com..UserService.findById(..))
匹配 com 包下的任意包中的 UserService 类或接口中所有名称为 findById 的方法
-
+
:专用于匹配子类类型execution(* *..*Service+.*(..))
使用率较低,描述子类的,做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类。
接下来通过一个案例熟悉一下
execution(void com.XXX.dao.BookDao.update())
匹配接口,能匹配到
execution(void com.XXX.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到
execution(* com.XXX.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* com.XXX.dao.impl.BookDaoImpl.update(*))
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加
参数
execution(void com.*.*.*.*.update())
返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.update())
返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
execution(void *..update())
返回值为void,方法名是update的任意包下的任意类,能匹配
execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* com.XXX.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.XXX.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配
书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
- 访问控制修饰符针对接口开发均采用 public 描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用
*
通配快速描述 - 包名书写尽量不使用
..
匹配,效率过低,常用*
做单个包描述匹配,或精准匹配 - 接口名/类名书写名称与模块相关的采用
*
匹配,例如 UserService 书写成 *Service,绑定业务 层接口名 - 方法名书写以动词进行精准匹配,名词采用
*
匹配,例如 getById 书写成 getBy*
,selectAll书写成 select*
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
AOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合 理的位置,AOP 提供了5种通知类型:
- 前置通知
- 后置通知
- 环绕通知
- 返回后通知
- 抛出异常后通知
- 前置通知:追加功能到方法执行前,类似于在代码1或者代码2添加内容
- 后置通知:追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
- 返回后通知:追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会添加
- 抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出现异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加
- 环绕通知,环绕通知功能比较强大,他可以追加功能到方法执行的前后,这也是比较常用的方法,它可以实现其他四种通知类型的功能
通知类型的使用
前置通知
在通知类的前置方法上加@Before
注解
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.XXX.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
//此处也可以写成 @Before("MyAdvice.pt()")
public void before() {
System.out.println("before advice ...");
}
}
名称 | @Before |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法之前运行 |
后置通知
在通知类的后置方法上加@After
注解
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.XXX.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
}
名称 | @After |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行 |
环绕通知
在通知类的后置方法上加@Around
注解
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.XXX.dao.BookDao.update())")
private void pt(){}
@Around("pt()")
public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
}
在这会有个问题,运行后,原始方法的内容没有被执行,因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用,具体实现:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.XXX.dao.BookDao.update())")
private void pt(){}
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ...");
}
}
因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可 以实现不同的通知类型的功能,如:
- 环绕通知必须依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
- 通知中如果未使用 ProceedingJoinPoint 对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置成 void 即可,如果接收返回值,最好设定为 Object 类型
- 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void,也可以设置成 Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常
proceed() 需要抛出异常,我们来看下:
此时原始方法就会被执行了
注意事项:
-
原始方法有返回值的处理
-
如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值
-
@Component @Aspect public class MyAdvice { @Pointcut("execution(int com.XXX.dao.BookDao.select())") private void pt2(){} @Around("pt2()") public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); //表示对原始操作的调用 Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; } }
-
但是原始方法是 void 的就是返回 Null
-
建议都使用 Object 作为返回值类型
-
返回的是 Object 而不是 int 的主要原因是 Object 类型更通用,在环绕通知中是可以对原始方法返回值进行修改的。
-
名称 | @Around |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的关系绑定,当前通知方法在原始切入点方法前后运行 |
Tip:
-
// 获取执行签名信息 Signature signature = proceedingJoinPoint.getSignature(); // /通过签名获取执行操作名称(接口名) String className = signature.getDeclaringTypeName(); // 通过签名获取执行操作名称(方法名) String methodName = signature.getName();
-
可以通过以上 API 来环绕通知中获取执行的方法的接口路径和方法名称
返回后通知
在通知类的返回后方法上加@AfterReturning
注解
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.XXX.dao.BookDao.select())")
private void pt2(){}
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
注意: 返回后通知是需要在原始方法正常执行后才会被执行,如果原始方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。
名称 | @AfterReturning |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始方法切入点方法正常执行完毕后执行 |
异常后通知
在通知类的异常后方法上加@@AfterThrowing
注解
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.XXX.dao.BookDao.select())")
private void pt2(){}
@@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
注意: 异常后通知是需要原始方法抛出异常,如果没有抛异常,异常后通知将不会被执行。
名称 | @AfterThrowing |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行 |
AOP通知获取数据
对于通知也会有参数、返回值、异常
- 获取切入点方法的参数,所有的通知类型都可以获取参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- ProceedingJoinPoint:适用于环绕通知
- 获取切入点方法返回值,前置和抛出异常通知是没有返回值,后置通知可有可无
- 返回后通知
- 环绕通知
- 获取切入点方法运行异常信息,前置和返回值通知是没有的,后置通知可有可无
- 抛出异常后通知
- 环绕通知
获取参数
非环绕通知获取方式
在方法上添加 JoinPoint,通过 JoinPoint 来获取参数
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
private void pt(){}
@Before("pt()")
public void before(JoinPoint jp){
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ..." );
}
//...其他的略
}
注意: 因为参数的个数是不固定的,所以使用数组更适配些,而且 JoinPoint 方法获取参数适用于前置、后置、返回后、抛出异常后通知。
环绕通知获取方式
环绕通话使用的是 ProceedingJoinPoint,因为 ProceedingJoinPoint 是 JoinPoint 类的子类,所以对于 ProceedingJoinPoint 类中应该也会有对应的 getArgs() 方法
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp)throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}
}
注意:
-
pjp.proceed() 方法是有两个构造方法,分别是:
-
-
调用无参数的 proceed,当原始方法有参数,会在调用的过程中自动传入参数
-
所以调用这两个方法的任意一个都可以完成功能
-
但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:
-
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.XXX.dao.BookDao.findName(..))") private void pt(){} @Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable{ Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; Object ret = pjp.proceed(args); return ret; } //其他的略 }
-
有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。
-
获取返回值
对于返回值,只有返回后 AfterReturing 和环绕 Around 这两个通知类型可以获取
环绕通知获取返回值
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = pjp.proceed(args);
return ret;
}
}
上述代码中,ret
就是方法的返回值,我们是可以直接获取,不但可以获取,如果需要还可以进行修 改。
返回后通知获取返回值
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
private void pt(){}
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(Object ret) {
System.out.println("afterReturning advice ..."+ret);
}
}
注意:
-
参数名的问题
-
afterReturning 方法参数类型的问题
- 参数类型可以写成 String,但是为了能匹配更多的参数类型,建议写成 Object 类型
-
afterReturning 方法参数的顺序问题
获取异常
对于获取抛出的异常,只有抛出异常后 AfterThrowing 和环绕 Around 这两个通知类型可以获取
环绕通知获取异常
只需要将异常捕获,就可以获取到原始方法的异常信息了
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = null;
try{
ret = pjp.proceed(args);
}catch(Throwable throwable){
t.printStackTrace();
}
return ret;
}
}
在 catch 方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和业务需求有关了。
抛出异常后通知获取异常
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.XXX.dao.BookDao.findName(..))")
private void pt(){}
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
}
注意:
AOP事务管理
事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring事务简介
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager
commit是用来提交事务,rollback是用来回滚事务。
PlatformTransactionManager
只是一个接口,Spring 还为其提供了一个具体的实现:
从名称上可以看出,我们只需要给它一个 DataSource 对象,它就可以帮你去在业务层管理事务。其内部采用的是 JDBC 的事务。所以说如果你持久层采用的是 JDBC 相关的技术,就可以采用这个事务管理器来管理你的事务,而Mybatis内部采用的就是JDBC的事务。
Spring管理事务具体步骤
-
在需要被事务管理的方法上添加注解
在类上加
@Transactional
注解@Transactional
可以写在接口类上、接口方法上、实现类上和实现类方法上- 写在接口类上,该接口的所有实现类的所有方法都会有事务
- 写在接口方法上,该接口的所有实现类的该方法都会有事务
- 写在实现类上,该类中的所有方法都会有事务
- 写在实现类方法上,该方法上有事务
- 建议写在实现类或实现类的方法上
-
在非主配置类中配置事务管理器
//配置事务管理器,mybatis使用的是jdbc事务 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; }
注意: 事务管理器要根据使用技术进行选择,Mybatis 框架使用的是 JDBC 事务,可以直接使用
DataSourceTransactionManager
-
开启事务注解
// 在说配置类中配置事务管理器 @EnableTransactionManagement // 加上该注解即可
名称 | @EnableTransactionManagement |
---|---|
类型 | 配置类注解 |
位置 | 配置类定义上方 |
作用 | 设置当前 Spring 环境中开启注解式事务支持 |
名称 | @Transactional |
---|---|
类型 | 接口注解 类注解 方法注解 |
位置 | 业务层接口上方 业务层实现类上方 业务方法上方 |
作用 | 为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务) |
### Spring事务角色
事务管理员:发起事务方,在 Spring 中通常指代业务层开启事务的方法
事务协调员:加入事务方,在 Spring 中通常指代数据层方法,也可以是业务层方法
举个例子:
未开事务前:
- AccountDao 的 outMoney 因为是修改操作,会开启一个事务T1
- AccountDao 的 inMoney 因为是修改操作,会开启一个事务T2
- AccountService的transfer没有事务
- 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
- 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行,就会导致数据出现错误
开启事务后:
- transfer 上添加了
@Transactional
注解,在该方法上就会有一个事务 - AccountDao 的 outMoney 方法的事务T1加入到 transfer 的事务T中
- AccountDao 的 inMoney 方法的事务T2加入到 transfer 的事务T中
- 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性
事务配置
上面这些属性都可以在@Transactional注解的参数上进行设置
- readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true
- timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间
- rollbackFor:当出现指定异常进行事务回滚
- noRollbackFor:当出现指定异常不进行事务回滚
- 并不是所有的异常都会回滚事务,Spring 的事务只会对
Error
异常和RuntimeException
异常及其子类进行事务回滚,其他的异常类型是不会回滚的
- 并不是所有的异常都会回滚事务,Spring 的事务只会对
- rollbackForClassName 等同于 rollbackFor,只不过属性为异常的类全名字符串
- noRollbackForClassName 等同于 noRollbackFor,只不过属性为异常的类全名字符串
- isolation 设置事务的隔离级别
- DEFAULT:默认隔离级别, 会采用数据库的隔离级别
- READ_UNCOMMITTED:读未提交
- READ_COMMITTED:读已提交
- REPEATABLE_READ:重复读取
- SERIALIZABLE:串行化
事务的传播行为
-
log方法、inMoney 方法和 outMoney 方法都属于增删改,分别有事务T1,T2,T3
-
transfer 因为加了
@Transactional
注解,也开启了事务T -
现在需要让 inMoney 和 outMoney 方法加入事务T,而 log 方法不加入(开启一个新事务)
-
@Service public class LogServiceImpl implements LogService { @Autowired private LogDao logDao; //propagation设置事务属性:传播行为设置为当前操作需要新事务 @Transactional(propagation = Propagation.REQUIRES_NEW) public void log(String out,String in,Double money ) { logDao.log("转账操作由"+out+"到"+in+",金额:"+money); } }
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
事务传播机制的可选值:
对于开发实际中使用的话,因为默认值需要事务是常态的,根据开发过程选择其他的就可以了
性为异常的类全名字符串
- noRollbackForClassName 等同于 noRollbackFor,只不过属性为异常的类全名字符串
- isolation 设置事务的隔离级别
- DEFAULT:默认隔离级别, 会采用数据库的隔离级别
- READ_UNCOMMITTED:读未提交
- READ_COMMITTED:读已提交
- REPEATABLE_READ:重复读取
- SERIALIZABLE:串行化
事务的传播行为
[外链图片转存中…(img-HRirldMA-1716951605866)]
-
log方法、inMoney 方法和 outMoney 方法都属于增删改,分别有事务T1,T2,T3
-
transfer 因为加了
@Transactional
注解,也开启了事务T -
现在需要让 inMoney 和 outMoney 方法加入事务T,而 log 方法不加入(开启一个新事务)
-
@Service public class LogServiceImpl implements LogService { @Autowired private LogDao logDao; //propagation设置事务属性:传播行为设置为当前操作需要新事务 @Transactional(propagation = Propagation.REQUIRES_NEW) public void log(String out,String in,Double money ) { logDao.log("转账操作由"+out+"到"+in+",金额:"+money); } }
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
事务传播机制的可选值:
[外链图片转存中…(img-OsZ4kJqf-1716951605866)]
对于开发实际中使用的话,因为默认值需要事务是常态的,根据开发过程选择其他的就可以了