菜鸟之路Day40一一事物管理&AOP
作者:blue
时间:2025.6.6
0.概述
文章内容大部分学习自黑马程序员BV1m84y1w7Tb
1.事物管理
1.1事物回顾
事物是一组操作的集合,它是一个不可分割的工作单位。事物会把所有操作作为一个整体一起向系统提交或者撤销操作请求,即这些操作要么同时成功,要么同时失败
-- 事物控制
-- 开启事物
start transaction;
或
begin;
-- 提交事物
commit;
-- 回滚事物
rollback;
1.2Spring事物管理
注解:@Transactional
位置:业务(Service)层的方法上,类上,接口上
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
案例:原本项目中删除部门的操作,我们仅仅只是将部门删除,但是没有删除该部门下的员工,所以我们应该在删除部门的逻辑下加上删除对应的员工
DeptServiceImpl
@Transactional//将当前方法交给spring进行事务管理
@Override
public void delete(Integer id) {
//根据部门id删除部门
deptMapper.delete(id);
//如果此处出现异常,没有利用事物,将导致第一个语句执行
//下面的语句没执行,导致数据不一致
//int i = 1/0;模拟异常情况
//根据部门id删除对应员工
empMapper.deleteByDeptId(id);
}
EmpMapper
//根据部门id删除员工
@Delete("delete from emp where dept_id = #{id}")
void deleteByDeptId(Integer id);
配置文件中开启事物管理日志
#开启Spring事物管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
1.3注解:@Transactional
属性一:rollbackFor
默认情况下,只有出现 RuntimeException 才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。
@Transactional(rollbackFor = Exception.class)//通过这样设置,就可以令所有异常都回滚了
属性二:propagation

