Spring - 3( AOP )
Spring - 3
AOP 简介
- AOP ( Aspect Oriented Programming ) 面向切面编程,一种编程范式,指导开发者如何组织程序结构
- OOP ( Object Oriented Programming ) 面向对象编程
- OOP 是一种编程思想,那么 AOP 也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式
- AOP 作用:在不惊动原有代码的前提下对其进行功能增强
- Spring 理念:无入侵式 / 无侵入式
核心概念
-
为了能更好的理解 AOP 的相关概念,准备了一个环境,主要看类 BookDaoImpl:
@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 ..."); } }
-
此时在 main 中使得不仅是执行 save 方法会重复输出一万次并显示执行时间,就连 update 和 delete 方法执行时也会重复输出一万次并显示执行时间,但 select 方法只是输出一句
book dao select ...
- 上述代码从表面根本看不出来,这就是 AOP 所做
- 代码无 spring 痕迹,却增加了功能 —— 无入侵式
-
其中:
- BookServiceImpl 中的 save、update、delete 和 select 所有的方法 —— 连接点
- update、delete 仅是需要增强的方法 —— 切入点
- save 方法中编写的、计算万次执行消耗时间的功能,将这个功能抽取到一个方法中,即:存放共性功能的方法 —— 通知
- 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类 —— 通知类
- 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述 —— 切面
总结
- 连接点 ( JoinPoint ):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- AOP 不仅是在 spring 中存在,所以上述为较标准说法
- 在 SpringAOP 中,理解为方法的执行,各个方法
- 切入点 ( Pointcut ):匹配连接点的式子
- 在 SpringAOP 中,一个切入点可以描述一个具体方法,也可也匹配多个方法
- 一个具体的方法:如 BookDao 接口中的无形参无返回值的 save 方法
- 匹配多个方法:所有的 save 方法,所有的 get 开头的方法,所有以 Dao 结尾的接口中的任意方法,所有带有一个参数的方法
- 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点
- 在 SpringAOP 中,一个切入点可以描述一个具体方法,也可也匹配多个方法
- 通知 ( Advice ):在切入点处执行的操作,也就是共性功能
- 在 SpringAOP 中,功能最终以方法的形式呈现
- 通知类:定义通知的类
- 切面 ( Aspect ):描述通知与切入点的对应关系
AOP 入门案例
- 使用 SpringAOP 的注解方式完成在方法执行的前打印出当前系统时间
环境准备
-
添加 Spring 依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency>
-
添加 BookDao 和 BookDaoImpl 类
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 ..."); } }
-
创建 Spring 的配置类
@Configuration @ComponentScan("com.qst") public class SpringConfig { }
-
编写 App 运行类
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); bookDao.save(); } }
AOP 实现步骤
-
添加依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency>
- 因为
spring-context
中已经导入了spring-aop
,所以不需要再单独导入spring-aop
依赖 - 导入 AspectJ 的 jar 包,AspectJ 是 AOP 思想的一个具体实现,Spring 有自己的 AOP 实现,但是相比于 AspectJ 来说比较麻烦,所以我们直接采用 Spring 整合 ApsectJ 的方式进行 AOP 开发
- 因为
-
定义接口与实现类
- 环境准备时候的 BookDaoImpl 代码不变
-
定义通知类和通知
-
通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印
public class MyAdvice { public void method(){ System.out.println(System.currentTimeMillis()); } }
-
-
通知类中编写切入点
-
BookDaoImpl 中有两个方法,分别是 save 和 update,我们要增强的是 update 方法
public class MyAdvice { @Pointcut("execution(void com.qst.dao.BookDao.update())") // 切入点:表示执行到update方法的时候 private void pt(){} // 私有的自定义空壳 public void method(){ System.out.println(System.currentTimeMillis()); } }
- 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑
-
-
制作切面
public class MyAdvice { @Pointcut("execution(void com.qst.dao.BookDao.update())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
- 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
- @Before 表示通知会在切入点方法执行之前执行
-
将通知类配给容器并标识其为切面类
@Component // 表示扫描到此时,当作aop来处理: @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
-
开启注解格式 AOP 功能
@Configuration @ComponentScan("com.qst") @EnableAspectJAutoProxy public class SpringConfig { }
- 感觉就像是:@EnableAspectJAutoProxy 开启了 @Aspect 的使用,@Aspect 开启了 @Pointcut 和 @Before 的作用
-
运行程序
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); bookDao.update(); } }
- 若是看到在执行 update 方法之前打印了系统时间戳,说明对原始方法进行了增强,AOP 编程成功
AOP 工作流程
AOP 工作流程
- 由于 AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring 加载 bean 说起
- Spring 容器启动
- 容器启动就需要去加载 bean:
- 需要被增强的类,如:BookServiceImpl
- 通知类,如:MyAdvice
- 注意,此时 bean 对象还没有创建成功
- 容器启动就需要去加载 bean:
- 读取所有切面配置中的切入点
- 将需要的 @Pointcut 注解的切入点加以使用
- 初始化 bean,判定 bean 对应的类中的方法是否匹配到任意切入点
- 注意第一步在容器启动的时候,bean 对象还没有被创建成功。
- 要被实例化 bean 对象的类中的方法和切入点进行匹配
- 即:看 @Pointcut 注解的 execution 路径对应的 bean 对象
- 匹配失败,创建原始对象,如
UserDao
( 上述案例中@Pointcut("execution(void com.itheima.dao.BookDao.update())")
不是 UserDao 对象 )- 匹配失败说明不需要增强,直接调用原始对象的方法即可
- 匹配成功,创建原始对象 ( 目标对象 ) 的代理对象,如:
BookDao
- 匹配成功说明需要对其进行增强
- 对哪个类做增强,这个类对应的对象就叫做目标对象
- 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
- 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
- 获取 bean 执行方法
- 获取的 bean 是原始对象时,调用方法并执行,完成操作
- 获取的 bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
- 即 AOP 核心是使用的代理模式
验证容器中是否为代理对象
-
App 类中:
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); System.out.println(bookDao); System.out.println(bookDao.getClass()); } }
-
修改 MyAdvice 类
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update1())") private void pt(){} @Before("pt()") public void method(){ System.out.println(System.currentTimeMillis()); } }
- 因为定义的切入点中,被修改成 update1,所以 BookDao 中的 update 方法在执行的时候,就不会被增强,所以容器中的对象应该是目标对象本身
-
但若是改回来 update:
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
- 就会发现
bookDao.getClass()
打印出类似class com.sun.proxy.$Proxy19
,而bookDao
打印出的结果没有变化,所以查看时,就只使用.class
- 就会发现
AOP核心概念
- AOP 的工作流程中,提到了两个核心概念:
- 目标对象 ( Target ) :原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的 ( 即未有加强方法的 )
- 目标对象就是要增强的类对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的
- 代理 ( Proxy ):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
- SpringAOP 是在不改变原有设计 ( 代码 ) 的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知内容加进去,就实现了增强,这就是我们所说的代理 ( Proxy )
- 目标对象 ( Target ) :原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的 ( 即未有加强方法的 )
- 未增强 —— 原始对象
- 增强 —— 代理对象
AOP 配置管理
AOP 切入点表达式
- 就是上述案例中的 @Pointcut 中的
execution( ...... )
部分
语法格式
-
首先要先明确两个概念:
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
-
对于切入点的描述,其实是有两种方式:
-
一是只有方法名的 dao 接口,执行 com.qst.dao 包下的 BookDao 接口中的无参数 update 方法
execution(void com.qst.dao.BookDao.update())
-
二是继承了接口的 daoImpl 类,执行 com.qst.dao.impl 包下的 BookDaoImpl 类中的无参数 update 方法
execution(void com.qst.dao.impl.BookDaoImpl.update())
-
-
因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的
-
对于切入点表达式的语法为:
- 切入点表达式标准格式:动作关键字 ( 访问修饰符 返回值 包名.类 / 接口名.方法名 ( 参数 ) 异常名)
-
不需要硬记,而是通过一个例子理解:
execution(public User com.qst.service.UserService.findById(int))
- execution:动作关键字,描述切入点的行为动作,例如 execution 表示执行到指定切入点
- public:访问修饰符,还可以是 public,private 等,可以省略
- User:返回值,写返回值类型
- com.qst.service:包名,多级包使用符号 . 点来连接
- UserService:类 / 接口名称
- findById:方法名
- int:参数,直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
-
切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,想想就会觉得编写起来会比较麻烦,就需要用到下面所学习的通配符
通配符
-
我们使用通配符描述切入点,主要的目的就是简化之前的配置
-
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现execution (public * com.qst.*.UserService.find*(*))
- 匹配 com.qst 包下的任意包中的 UserService 类或接口中所有 find 开头的带有一个参数的方法
-
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写execution (public User com..UserService.findById(..))
- 匹配 com 包下的任意包中的 UserService 类或接口中所有名称为 findById 的方法
- 参数列表处写
..
表示任意参数,但不包含无参情况 ( 即一定是有参的任意 )
-
+
:专用于匹配子类类型 ( 很少用 )execution(* *..*Service+.*(..))
- 这个使用率较低,描述子类的,做 JavaEE 开发,继承机会就一次,使用都很慎重,所以很少用。*Service+,表示所有以 Service 结尾的接口的子类
-
环境若为:com.qst.dao 下有 BookDao 接口,内含无参 save、update 方法;com.qst.dao.impl 下有 BookDaoImpl implements BookDao,完善 save、update 方法功能
execution(void com.qst.dao.BookDao.update()) 匹配接口,能匹配到 execution(void com.qst.dao.impl.BookDaoImpl.update()) 匹配实现类,能匹配到 execution(* com.qst.dao.impl.BookDaoImpl.update()) 返回值任意,能匹配到 execution(* com.qst.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.qst.*.*Service.find*(..)) 将项目中所有业务层方法的以find开头的方法匹配 execution(* com.qst.*.*Service.save*(..)) 将项目中所有业务层方法的以save开头的方法匹配
书写技巧
对于切入点表达式的编写其实是很灵活的,在编写的时候有一些好的技巧使用:
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不描述实现类
- 如果描述到实现类,就出现紧耦合了
- 访问控制修饰符针对接口开发均采用 public 描述 ( 可省略访问控制修饰符描述 )
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用 * 通配快速描述
- 包名书写尽量不使用 .. 匹配,效率过低,常用 * 做单个包描述匹配,或精准匹配
- 接口名 / 类名书写名称与模块相关的采用 * 匹配,例如 UserService 书写成 *Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用 * 匹配,例如 getById 书写成 getBy*,selectAll 直接书写成 selectAll 就可
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
AOP 通知类型
类型介绍
-
共提供了 5 种通知类型:
- 前置通知
- 追加功能到方法执行,,类似于在代码 1 或者代码 2 添加内容
- 后置通知
- 追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码 5 添加内容
- 环绕通知 ( 重点 )
- 环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能,具体是如何实现的,在后面学习
- 返回后通知 ( 了解 )
- 追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码 3 添加内容,如果方法执行抛出异常,返回后通知将不会被添加
- 抛出异常后通知 ( 了解 )
- 追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码 4 添加内容,只有方法抛出异常后才会被添加
- 前置通知
环境准备
-
pom.xml 添加 Spring 依赖
<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>
-
添加 BookDao 和 BookDaoImpl 类
public interface BookDao { public void update(); public int select(); } @Repository public class BookDaoImpl implements BookDao { public void update(){ System.out.println("book dao update is running ..."); } public int select() { System.out.println("book dao select is running ..."); return 100; } }
-
创建 Spring 的配置类
@Configuration @ComponentScan("com.qst") @EnableAspectJAutoProxy public class SpringConfig { }
-
创建通知类
@Component @Aspect public class MyAdvice { // @Pointcut("execution(void com.qst.dao.BookDao.update())") // private void pt(){} }
-
编写 App 运行类
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); bookDao.update(); } }
通知类型的使用
前置通知 - @Before
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.qst.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
//此处也可以写成 @Before("MyAdvice.pt()"),但一般pt()都是在通知类种,所以不建议这样写
public void before() {
System.out.println("before advice ...");
}
}
后置通知 - @After
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.qst.dao.BookDao.update())")
private void pt(){}
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
}
- 后置通知是不管原始方法 update() 有没有抛出异常都会被执行
环绕通知 - @Around
-
类似前后置的方法不完全
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.qst.dao.BookDao.update())") private void pt(){} @Around("pt()") public void around(){ System.out.println("around before advice ..."); System.out.println("around after advice ..."); } }
-
这样输出打印的话只有通知内容,却没有了原始方法的内容
around before advice ... around after advice ...
-
因为环绕通知需要在原始方法的前后进行增强 ( 否则怎么知道哪个是环绕前,哪个是环绕后 ),所以环绕通知就必须要能对原始操作进行调用
@Component @Aspect public class MyAdvice { @Pointcut("execution(void com.qst.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 ..."); } }
-
proceed() 为什么要抛出异常 —— 因为无法保证原始方法是否异常,所以在这里就统一都抛异常了
- 只管加强,不管异常的解决
-
运行结果
around before advice ... book dao update is running ... around after advice ...
-
-
但若是有返回值的方法如:select() 中 return 100;
- 再做环绕通知就会报错 ( select 方法非 void,而是有返回值 int )
- 而原始 void 就是返回 Null
-
所以要修改 MyAdvice,对 BookDao 中的 select 方法添加环绕通知
@Component @Aspect public class MyAdvice { @Pointcut("execution(int com.qst.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(); //想再加操作的话就要强转 //Integer ret = (Integer) pjp.proceed(); System.out.println("around after advice ..."); return ret; } }
- 一是改 aroundSelect 方法的 void 为 Object
- 二是 Object 接收返回值
- 三是有返回值的话就在环绕后 return 出来
-
环绕后通知注意事项
- 环绕通知必须依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
- 通知中如果未使用 ProceedingJoinPoint 对原始方法进行调用将跳过原始方法的执行
- 没调用原始方法的话,就相当于把原始方法隔离了
- 在这步可以编写权限的校验,符合才能执行
- 对原始方法的调用可以不接收返回值,通知方法设置成 void 即可,如果接收返回值,最好设定为 Object 类型
- 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void,也可以设置成 Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常
- 否则出了异常可能会被吞掉,反而显示不出来有异常了
返回后通知 - @AfterReturning
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.qst.dao.BookDao.select())")
private void pt2(){}
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
- 返回后通知是需要在原始方法 select 正常执行后才会被执行,如果 select() 方法执行的过程中出现了异常,那么返回后通知是不会被执行
- 后置通知是不管原始方法有没有抛出异常都会被执行
异常后通知 - @AfterThrowing
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.qst.dao.BookDao.select())")
private void pt2(){}
@AfterThrowing ("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
- 异常后通知是需要原始方法抛出异常,可以在
select()
方法中添加一行代码int i = 1/0
即可- 如果没有抛异常,异常后通知将不会被执行
业务层接口执行效率
需求分析
- 需求:任意业务层接口执行均可显示其执行效率 ( 执行时长 )
- 开始执行方法之前记录一个时间
- 执行方法
- 执行完方法之后记录一个时间
- 用后一个时间减去前一个时间的差值,就是我们需要的结果
- 所以要在方法执行的前后添加业务,所以采用环绕通知,原始方法如果只执行一次,时间太快,两个时间差可能为 0,所以要执行万次来计算时间差
功能开发
-
开启 SpringAOP 的注解功能
- 在 Spring 的主配置文件 SpringConfig 类中添加注解
@EnableAspectJAutoProxy
- 在 Spring 的主配置文件 SpringConfig 类中添加注解
-
创建 AOP 的通知类 ProjectAdvice 并添加环绕通知
@Component @Aspect public class ProjectAdvice { //配置业务层的所有方法 @Pointcut("execution(* com.itheima.service.*Service.*(..))") private void servicePt(){} //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式 @Around("servicePt()") // 环绕改返回值类型 public void runSpeed(ProceedingJoinPoint pjp){ long start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { pjp.proceed(); } long end = System.currentTimeMillis(); System.out.println("业务层接口万次执行时间: "+(end-start)+"ms"); } }
-
优化通知类,想办法区分是哪个接口的哪个方法的具体执行时间
//...... public void runSpeed(ProceedingJoinPoint pjp){ //获取执行签名信息 Signature signature = pjp.getSignature(); //通过签名获取执行操作名称(接口名) String className = signature.getDeclaringTypeName(); //通过签名获取执行操作名称(方法名) String methodName = signature.getName(); long start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { pjp.proceed(); } long end = System.currentTimeMillis(); System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms"); // 这样就是具体显示哪个接口下的哪个方法的具体执行时间 } }
- 当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程 ( 这里只是测试了业务层的 )
- 这块只是通过该案例把 AOP 的使用进行了学习,具体的实际值是有很多因素共同决定的
AOP 通知获取数据
- 先前介绍通知类型的时候总共讲了五种,那么对于这五种类型却不是都有参数,返回值和异常的
- 获取切入点方法的参数,所有的通知类型都可以获取参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- ProceedingJoinPoint:适用于环绕通知
- 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- 返回后通知 ( @AfterReturning )
- 环绕通知 ( @Around )
- 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
- 抛出异常后通知 ( @AfterThrowing )
- 环绕通知 ( @Around )
环境准备
-
添加 Spring 依赖
<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>
-
添加 BookDao 和 BookDaoImpl 类
public interface BookDao { public String findName(int id); } @Repository public class BookDaoImpl implements BookDao { public String findName(int id) { System.out.println("id:"+id); return "qst"; } }
-
创建 Spring 的配置类
@Configuration @ComponentScan("com.qst") @EnableAspectJAutoProxy public class SpringConfig { }
-
编写通知类
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.qst.dao.BookDao.findName(..))") private void pt(){} @Before("pt()") public void before() { System.out.println("before advice ..." ); } @After("pt()") public void after() { System.out.println("after advice ..."); } @Around("pt()") public Object around() throws Throwable{ Object ret = pjp.proceed(); return ret; } @AfterReturning("pt()") public void afterReturning() { System.out.println("afterReturning advice ..."); } @AfterThrowing("pt()") public void afterThrowing() { System.out.println("afterThrowing advice ..."); } }
-
编写 App 运行类
public class App { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = ctx.getBean(BookDao.class); String name = bookDao.findName(100); System.out.println(name); } }
获取参数
非环绕通知获取方式
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
private void pt(){}
@Before("pt()")
public void before(JoinPoint jp)
Object[] args = jp.getArgs(); // 对象数组存储
System.out.println(Arrays.toString(args)); // 输出此对象内容参数,运维运行类执行的是查100,则输出时就为 [100]
System.out.println("before advice ..." );
}
//......
}
- 方法的参数只有一个,但获取的是一个数组
- 因为参数的个数是不固定的,所以使用数组更通配些
环绕通知获取方式
-
ProceedingJoinpoint 继承了 Joinpoint,所以也是可以 pjp.getArgs() 的方式进行获取
-
但 pjp.proceed() 方法是有两个构造方法,所以可以把数组放入
- 但实际上数组 args 放与不放运行都一样,但若是加上、指定数组的话,就可以在环绕通知里,数组获取后,方法执行前对 args 数组进行修改与更正原始方法的参数
- 无参的方法会在调用的过程中自动传入参数
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.qst.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; } //......输出后[]内没变,但是return的ret里原来的100变成了666 }
- 有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性
- 但实际上数组 args 放与不放运行都一样,但若是加上、指定数组的话,就可以在环绕通知里,数组获取后,方法执行前对 args 数组进行修改与更正原始方法的参数
返回值获取
- 对于返回值,只有返回后
AfterReturing
和环绕Around
这两个通知类型可以获取
环绕通知获取返回值
-
似上获取参数
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.qst.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; } //...... }
返回后通知获取返回值
-
需要编写注解的属性
@Component @Aspect public class MyAdvice { @Pointcut("execution(* com.qst.dao.BookDao.findName(..))") private void pt(){} @AfterReturning(value = "pt()",returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..."+ret); } //...... }
- returning 要与 参数列表中的 ret 匹配
- 如果参数列表里想要加上 JoinPoint 参数,就必须放在第一位,如:
public void afterReturning(JoinPoint jp,Object ret)
,两者位置反了就会报错
获取异常 ( 了解 )
- 对于获取抛出的异常,只有抛出异常后
AfterThrowing
和环绕Around
这两个通知类型可以获取
环绕通知获取异常
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.qst.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.qst.dao.BookDao.findName(..))")
private void pt(){}
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
//......
}
-
此处注解属性 throwing 指向值必须和下面形参名一致
-
让原始方法抛出异常的方式进行测试上述操作
@Repository public class BookDaoImpl implements BookDao { public String findName(int id,String password) { System.out.println("id:"+id); if(true){ throw new NullPointerException(); } return "qst"; } }
实例
- 在百度网盘提供网址和提取码时,如果在复制时提取码后面多了空格的话,不处理就比对是错误的,所以后端内部就将提取码前后的空格给 trim() 去掉了再进行匹配
- 这里的格式处理就是通过 AOP 环绕通知完成的操作
- 在业务方法执行之前对所有的输入参数进行格式处理 —— trim()
- 使用处理后的参数调用原始方法 —— 环绕通知中存在对原始方法的调用
代码编写
- 开启 SpringAOP 的注解功能
@Configuration
@ComponentScan("com.qst")
@EnableAspectJAutoProxy
public class SpringConfig {
}
- 编写通知类
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.qst.service.*Service.*(*,*))")
private void servicePt(){}
}
- 添加环绕通知
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.qst.service.*Service.*(*,*))")
private void servicePt(){}
@Around("DataAdvice.servicePt()")
// @Around("servicePt()")这两种写法都对
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed();
return ret;
}
}
- 完成核心业务,在上述操作的基础上处理参数中的空格
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
private void servicePt(){}
@Around("DataAdvice.servicePt()")
// @Around("servicePt()")这两种写法都对
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
//获取原始方法的参数
Object[] args = pjp.getArgs();
// foreach循环不能更改数组内容,此处需用for循环
for (int i = 0; i < args.length; i++) {
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
//将修改后的参数传入到原始方法的执行中
Object ret = pjp.proceed(args);
return ret;
}
}
- 运行程序
不管密码 root
前后是否加空格,最终控制台打印的都是 true
-
优化测试
- 在 dao 层实现类中可以打印出密码长度到控制器,可以发现长度是经过 trim 格式后没有空格的密码长度
@Repository public class ResourcesDaoImpl implements ResourcesDao { public boolean readResources(String url, String password) { System.out.println(password.length()); //模拟校验,比对 return password.equals("root"); } }
AOP 总结
-
作用是在不惊动原始代码的基础上进行功能的增强
-
核心概念
- 代理 ( Proxy ):SpringAOP 的核心本质是采用代理模式实现的
- 连接点 ( JoinPoint ):在 SpringAOP 中,理解为任意方法的执行
- 切入点 ( Pointcut ):匹配连接点的式子,也是具有共性功能的方法描述
- 切入点表达式
- 通知 ( Advice ):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
- 切面 ( Aspect ):描述通知与切入点的对应关系,两者的结合
- 目标对象 ( Target ):被代理的原始对象成为目标对象
-
环绕通知
- 环绕通知依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用
- 环绕通知可以隔离原始方法的调用执行
- 环绕通知返回值设置为 Object 类型
- 环绕通知中可以对原始方法调用过程中出现的异常进行处理
-
获取切入点方法的参数,所有的通知类型都可以获取参数
- JoinPoint:适用于前置、后置、返回后、抛出异常后通知
- ProceedingJoinPoint:适用于环绕通知
-
获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- 返回后通知
- 环绕通知
AOP 事务管理
Spring 事务简介
相关概念介绍
-
事务作用:在数据层保障一系列的数据库操作同成功同失败
-
Spring 事务作用:在数据层或业务层保障一系列的数据库操作同时成功或者失败
-
业务层也需要处理事务
-
转账业务会有两次数据层的调用,一次是加钱一次是减钱
-
把事务放在数据层,加钱和减钱就有两个事务
-
没办法保证加钱和减钱同时成功或者同时失败
-
这个时候就需要将事务放在业务层进行处理
-
-
Spring 为了管理事务,提供了一个平台事务管理器
PlatformTransactionManager
- 内含 commit 方法用来提交事务,rollback 方法用来回滚事务
-
PlatformTransactionManager 只是一个接口,Spring 还为其提供了一个具体的实现:
DataSourceTransactionManager
- 从名称上可以看出,我们只需要给它一个 DataSource 对象,它就可以帮你去在业务层管理事务。其内部采用的是 JDBC 的事务。所以说如果你持久层采用的是 JDBC 相关的技术,就可以采用这个事务管理器来管理你的事务。而 Mybatis 内部采用的就是 JDBC 的事务 ( 后期 Spring 整合 Mybatis 也采用此 DataSourceTransactionManager 事务管理器 )
转账案例 - 需求分析
-
需求:实现任意两个账户间转账操作 ( 即 A 账户减钱,B 账户加钱 )
-
为了实现上述的业务需求,要按照下面步骤来实现:
-
数据层提供基础操作,指定账户减钱 ( outMoney ),指定账户加钱 ( inMoney )
-
业务层提供转账操作 ( transfer ),调用减钱与加钱的操作
-
提供两个账号和操作金额执行转账操作
-
基于 Spring 整合 MyBatis 环境搭建上述操作
-
环境搭建
- 准备数据库表
create database spring_db character set utf8;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
insert into tbl_account values(1,'Tom',1000);
insert into tbl_account values(2,'Jerry',1000);
- 创建项目导入 jar 包,添加相关依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
- 根据表创建模型类
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
//setter...getter...toString
}
- 创建 Dao 数据层接口
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
- 创建 Service 业务层接口和实现类
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
public void transfer(String out,String in ,Double money) ;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
accountDao.inMoney(in,money);
}
}
-
添加 jdbc.properties 文件
-
创建 JdbcConfig 配置类
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}
- 创建 MybatisConfig 配置类
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
// 映射的bean类
ssfb.setTypeAliasesPackage("com.qst.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.qst.dao");
return msc;
}
}
- 创建 SpringConfig 配置类
@Configuration
@ComponentScan("com.qst")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}
- 编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",100D);
}
}
事务管理
-
正常情况下,上述环境,运行单元测试类,会执行转账操作,Tom 的账户会减少 100,Jerr 的账户会加 100。
-
但是如果在转账的过程中出现了异常,如:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0; // 异常处
accountDao.inMoney(in,money);
}
}
- 继上面的数据,这个时候就模拟了转账过程中出现异常的情况,正确的操作应该是转账出问题了,而真正运行后会发现,Tom 账户为 800 而 Jerry 还是 1100
- 程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
- 当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而 Spring 的事务管理就是用来解决这类问题的。
Spring 事务管理具体的实现步骤为:
- 在需要被事务管理的方法上添加注解
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money) ;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
// 写在实现类方法上,该方法上有事务
@Transactional
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}
}
-
注意:@Transactional 可以写在接口类上、接口方法上、实现类上和实现类方法上
-
写在接口类上,该接口的所有实现类的所有方法都会有事务
-
写在接口方法上,该接口的所有实现类的该方法都会有事务
-
写在实现类上,该类中的所有方法都会有事务
-
写在实现类方法上,该方法上有事务
-
-
通常写在业务层接口上而不是实现类中 —— 降低耦合
- 在 JdbcConfig 类中配置事务管理器
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
// 配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
//
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
- 注意:事务管理器要根据使用技术进行选择,Mybatis 框架使用的是 JDBC 事务,可以直接使用 PlatformTransactionManager、DataSourceTransactionManager ( spring 提供 )
- 开启事务注解
- 在 SpringConfig 的配置类中开启
@Configuration
@ComponentScan("com.qst")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
- 运行测试类
- 会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性
@EnableTransactionManagement:设置当前 Spring 环境中开启注解式事务支持
@Transactional:为当前业务层方法添加事务 ( 如果设置在类或接口上方则类或接口中所有方法均添加事务 )
Spring 事务角色
这节中我们重点要理解两个概念,分别是事务管理员和事务协调员
- 未开启 Spring 事务之前:
- AccountDao 的 outMoney() 因为是修改操作,会开启一个事务 T1
- AccountDao 的 inMoney() 因为是修改操作,会开启一个事务 T2
- AccountService 的 transfer() 没有事务
- 运行过程中如果没有抛出异常,则 T1 和 T2 都正常提交,数据正确
- 如果在两个方法中间抛出异常,T1 因为执行成功提交事务,T2 因为抛异常不会被执行,T1 不会回滚
- 就会导致数据出现错误
- 开启 Spring 的事务管理后
- transfer() 上添加了 @Transactional 注解,在该方法上就会有一个事务 T
- AccountDao 的 outMoney() 的事务 T1 加入到 transfer() 的事务 T 中
- AccountDao 的 inMoney() 的事务 T2 加入到 transfer() 的事务 T 中
- 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
- 上述例子可得如下概念:
-
事务管理员:发起事务方,在 Spring 中通常指代业务层开启事务的方法
- transfer() 的事务 T
-
事务协调员:加入事务方,在 Spring 中通常指代数据层方法,也可以是业务层方法
- outMoney() 的事务 T1 和 inMoney() 的事务 T2
-
注意:目前的事务管理是基于
DataSourceTransactionManager
和SqlSessionFactoryBean
使用的是同一个数据源- 即 JdbcConfig 与 MybatisConfig 两个配置文件用的是同一个数据源,所以才能统一管理
Spring 事务属性
事务配置
- 注意:并不是所有的异常都会回滚
- Spring 的事务只会对 Error 异常和 RuntimeException 异常及其子类进行事务回顾,其他的异常类型是不会回滚的,例如 IOException 不符合上述条件所以就不会回滚
- 了解
@Transactional
注解的各属性:
上面这些属性都可以在 @Transactional
注解的参数上进行设置。
-
readOnly:true 只读事务,false 读写事务,增删改要设为 false,查询设为 true
-
timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1 表示不设置超时时间。
-
rollbackFor:当出现指定异常进行事务回滚 ( 就是因为有的异常不会回滚 )
-
如下使用 rollbackFor 属性来设置出现 IOException 异常回滚
public interface AccountService { @Transactional(rollbackFor = {IOException.class}) public void transfer(String out,String in ,Double money) throws IOException; } // ..................... @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; public void transfer(String out,String in ,Double money) throws IOException{ accountDao.outMoney(out,money); //int i = 1/0; //这个异常事务会回滚 if(true){ throw new IOException(); //这个异常事务就不会回滚 } accountDao.inMoney(in,money); } }
-
-
noRollbackFor:当出现指定异常不进行事务回滚
- rollbackForClassName 等同于 rollbackFor,只不过属性为异常的类全名字符串
-
noRollbackForClassName 等同于 noRollbackFor,只不过属性为异常的类全名字符串
-
isolation 设置事务的隔离级别
- DEFAULT:默认隔离级别, 会采用数据库的隔离级别
- READ_UNCOMMITTED:读未提交
- READ_COMMITTED:读已提交
- REPEATABLE_READ:重复读取
- SERIALIZABLE:串行化
- 还有最后一个事务的传播行为,为了讲解该属性的设置,我们需要完成下面的案例
转账业务追加日志案例
需求分析
-
在前面的转案例的基础上添加新的需求,完成转账后记录日志
-
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
-
需求微缩:A 账户减钱,B 账户加钱,数据库记录日志
-
分析:
- 基于转账操作案例添加日志模块,实现数据库中记录日志
- 业务层转账操作 ( transfer ),调用减钱、加钱与记录日志功能
-
预期效果为:无论转账操作是否成功,均进行转账操作的日志留痕
环境准备
该环境是基于转账环境来完成的,在其基础上,我们继续往下写
- 创建日志表
create table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
)
- 添加 LogDao 接口
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
- 添加 LogService 接口与实现类
public interface LogService {
void log(String out, String in, Double money);
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
@Transactional
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
- 在转账的业务中添加记录日志
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
public void transfer(String out,String in ,Double money)throws IOException ;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
@Transactional
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
// int i =1/0;
accountDao.inMoney(in,money);
}finally {
// 不常使用,这里是为了百分百会执行此log方法
logService.log(out,in,money);
}
}
}
- 运行程序
-
当程序正常运行,tbl_account 表中转账成功,tbl_log 表中日志记录成功
-
当转账业务之间出现异常 ( int i =1/0 ),转账失败,tbl_account 成功回滚,但是 tbl_log 表未添加数据
-
这个结果和我们想要的不一样:
- 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
- 期望效果:无论转账操作是否成功,日志必须保留
事务传播行为
-
分析:
- log 方法、inMoney 方法和 outMoney 方法都属于增删改,分别有事务 T1,T2,T3;transfer 因为加了 @Transactional 注解,也开启了事务 T
- 之前所讲 Spring 事务会把 T1,T2,T3 都加入到事务T中
- 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
-
这里就需要让 log 方法单独是一个事务
- 要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:
- 事务传播行为:事务协调员对事务管理员所携带事务的处理态度
- 具体如何解决,就需要用到之前我们没有说的
propagation
属性
- 要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:
- 修改 logService 改变事务的传播行为
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
//propagation设置事务属性:传播行为设置为当前操作需要新事务,不受 T 事务干扰
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
-
运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志
-
事务传播行为的可选值
- 常为 REQUIRED 默认
- 而 REQUIRES_NEW 就是不管事务管理员如何,事务协调员都会新建事务
- 对于我们开发实际中使用的话,因为默认值需要事务是常态的,根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。其实入账和出账操作上也有事务,采用的就是默认值
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人