@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 ...");
}
}
分别执行对应的方法:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
update执行的方法输出为:
save执行的方法输出为:
select的方法输出为:
、
引出:这个功能是怎么实现的呢?AOP(无侵入式编程)
(1) Spring 的 AOP 是对一个类的方法在不进行任何修改的前提下实现增强。对于上面的案例中BookServiceImpl 中有 save , update , delete 和 select 方法 , 这些方法我们给起了一 个名字叫 连接点 (2) 在 BookServiceImpl 的四个方法中, update 和 delete 只有打印没有计算万次执行消耗时间, 但是在运行的时候已经有该功能,那也就是说 update 和 delete 方法都已经被增强,所以 对于需要增 强的方法 我们给起了一个名字叫 切入点 (3) 执行 BookServiceImpl 的 update 和 delete 方法的时候都被添加了一个计算万次执行消耗时间 的功能,将这个功能抽取到一个方法中,换句话说就是 存放共性功能的方法 ,我们给起了个名字叫 通 知。 (4) 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述,我们给起了个名字叫 切面 (5) 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫 通知类 至此 AOP 中的核心概念就已经介绍完了,总结下 : 连接点(JoinPoint) :程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等 在SpringAOP中,理解为方法的执行 切入点 (Pointcut): 匹配连接点的式子 在 SpringAOP 中,一个切入点可以描述一个具体方法,也可也匹配多个方法 ,一个具体的方法: 如 com.itheima.dao 包下的 BookDao 接口中的无形参无返回值的 save 方法 匹配多个方法: 所有的 save 方法,所有的 get 开头的方法,所有以 Dao 结尾的接口中的任意 方法,所有带有一个参数的方法 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一 定要被增强,所以可能不是切入点。 通知 (Advice): 在切入点处执行的 *** 作,也就是共性功能 在 SpringAOP 中,功能最终以方法的形式呈现 通知类:定义通知的类 切面(Aspect):描述通知与切入点的对应关系。 AOP的入门案例 案例设定:测算接口执行效率,但是这个案例稍微复杂了点,我们对其进行简化。 简化设定:在方法执行前输出当前系统时间。 开发方式:xml或 注解 解决思路: 1. 导入坐标 (pom.xml) 2. 制作连接点 ( 原始 *** 作, Dao 接口与实现类 ) 3.制作共性功能(通知类与通知) 4.定义切入点 5.绑定切入点与通知关系(切面) 1.导入坐标
org.aspectj
aspectjweaver
1.9.4
2.定义接口与实现类
package com.itheima.dao;
public interface BookDao {
public void save();
public void update();
}
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
@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 ...");
}
}
3.定义通知类和通知
通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。4.:定义切入点
BookDaoImpl 中有两个方法,分别是 save 和 update ,我们要增强的是 update 方法,该如何定义呢 ? 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。 5. :制作切面 切面是用来描述通知和切入点之间的关系,如何进行关系的绑定? 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行 位置
6:将通知类配给容器并标识其为切面类
步骤7:开启注解格式AOP功能
8 运行程序
通知类的关键:
package com.itheima.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* Created by IntelliJ IDEA.
*
* @Author : Runqiang Jiang
* @create 2022/5/14 16:10
*/
//通知类必须配置成Spring管理的bean
@Component
//设置当前类为切面类类
@Aspect
public class MyAdvice {
//设置切入点,要求配置在方法上方
@Pointcut("execution( void com.itheima.dao.BookDao.update())")
public void pt(){}
//设置在切入点pt()的前面运行当前 *** 作(前置通知)
@Before("pt()") //把切入点与通知联系起来
public void method(){
//定义好通知
System.out.println(System.currentTimeMillis());
}
}
AOP相关注解总结:
3 AOP工作流程 3.1 AOP 工作流程 由于 AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring加载bean说起: 流程 1:Spring 容器启动 容器启动就需要去加载 bean, 哪些类需要被加载呢 ? 需要被增强的类,如 :BookServiceImpl 通知类,如 :MyAdvice 注意此时 bean 对象还没有创建成功 流程 2: 读取所有切面配置中的切入点 流程 3: 初始化 bean , 判定bean 对应的类中的方法是否匹配到任意切入点 ,注意第1 步在容器启动的时候, bean 对象还没有被创建成功。 要被实例化bean对象的类中的方法和切入点进行匹配 流程 4: 获取 bean 执行方法 获取的bean 是原始对象时,调用方法并执行,完成 *** 作 获取的bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成 *** 作 验证容器中是否为代理对象 为了验证 IOC 容器中创建的对象和我们刚才所说的结论是否一致,首先先把结论理出来 : 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。 验证思路 1.要执行的方法,不被定义的切入点包含,即不要增强,打印当前类的 getClass() 方法 2.要执行的方法,被定义的切入点包含,即要增强,打印出当前类的 getClass() 方法 3.观察两次打印的结果
3.2 AOP核心概念
目标对象 (Target) :原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终 工作的 代理 (Proxy) :目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实 现 目标对象就是要增强的类 [ 如 :BookServiceImpl 类 ] 对应的对象,也叫原始对象,不能说它不能运 行,只能说它在运行的过程中对于要增强的内容是缺失的。 SpringAOP 是在不改变原有设计 ( 代码 ) 的前提下对其进行增强的,它的底层采用的是代理模式实现 的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [ 如 :MyAdvice 中的 method 方法 ] 内容加进去,就实现了增强 , 这就是我们所说的代理 (Proxy) 。 4,AOP配置管理 切入点 : 要进行增强的方法 切入点表达式:要进行增强的方法的描述方式对于切入点表达式的语法为:
切入点表达式标准格式:动作关键字 (访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常 名)execution(public User com.itheima.service.UserService.findById(int))
execution:
动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
public:
访问修饰符,
还可以是
public
,
private等,可以省略
User:
返回值,写返回值类型
com.itheima.service:
包名,多级包使用点连接
UserService:
类/接口名称
findById:
方法名
int:
参数,直接写参数的类型,多个类型用逗号隔开
异常名:
方法定义中抛出指定异常,可以省略
通配符简化的 *** 作:
execution(void com.itheima.dao.BookDao.update())
匹配接口,能匹配到
execution(void com.itheima.dao.impl.BookDaoImpl.update())
匹配实现类,能匹配到
execution(* com.itheima.dao.impl.BookDaoImpl.update())
返回值任意,能匹配到
execution(* com.itheima.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.itheima.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* com.itheima.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配
4.2 AOP
通知类型
AOP
通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合
理的位置,通知具体要添加到切入点的哪里? 共提供了5
种通知类型
:
前置通知
后置通知
环绕通知
(
重点
)
返回后通知
(
了解
)
抛出异常后通知
(
了解
(1) 前置通知,追加功能到方法执行前 , 类似于在代码 1 或者代码 2 添加内容 (2) 后置通知 , 追加功能到方法执行后 , 不管方法执行的过程中有没有抛出异常都会执行,类似于在代 码 5 添加内容 (3) 返回后通知 , 追加功能到方法执行后,只有方法正常执行结束后才进行 , 类似于在代码 3 添加内容, 如果方法执行抛出异常,返回后通知将不会被添加 (4) 抛出异常后通知 , 追加功能到方法抛出异常后,只有方法执行出异常才进行 , 类似于在代码 4 添加内 容,只有方法抛出异常后才会被添加 (5) 环绕通知 , 环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式, 它可以实现其他四种通知类型的功能
package com.itheima.aop;
/**
* Created by IntelliJ IDEA.
*
* @Author : Runqiang Jiang
* @create 2022/5/15 10:38
*/
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
//@Before:前置通知,在原始方法运行之前执行
// @Before("pt()")
public void before() {
System.out.println("before advice ...");
}
//@After:后置通知,在原始方法运行之后执行
//@After("pt()")
public void after() {
System.out.println("after advice ...");
}
//@Around:环绕通知,在原始方法运行的前后执行
// @Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始 *** 作的调用
pjp.proceed();
System.out.println("around after advice ...");
}
@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始 *** 作的调用
Integer ret = (Integer) pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
//@AfterReturning:返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
//@AfterThrowing:抛出异常后通知,在原始方法执行过程中出现异常后运行
@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
注解总结:
环绕通知注意事项 1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法 调用前后同时添加通知 2. 通知中如果未使用ProceedingJoinPoint对原始方法 进行调用将跳过原始方法的执行 3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为 Object类型 4. 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void, 也可以设置成 Object 5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常4.3 业务层接口执行效率
需求:任意业务层接口执行均可显示其执行效率(执行时长)
这个案例的目的是查看每个业务层执行的时间,这样就可以监控出哪个业务比较耗时,将其查找出来 方便优化。 具体实现的思路 : (1) 开始执行方法之前记录一个时间 (2) 执行方法 (3) 执行完方法之后记录一个时间 (4) 用后一个时间减去前一个时间的差值,就是我们需要的结果。 所以要在方法执行的前后添加业务,经过分析我们将采用 环绕通知。 说明 : 原始方法如果只执行一次,时间太快,两个时间差可能为 0 ,所以我们要执行万次来计算时间 差。package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* Created by IntelliJ IDEA.
*
* @Author : Runqiang Jiang
* @create 2022/5/15 11:08
*/
@Component
@Aspect
public class ProjectAdvice1 {
//匹配业务层的所有的方法(切入点)
@Pointcut("execution(* com.itheima.service.impl.AccountServiceImpl.*(..))")
public void servicePt(){}
// 通知方法
@Around("servicePt()")
public void renSpeed(ProceedingJoinPoint pjp) throws Throwable {
//代表了一次执行的签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行 *** 作名称(接口名)
String className=signature.getDeclaringTypeName();
//通过签名获取执行 *** 作名称(方法名)
String methodName=signature.getName();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
pjp.proceed();
}
long endTime = System.currentTimeMillis();
System.out.println("业务层接口千次执行时间:"+className+"."+methodName+"--->"+(endTime-startTime)+"ms");
}
}
4.4 AOP通知获取数据
目前我们写
AOP
仅仅是在原始方法前后追加一些 *** 作,接下来要说说
AOP
中数据相关的内容,将从获取
参数、获取返回值和获取异常
三个方面来研究切入点的相关信息。
前面介绍通知类型的时候总共讲了五种,那么对于这五种类型都会有参数,返回值和异常吗
?
先来一个个分析下
:
获取切入点方法的参数,所有的通知类型都可以获取参数
JoinPoint:适用于前置、后置、返回后、抛出异常后通知
ProceedingJoinPoint:适用于环绕通知
获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
返回后通知
环绕通知
获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
抛出异常后通知
环绕通知
例子:
package com.itheima.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* Created by IntelliJ IDEA.
*
* @Author : Runqiang Jiang
* @create 2022/5/15 11:42
*/
@Component
@Aspect
public class MyAdvice1 {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
//JoinPoint:用于描述切入点的对象,必须配置成通知方法中的第一个参数,可用于获取原始方法调用的参数
@Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ..." );
}
@After("pt()")
public void after(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("after advice ...");
}
//ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] =99999;
Object ret = null;
try {
ret = pjp.proceed(args);
} catch (Throwable t) {
t.printStackTrace();
}
return ret;
}
//设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(JoinPoint jp,String ret) {
System.out.println("afterReturning advice ..."+ret);
}
//设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
}
非环绕通知获取方式
在方法上添加
JoinPoint,
通过
JoinPoint
来获取参数
思考
:
方法的参数只有一个,为什么获取的是一个数组
?
因为参数的个数是不固定的,所以使用数组更通配些。
环绕通知获取方式
环绕通知使用的是
ProceedingJoinPoint
,因为
ProceedingJoinPoint
是
JoinPoint
类的子
类,所以对于
ProceedingJoinPoint
类中应该也会有对应的
getArgs()
方法,
对于返回值,只有返回后
AfterReturing
和环绕
Around
这两个通知类型可以获取,具体如何获取
?
环绕通知获取返回值
4.5 百度网盘密码数据兼容处理
需求
:
对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。
当我们从别人发给我们的内容中复制提取码的时候,有时候会多复制到一些空格,直接粘贴到百度 的提取码输入框,但是百度那边记录的提取码是没有空格的。这个时候如果不做处理,直接对比的话,就会引发提取码不一致,导致无法访问百度盘上的内容 ,所以多输入一个空格可能会导致项目的功能无法正常使用。此时我们就想能不能将输入的参数先帮用户去掉空格再 *** 作呢? 答案是可以的,我们只需要在业务方法执行之前对所有的输入参数进行格式处理 ——trim() 是对所有的参数都需要去除空格么 ? 也没有必要,一般只需要针对字符串处理即可。 以后涉及到需要去除前后空格的业务可能会有很多,这个去空格的代码是每个业务都写么 ? 可以考虑使用AOP来统一处理。 AOP 有五种通知类型,该使用哪种呢 ? 我们的需求是将原始方法的参数处理后在参与原始方法的调用,能做这件事的就只有环绕通知。 综上所述,我们需要考虑两件事 : ①:在业务方法执行之前对所有的输入参数进行格式处理 —— trim() ②:使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* Created by IntelliJ IDEA.
*
* @Author : Runqiang Jiang
* @create 2022/5/15 15:35
*/
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.ResourcesService.openURL(*,*))")
private void servicePt(){}
@Around("DataAdvice.servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
//对象数组
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
// 判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i]= args[i].toString().trim();
}
}
// 改完去掉空格的参数再放进去
Object proceed = pjp.proceed(args);
return proceed;
}
}
5,AOP总结
5.1 AOP
的核心概念
概念:
AOP(Aspect Oriented Programming)
面向切面编程,一种编程范式
作用:在不惊动原始设计的基础上为方法进行功能
增强
核心概念
代理(Proxy
):
SpringAOP
的核心本质是采用代理模式实现的
连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
切面(Aspect):描述通知与切入点的对应关系
目标对象(
Target
):被代理的原始对象成为目标对象
5.2
切入点表达式
切入点表达式标准格式:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常
名)
切入点表达式描述通配符:
作用:用于快速描述,范围描述
*:匹配任意符号(常用)
.. :匹配多个连续的任意符号(常用)
+:匹配子类类型
切入点表达式书写技巧
1.
按
标准规范
开发
2.
查询 *** 作的返回值建议使用
*
匹配
3.
减少使用
..
的形式描述包
4.
对接口
进行描述
,使用
*
表示模块名,例如
UserService
的匹配描述为
*Service 5.
方法名书写保留动
词,例如
get
,使用
*
表示名词,例如
getById
匹配描述为
getBy* 6.
参数根据实际情况灵活调整
5.3
五种通知类型
前置通知
后置通知
环绕通知(重点)
环绕通知依赖形参
ProceedingJoinPoint
才能实现对原始方法的调用
环绕通知可以隔离原始方法的调用执行
环绕通知返回值设置为
Object
类型
环绕通知中可以对原始方法调用过程中出现的异常进行处理
返回后通知
抛出异常后通知
1
execution(* com.itheima.service.*Service.*(..))
5.4
通知中获取参数
获取切入点方法的参数,所有的通知类型都可以获取参数
JoinPoint:适用于前置、后置、返回后、抛出异常后通知
ProceedingJoinPoint:适用于环绕通知
获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
返回后通知
环绕通知
获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
抛出异常后通知
环绕通知
6.1 Spring事务简介
6.1.1
相关概念介绍
事务作用:在数据层保障一系列的数据库 *** 作
同成功同失败
Spring
事务作用:在数据层或
业务层
保障一系列的数据库 *** 作同成功同失败
数据层有事务我们可以理解,为什么业务层也需要处理事务呢
?
举个简单的例子,
转账业务会有两次数据层的调用,一次是加钱一次是减钱
把事务放在数据层,加钱和减钱就有两个事务
没办法保证加钱和减钱同时成功或者同时失败
这个时候就需要将事务放在业务层进行处理。
Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager
commit
是用来提交事务,
rollback
是用来回滚事务。
PlatformTransactionManager
只是一个接口,
Spring
还为其提供了一个具体的实现
:
从名称上可以看出,我们只需要给它一个
DataSource
对象,它就可以帮你去在业务层管理事务。其 内部采用的是JDBC
的事务。所以说如果你持久层采用的是
JDBC
相关的技术,就可以采用这个事务管理 器来管理你的事务。而Mybatis
内部采用的就是
JDBC
的事务,所以后期我们
Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。
6.1.2
转账案例
-
需求分析
接下来通过一个案例来学习下
Spring
是如何来管理事务的。
先来分析下需求
:
需求: 实现任意两个账户间转账 *** 作
需求微缩: A账户减钱,B账户加钱
为了实现上述的业务需求,我们可以按照下面步骤来实现下
:
①:数据层提供基础 *** 作,指定账户减钱(outMoney
),指定账户加钱(
inMoney
)
②:业务层提供转账 *** 作(
transfer
),调用减钱与加钱的 *** 作
③:提供
2
个账号和 *** 作金额执行转账 *** 作
④:基于Spring整合MyBatis环境搭建上述 *** 作
运行单元测试类,会执行转账 *** 作,
Tom
的账户会减少
100
,
Jerry
的账户会加
100
。
这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如
:
import com.itheima.dao.AccountDao;
import com.itheima.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.*;
@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
应该还是
900
,
Jerry
应该还是
1100
,但是真正运行后会发现,并没有像我们想象的那样,
Tom
账户为
800
而
Jerry
还是
1100,100
块钱凭空消息了,银行乐疯了。如果把转账换个顺序,银行就该哭了。
不管哪种情况,都是不允许出现的,对刚才的结果我们做一个分析
:
①:程序正常执行时,账户金额
A
减
B
加,没有问题
②:程序出现异常后,转账失败,但是异常之前 *** 作成功,异常之后 *** 作失败,整体业务失败
当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而
Spring
的事务管理就是用来解决这类问题的。
Spring事务管理具体的实现步骤为:
步骤
1:
在需要被事务管理的方法上添加注解
public interface AccountService {
/**
* 转账 *** 作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional
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
可以写在接口类上、接口方法上、实现类上和实现类方法上
写在接口类上,该接口的所有实现类的所有方法都会有事务
写在接口方法上,该接口的所有实现类的该方法都会有事务
写在实现类上,该类中的所有方法都会有事务
写在实现类方法上,该方法上有事务
建议写在实现类或实现类的方法上
步骤
2:
在
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 dataSourceTransactionManager=new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
注意:
事务管理器要根据使用技术进行选择,
Mybatis
框架使用的是
JDBC
事务,可以直接使用
DataSourceTransactionManager
步骤
3
:开启事务注解
在
SpringConfig
的配置类中开启
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
步骤
4:
运行测试类
会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性。
6.2 Spring 事务角色 要理解两个概念,分别是 事务管理员和事务协调员 。 1. 未开启 Spring 事务之前 :
AccountDao 的 outMoney 因为是修改 *** 作,会开启一个事务 T1 AccountDao 的 inMoney 因为是修改 *** 作,会开启一个事务 T2 AccountService的transfer没有事务, 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行 就会导致数据出现错误 2. 开启 Spring 的事务管理后 transfer上添加了@Transactional注解,在该方法上就会有一个事务T AccountDao 的 outMoney 方法的事务 T1 加入到 transfer 的事务 T 中 AccountDao 的 inMoney 方法的事务 T2 加入到 transfer 的事务 T 中 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性 通过上面例子的分析,可以得到如下概念 : 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法 注意 : 目前的事务管理是基于 DataSourceTransactionManager和SqlSessionFactoryBean使用的是同一 个数据源 6.3 Spring 事务属性 除了这两个概念,还有就是事务的其他相关配置都有哪些,就是接下来的内容。 主要学习三部分内容 事务配置、转账业务追加日志、事务传播行为。 6.3.1 事务配置
上面这些属性都可以在@Transactional注解的参数上进行设置。 readOnly : true 只读事务, false 读写事务,增删改要设为 false, 查询设为 true 。 timeout: 设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚, -1 表示不设置超 时时间。 rollbackFor: 当出现指定异常进行事务回滚 noRollbackFor: 当出现指定异常不进行事务回滚 思考 : 出现异常事务会自动回滚,这个是我们之前就已经知道的 noRollbackFor 是设定对于指定的异常不回滚,这个好理解 rollbackFor 是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定 ? 这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚
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);
}
public interface AccountService {
//rollback:设置当前事务参与回滚的异常,默认非运行时异常不参与回滚
@Transactional( rollbackFor = IOException.class)
public void transfer(String out,String in ,Double money) throws IOException;
}
rollbackForClassName
等同于
rollbackFor,
只不过属性为异常的类全名字符串
noRollbackForClassName
等同于
noRollbackFor
,只不过属性为异常的类全名字符串
isolation设置事务的隔离级别
DEFAULT :默认隔离级别, 会采用数据库的隔离级别
READ_UNCOMMITTED : 读未提交
READ_COMMITTED : 读已提交
REPEATABLE_READ : 重复读取
SERIALIZABLE: 串行化
6.3.2
转账业务追加日志案例
6.3.2.1
需求分析
在前面的转案例的基础上添加新的需求,完成转账后记录日志。
需求:实现任意两个账户间转账 *** 作,并对每次转账 *** 作在数据库进行留痕
需求微缩:
A
账户减钱,
B
账户加钱,数据库记录日志
基于上述的业务需求,我们来分析下该如何实现
:
①:基于转账 *** 作案例添加日志模块,实现数据库中记录日志
②:业务层转账 *** 作(transfer),调用减钱、加钱与记录日志功能
需要注意一点就是,我们这个案例的预期效果为
:
无论转账 *** 作是否成功,均进行转账 *** 作的日志留痕
添加
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); }
}
在转账的业务中添加记录日志
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}
步骤
5:
运行程序
当程序正常运行,
tbl_account
表中转账成功,
tbl_log
表中日志记录成功
当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表
未添加数据
这个结果和我们想要的不一样,什么原因
?
该如何解决
?
失败原因:日志的记录与转账 *** 作隶属同一个事务,同成功同失败
最终效果:无论转账 *** 作是否成功,日志必须保留
6.3.3
事务传播行为
对于上述案例的分析 : log方法、inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3 transfer 因为加了 @Transactional 注解,也开启了事务 T Spring事务会把T1,T2,T3都加入到事务T中 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来 这和需求不符,这个时候我们就想能不能让 log 方法单独是一个事务呢 ? 要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是 事务传播行为:事务协调员对事务管理员所携带事务的处理态度。 具体如何解决,就需要用到之前我们没有说的 propagation属性 。
public interface LogService {
//propagation设置事务属性:传播行为设置为当前 *** 作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
具体视频链接:
黑马程序员2022最新SSM框架教程_Spring+SpringMVC+Maven高级+SpringBoot+MyBatisPlus企业实用开发技术_哔哩哔哩_bilibili
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)