2025.05.29

JavaWeb学习之JavaWebWeb事务管理&AOP

一、Web事务管理

1.1 事务的概念

  • 事务是指一组操作的集合,这些操作要么全部成功,要么全部失败。
  • 事务的操作:
    • 开启事务(一组操作开始前,开启事务) :start transaction / begin
    • 提交事务(这组操作全部成功后,提交事务) :commit
    • 回滚事务(中间任何一个操作出现异常,回滚事务) :rollback
  • Spring事务管理:
    • 注解:@Transactional
    • 位置:业务(service)层的方法上、类上、接口上
    • 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
    • spring事务管理日志:
    1
    2
    3
    logging:
    level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug
  • 案例:解散部门:删除部门,同时删除该部门下的员工
    • DeptServiceIml.java
    1
    2
    3
    4
    5
    6
    7
    @Transactional  // spring事务管理
    @Override
    public void delete(Integer id) {
    deptMapper.deleteById(id);

    empMapper.deleteByDeptId(id); // 根据部门id删除部门
    }
    • EmpMapper.java
    1
    2
    3
    4
    5
    /*
    根据部门id删除员工
    */
    @Delete("delete from emp where dept_id = #{deptId}")
    void deleteByDeptId(Integer deptId);

1.2 事务进阶-rollbackFor

  • rollbackFor
    • 默认情况下,只有出现RuntimeException异常,才会回滚事务
    • rollbackFor属性用于控制出现何种异常类型时回滚事务
    • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Transactional(rollbackFor = Exception.class)  // spring事务管理
    @Override
    public void delete(Integer id) {
    deptMapper.deleteById(id); // 删除部门

    // 模拟异常
    if (true) {
    throw new Exception("模拟异常,回滚事务");
    }

    empMapper.deleteByDeptId(id); // 根据部门id删除员工
    }

1.3 事务进阶-propagation

  • propagation

    • 事务传播行为,指的就是当一个事物方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
    • 常用的事务传播行为
      • REQUIRED:大部分情况下都是用该传播行为即可
      • REQUIRES_NEW:当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功
    属性值 含义
    REQUIRED [默认值]需要事务,有则加入,无则创建新事务
    REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
    SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
    NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
    MANDATORY 必须有事务,否则抛异常
    NEVER 必须有事务,否则抛异常
    -
  • 案例:解散部门时,记录操作日志

    • 需求:解散部门时,无论成功还是失败,都需要记录操作日志
    • 步骤:
      1. 解散部门:删除部门、删除部门下的员工
      2. 记录日志到数据库表中
    • DeptServiceImpl.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Transactional(rollbackFor = Exception.class)  // spring事务管理
    @Override
    public void delete(Integer id) {
    try {
    deptMapper.deleteById(id);
    int i= 1/0;
    empMapper.deleteByDeptId(id); // 根据部门id删除部门
    } finally {
    // 记录操作日志
    DeptLog deptLog = new DeptLog();
    deptLog.setCreateTime(LocalDateTime.now());
    deptLog.setDescription("执行了解散部门的操作,此次解散的部门是" + id + "号部门");
    deptLogService.insert(deptLog);
    }
    }
    • DeptLogServiceImpl.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Service
    public class DeptLogServiceImpl implements DeptLogService {
    @Autowired
    private DeptLogMapper deptLogMapper;

    // 无论如何都会开启新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void insert(DeptLog deptLog) {
    deptLogMapper.insert(deptLog);
    }
    }

二、AOP

2.1 AOP概述

  • AOP(Aspect Oriented Programming)面向切面编程,其实就是面向特定方法编程
  • 场景
    • 案例部分功能运行慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时
  • 实现
    • 动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术, 旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程
  • AOP入门:统计各个业务层方法执行耗时
    • 导入依赖:在pom.xml中添加Spring AOP依赖
    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    • 编写AOP程序:针对特定方法根据业务需要进行编程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Slf4j
    @Component
    @Aspect // 标识该类为AOP类
    public class TimeAspect {

    @Around("execution(* com.itheima.service.*.*(..))") // 切入点表达式
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
    // 1. 记录开始时间
    long begin = System.currentTimeMillis();

    // 2. 调用原始方法运行
    Object result = joinPoint.proceed();

    // 3. 记录时间,计算方法执行时间
    long end = System.currentTimeMillis();

    log.info(joinPoint.getSignature() + "方法执行耗时:{}ms", end - begin);
    return result;
    }
    }

2.2 AOP核心概念

AOP核心概念

AOP执行流程

  • AOP会生成目标对象的增强版,也就是代理对象,之后Controller层在调用对象时调用的就不是原来的目标对象了,而是进行过方法增强的代理对象

