Spring 学习第六天:事务管理与 AOP
在后端开发中,保证数据的一致性至关重要。然而,在实际开发中总会有各种异常情况发生,导致数据可能出现问题。本文将通过一个示例讲解如何使用 事务管理 和 AOP 解决这些问题。
事务管理
场景描述
假设我们有两张表:
dept
表:存储部门信息
ID | 部门名称 | 创建时间 | 更新时间 |
---|---|---|---|
1 | 销售部 | 2024-11-06 13:36:45 | 2024-11-11 16:56:02 |
2 | 教研部 | 2024-11-06 13:36:45 | 2024-11-06 13:36:45 |
3 | 咨询部 | 2024-11-06 13:36:45 | 2024-11-06 13:36:45 |
4 | 就业部 | 2024-11-06 13:36:45 | 2024-11-06 13:36:45 |
5 | 人事部 | 2024-11-06 13:36:45 | 2024-11-06 13:36:45 |
7 | 司令部 | 2024-11-11 15:28:31 | 2024-11-11 15:28:31 |
9 | 学工部 | 2024-11-11 16:56:43 | 2024-11-11 16:56:43 |
2.emp
表:存储员工信息
员工ID | 姓名 | 性别 | 年龄 | 职位 | 部门ID |
---|---|---|---|---|---|
101 | 张三 | 男 | 28 | 销售经理 | 1 |
102 | 李四 | 男 | 25 | 销售专员 | 1 |
103 | 王五 | 女 | 30 | 教研主任 | 2 |
104 | 赵六 | 女 | 26 | 教学助理 | 2 |
105 | 钱七 | 男 | 35 | 高级咨询师 | 3 |
106 | 孙八 | 女 | 24 | 咨询顾问 | 3 |
107 | 周九 | 男 | 32 | 就业指导员 | 4 |
108 | 吴十 | 女 | 27 | 人事专员 | 5 |
109 | 郑十一 | 男 | 40 | 人事主管 | 5 |
110 | 王十二 | 男 | 38 | 部门司令 | 7 |
111 | 刘十三 | 女 | 29 | 学工辅导员 | 9 |
112 | 陈十四 | 男 | 26 | 学工助理 | 9 |
我们需要实现 删除某个部门 的操作,步骤如下:
- 删除
dept
表中对应部门的记录。 - 删除
emp
表中该部门的所有员工。
public void DeleteById(Integer id) {
//删除部门
deptMapper.delete(id);
//删除员工Id
empMapper.deleteByDeptId(id);
}
通过调用对于的mapper执行删除操作,这切看起来都非常合理,但是这里面存在一个很大的问题是,如果deptMapper.delete(id);
顺利执行,但是empMapper.deleteByDeptId(id);
中出现了某种异常,导致执行失败,程序会抛出异常,导致员工无法正常删除,员工的脏数据就会一直存在数据库中。
解决方案:事务管理
Spring 提供了事务管理的支持,该注解会为标注的方法或类提供事务支持,确保这些方法中的数据库操作要么全部成功提交(commit
),要么在出错时全部回滚(rollback
),在此注解中,rollbackFor = Exception.class
表示所有异常(包括运行时异常和受检异常)都会触发事务回滚。也就是说即使发生异常,我们可以返回错误信息,撤销已经完成的删除操作,直到异常解决。
通过 @Transactional
注解,可以实现以下效果:
- 所有操作要么全部成功提交(
commit
), - 要么在发生异常时全部回滚(
rollback
)。
@Transactional(rollbackFor = Exception.class)
public void DeleteById(Integer id) {
// 删除部门
deptMapper.delete(id);
// 删除员工
empMapper.deleteByDeptId(id);
}
@Transactional
注解:声明该方法需要事务支持。rollbackFor = Exception.class
:指明发生任何异常时,回滚所有操作。
这样,若 empMapper.deleteByDeptId(id)
抛出异常,整个事务将回滚,deptMapper.delete(id)
的删除操作也会撤销,从而保证数据一致性。
AOP:面向切面编程
什么是 AOP?
AOP(Aspect Oriented Programming) 是一种编程思想,用于动态地将某些通用功能(如日志记录、性能监控、事务管理等)附加到目标方法中,而无需修改目标方法的代码。它由以下几个部分组成
- 连接点(JoinPoint)
可以被 AOP 控制的方法(暗含方法执行时的相关信息) - 通知(Advice)
指定义的重复逻辑,也就是共性功能(最终体现为一个方法) - 切入点(PointCut)
匹配连接点的条件,通知仅会在符合切入点的条件时应用 ) - 切面(Aspect)
描述通知与切入点的对应关系(通知 + 切入点) - 目标对象(Target)
通知所应用的对象
我一向不爱听繁琐的概念,上面的AOP的定义对于入门实在太不友好了,下面我们举一个小例子来解释AOP是什么,能干什么。
假定我们要给项目中的每一个实现方法都计算它的运行时间,最简单暴力的当然是在每一个方法的开头记录一个时间,在末尾也记录一个时间,两个时间的插值就得到了方法的运行时间。
示例:方法耗时统计
常规实现(不使用 AOP)
如果我们想统计项目中所有方法的运行时间,传统做法是直接在每个方法内添加以下代码:
long begin = System.currentTimeMillis();
// 在这中间写执行的方法
//方法。。。。。
long end = System.currentTimeMillis();
long costTime = end - begin;
这段代码要加到每一个方法前面,实在太繁琐了。这时候就到AOP大显身手了。简单来说,AOP支持我们将任何方法夹杂到一串我们自定义的代码中(可以前中后任何地方),所以我们只需要通过AOP加到上面那串计算时间的代码中即可。通过 AOP,我们可以将 运行时间统计的逻辑 单独抽取出来,并自动应用到指定的方法上。
实现步骤
- 定义一个切面类
@Component
@Aspect
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.practice.Service.*.*(..))") // 切入点表达式
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录开始时间
long begin = System.currentTimeMillis();
// 执行原方法
Object result = joinPoint.proceed();
// 记录结束时间
long end = System.currentTimeMillis();
// 计算耗时
long costTime = end - begin;
log.info("方法耗时: {} ms", costTime);
return result;
}
}
声明的方式与普通类的定义方式没什么区别,不同之处在于在类上要声明@Aspect
注解,表明这是一个拦截器,
代码中
@Around
:声明拦截类型,表示在目标方法前后执行代码。execution
表达式:定义拦截的目标方法范围,例如com.itheima.practice.Service
包下的所有方法。joinPoint.proceed()
:调用原始目标方法。
- 效果
AOP 自动为指定范围内的方法插入耗时统计代码,无需手动在每个方法中重复写逻辑。
代表你要拦截哪些方法,这里我要拦截的是我的目录中com.itheima.practice.Service的所有方法,切入点表达式中写你的方法所在的相对路径即可,关于切入点表达式这里不细讲。
切入点与通知类型
Spring AOP 提供多种通知类型,根据不同的需求可以选择合适的拦截时机。
-
@Before
在目标方法执行前运行通知逻辑。 -
@After
在目标方法执行后运行通知逻辑,无论成功或异常都会执行。 -
@AfterReturning
在目标方法成功执行后运行通知逻辑(不会拦截异常)。 -
@AfterThrowing
在目标方法抛出异常后运行通知逻辑。 -
@Around
环绕目标方法执行,在目标方法的前后都可以插入逻辑(包含成功和异常场景)。