应用于事物传播的场景
事物传播,一个事物中调用了另一个带有@Transactional的事物,这时如果是:
REQUIRED:则两个操作共用一个事物
REQUIRES NEW:当我们不希望事务之间相互影响时,可以使用该传播行为。也就是直接建立两个事物。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
2.AOP基础
2.1AOP概述
介绍:
面向切面编程(AOP)是一种编程范式,它允许你将那些影响多个模块的通用功能(如日志记录、事务管理、权限验证等)提取出来,封装成独立的 “切面”,然后在不修改原有代码的情况下,将这些切面动态地切入到需要它们的地方。
举个生活化的例子帮助你理解:
想象一下,你正在举办一场大型晚宴,有很多客人参加。作为主人,你需要完成一系列任务:迎接客人、上菜、收拾餐具、送客等。这些任务中的一部分(比如迎接客人和送客)对于每一位客人来说都是重复的,但又不是晚宴的核心活动(比如烹饪美食)。
传统的编程方式可能会让你在接待每一位客人的代码中都重复编写迎接和送客的逻辑,这会导致代码冗余。而 AOP 的做法是,将迎接客人和送客这两个通用的任务提取出来,形成一个独立的 “切面”。然后,在需要的地方(比如客人到达和离开时)自动应用这个切面,而不需要在核心的烹饪代码中混入这些无关的逻辑。
这样做的好处是:
- 代码复用:避免了重复编写相同的代码。
- 可维护性:当需要修改迎接或送客的方式时,只需要在一个地方修改。
- 关注点分离:核心业务逻辑(烹饪)和通用功能(接待)被清晰地分开。
在技术实现上,AOP 通常通过 “代理模式” 或 “字节码增强” 来实现。它允许你在方法执行前后、抛出异常时等特定点插入额外的代码,而不需要修改原始方法。常见的应用场景包括日志记录、性能监控、事务管理、权限控制等。
实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
引入AOP依赖
<!--AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
AOP快速入门:计算service层各个方法的运行时间
@Slf4j
@Component
@Aspect //声明该类为AOP类
public class TimeAspect {
@Around("execution(* com.bluening.talis_web_demo.service.*.*(..))") //切入点表达式
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//1.记录开始时间
long startTime = System.currentTimeMillis();
//2.调用原始方法运行
Object result = joinPoint.proceed();
//3.记录结束时间,计算方法耗时
long endTime = System.currentTimeMillis();
log.info(joinPoint.getSignature()+"方法执行耗时: {} ms", endTime - startTime);
return result;
}
}
2.2AOP核心概念
- 切面(Aspect)
定义:切面是一个模块化的单元,它将横切多个对象的通用功能(如日志、权限验证)封装在一起。
类比:
想象你在经营一家餐厅,所有顾客的 “点餐 - 用餐 - 结账” 流程是核心业务。但有一些通用的事情需要在多个环节处理,比如:
- 卫生检查(每桌客人离开后消毒)
- 安全监控(全程录像)
- 会员积分(结账时自动累计)
这些通用功能就是 “切面”—— 它们与核心业务(点餐、用餐)无关,但需要在多个地方执行。
- 连接点(Join Point)
定义:程序执行过程中的某个特定点(如方法调用、异常抛出)。
类比:
在餐厅场景中,连接点就是流程中的 “事件点”,例如:
- 顾客入座
- 服务员上菜
- 顾客结账离开
每个事件点都可以成为插入通用功能的 “钩子”。
- 切入点(Pointcut)
定义:切入点是一个表达式,用于匹配多个连接点。
类比:
如果你想对 “所有 VIP 顾客的结账流程” 增加额外服务(如赠送甜点),那么:
- 连接点:所有顾客的结账事件
- 切入点:筛选出 VIP 顾客的结账事件
切入点就像一个 “过滤器”,只让符合条件的连接点执行特定的通用功能。
- 通知(Advice)
定义:通知是切面在特定连接点执行的代码(即具体要做的事)。
类比:
针对前面的例子,通知就是具体的 “额外服务”:
- 前置通知(Before):VIP 顾客入座前,提前准备好专属菜单。
- 后置通知(After):VIP 顾客结账后,赠送甜点券。
- 环绕通知(Around):全程为 VIP 顾客提供优先服务(替代普通服务)。
3.AOP进阶
3.1通知类型
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
注意事项:
@Around环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为0bject,来接收原始方法的返回值。
抽取切点表达式:
//该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可
@PointCut("execution(* com.bluening.talis_web_demo.service.*.*(..)))")
public void pt(){
}
// ⬆
//private:仅能在当前切面类中引用该表达式
//public:在其他外部的切面类中也可以引用该表达式
//直接引用
@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
}
3.2通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
执行顺序:
1.不同切面类中,默认按照切面类的类名字母排序
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
2.用 @Order(数字) 加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
3.3切入点表达式
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
execution(.):根据方法的签名来匹配
@annotation(.):根据注解匹配
3.3.1execution
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分
访问修饰符:可省路(比如:public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点
*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
3.3.2annotation
匹配带特定注解的方法
语法:@annotation(注解全路径)
示例:
@Pointcut("@annotation(com.example.annotation.Loggable)")
public void loggableMethods() {
}
- 匹配所有标注了
@Loggable注解的方法。
注意这个@Loggable注解是自定义的

@Target(ElementType.METHOD) // 注解作用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可见
public @interface Loggable {
}
书写建议
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头。
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
在满足业务需要的前提下尽量缩小切)、点的匹配范围。如:包名匹配尽量不使用…,使用 * 匹配单个包。
3.4连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等、
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
@Around("execution(* com.bluening.talis_web_demo.service.*.*(..))") //切入点表达式
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//1.获取目标对象的类名
String className = joinPoint.getTarget().getClass().getName();
log.info("目标对象的类名:{}",className);
//2.获取目标方法的方法名
String methodName = joinPoint.getSignature().getName();
log.info("目标方法的方法名:{}",methodName);
//3.获取目标方法运行是传入的参数
Object[] args = joinPoint.getArgs();
log.info("目标方法运行时传入参数:{}", Arrays.toString(args));
//4.放行目标方法执行
Object result = joinPoint.proceed();
//5.获取目标方法运行的返回值
log.info("目标方法运行的返回值:{}",result);
return result;
}
4.综合案例
将案例中 增、删、改 相关接口的操作日志记录到数据库表中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
分析:需要对所有业务类中的增、删、改 方法添加统一功能,使用 AOP 技术最为方便 @Around 环绕通知
由于增、删、改 方法名没有规律,可以自定义 @Log 注解完成目标方法匹配
步骤:
准备:在案例工程中引入AOP的起步依赖
准备好的数据库表结构,并引入对应的实体类
编码:
自定义注解 @Log
定义切面类,完成记录操作日志的逻辑
日志表
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
OperateLogMapper
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
自定义注解
@Target(ElementType.METHOD) // 注解作用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可见
public @interface Log {
}
AOP
获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id
@Slf4j
@Component
@Aspect //声明该类为AOP类
public class LogAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.bluening.talis_web_demo.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//1.获取操作人id --其实就是获取用户ID
String jwt = request.getHeader("token");//获取令牌
Claims claims = JwtUtils.parseJWT(jwt);//解析令牌
Integer operateUser = (Integer) claims.get("id");//获取id
//2.操作的时间
LocalDateTime operateTime = LocalDateTime.now();
//3.执行方法的全类名
String className = joinPoint.getTarget().getClass().getName();
//4.执行方法名
String methodName = joinPoint.getSignature().getName();
//5.方法运行时参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
//6.返回值
Long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();//运行原方法
Long endTime = System.currentTimeMillis();
String resultValue = JSONObject.toJSONString(result);//将结果转成json
//7.方法执行时长
Long costTime = endTime - startTime;
//8.创建日志对象
OperateLog operateLog = new OperateLog(null,operateUser,
operateTime,className,methodName,methodParams,resultValue,costTime);
//9.记录日志
operateLogMapper.insert(operateLog);
return result;
}
}
前后端联调之后,发现数据已经记录进来