2025.05.31

2.3 AOP通知类型

  • 通知类型
    • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
    • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
    • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
    • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
    • @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
  • 注意事项
    • @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
    • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值
  • 引用切入点表达式
    • @PointCut:该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切点表达式即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Slf4j
    @Component
    @Aspect // 标识该类为AOP类
    pubilc class MyAspect {

    @Pointcut("execution(* com.itheima.service.imp.DeptServiceImpl.*(..))") // 切入点表达式
    public void pt() {} // 定义切入点

    @Before("pt()") // 引用切入点表达式
    public void before() {
    log.info("before...")
    }
    }

2.4 通知顺序

  • 通知顺序:当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会执行
  • 执行顺序
    1. 不同切面类中,默认按照切面类的类名字母排序
      • 目标方法前的通知方法:字母排名靠前的先执行
      • 目标方法后的通知方法:字母排名靠前的后执行
    2. 用@Order(数字)加在切面类上来控制顺序
      • 目标方法前的通知方法:数字小的先执行
      • 目标方法后的通知方法:数字小的后执行

2.5 切入点表达式

  • 切入点表达式:描述切入点方法的一种表达式
  • 作用:主要用来决定项目中的哪些方法需要加入通知
  • 常见形式
    • execution(…):根据方法的签名来匹配
    • @annotation(…):根据注解来匹配

2.5.1 execution

  • execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
1
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
  • 其中带?的表示可以省略
    • 访问修饰符:如public、private等
    • 包名.类名
    • throws 异常:注意是方法上声明抛出的异常,不是实际抛出的异常
  • 可以使用通配符描述切入点
    • *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
    • …:多个连续的任意符号,可以通配任意层级的包或任意类型、任意个数的参数
  • 注意事项
    • 根据业务需要,可以使用且(&&)、或(||)、非(!)等逻辑运算符来组合多个切入点表达式
  • 书写建议
    • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是find开头,更新类方法都是update开头
    • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
    • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用…使用*匹配单个包

2.5.2 @annotation

  • @annotation主要用于匹配标识有特定注解的方法
1
@annotation(com.itheima.anno.MyLog)
1
2
3
4
5
// 自定义注解MyLog
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}

2.6 连接点

  • 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
    • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
    • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
  • 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
@Component
@Aspect // 标识该类为AOP类
pubilc class MyAspect {

@Pointcut("execution(* com.itheima.service.imp.DeptServiceImpl.*(..))") // 切入点表达式
public void pt() {} // 定义切入点

@Before("pt()") // 引用切入点表达式
public void before(JoinPoint joinPoint) {
log.info("before...")
}

@Around("pt()") // 引用切入点表达式
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取连接点信息
String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名
String methodName = joinPoint.getSignature().getName(); // 获取方法名
Object[] args = joinPoint.getArgs(); // 获取方法参数

log.info("执行的类名: {}, 方法名: {}, 参数: {}", className, methodName, Arrays.toString(args));

// 执行原始方法
return joinPoint.proceed();
}
}

2.7 案例

  • 需求: 将案例中增、删、改相关接口的操作日志记录到数据库表中

  • 操作日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时的参数、返回值、方法执行时长

  • 操作日志表

1
2
3
4
5
6
7
8
9
10
11
-- 操作日志表
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 '操作日志表';
  • 操作日志实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@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; //操作耗时
}
  • 操作日志Mapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itheima.mapper;

import com.itheima.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@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);

}
  • 自定义注解Log
1
2
3
4
5
6
7
8
9
10
11
package com.itheima.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}
  • 切面类LogAspect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.itheima.aop;

import com.alibaba.fastjson.JSONObject;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Arrays;

@Slf4j
@Component
@Aspect
public class LogAspect {

@Autowired
private HttpServletRequest request;

@Autowired
private OperateLogMapper operateLogMapper;

@Around("@annotation(com.itheima.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {

// 操作人ID
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUser = (Integer) claims.get("id");

// 操作时间
LocalDateTime operateTime = LocalDateTime.now();

// 操作类名
String className = joinPoint.getTarget().getClass().getName();

// 操作方法名
String methodName = joinPoint.getSignature().getName();

// 操作方法参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);

long startTime = System.currentTimeMillis();
// 调用原始方法运行
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();

// 方法返回值
String returnValue = JSONObject.toJSONString(result);

// 操作耗时
Long costTime = endTime - startTime;

// 记录操作日志
OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className, methodName, methodParams, returnValue, costTime);
operateLogMapper.insert(operateLog);

log.info("AOP记录操作日志:{}", operateLog);
return result;
}
}