一、事务管理
1. Spring事务管理
(1) 案例
① 步骤
-
根据ID删除部门数据
-
根据部门ID删除该部门下的员工
② 代码实现
DeptServiceImpl
java
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Override
@Transactional //当前方法添加了事务管理
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常发生
int i = 1/0;
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
DeptMapper
java
@Mapper
public interface DeptMapper {
/**
* 根据id删除部门信息
* @param id 部门id
*/
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
}
EmpMapper
java
@Mapper
public interface EmpMapper {
//根据部门id删除部门下所有员工
@Delete("delete from emp where dept_id=#{deptId}")
public int deleteByDeptId(Integer deptId);
}
(2) Transactional注解
**① 作用:**在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
② 注解: 我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
2. 事务进阶
(1) rollbackFor
假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
java
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Override
@Transactional(rollbackFor=Exception.class)
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常发生
int num = id/0;
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
(2) propagation
① 常见的事务传播行为:
| 属性值 | 含义 |
|---|---|
| REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
| REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
| SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
| NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
| MANDATORY | 必须有事务,否则抛异常 |
| NEVER | 必须没事务,否则抛异常 |
对于这些事务传播行为,我们只需要关注以下两个就可以了:
-
REQUIRED(默认值):大部分情况下都是用该传播行为即可。
-
REQUIRES_NEW:当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
② 代码实现
java
@Slf4j
@Service
//@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
//根据部门id,删除部门信息及部门下的所有员工
@Override
@Log
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) throws Exception {
try {
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常
if(true){
throw new Exception("出现异常了~~~");
}
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}finally {
//不论是否有异常,最终都要执行的代码:记录日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
//调用其他业务类中的方法
deptLogService.insert(deptLog);
}
}
//省略其他代码...
}
二、AOP基础
1. AOP快速入门
(1) 实现步骤
-
导入依赖:在pom.xml中导入AOP的依赖
-
编写AOP程序:针对于特定方法根据业务需要进行编程
(2) 代码实现
pom.xml
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
AOP程序:TimeAspect
java
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录方法执行结束时间
long end = System.currentTimeMillis();
//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);
return result;
}
}
核心拆解:execution(* com.itheima.service.*.*(..)) 切入点表达式
① 语法格式:
execution( [访问修饰符] 返回值类型 包路径.类名.方法名(参数列表) )
② 各部分含义逐一拆解(按从左到右顺序):
| 表达式片段 | 具体含义 | 补充说明 |
|---|---|---|
* |
方法返回值类型为「任意类型」 | 若指定返回值(如String),则仅匹配返回值为该类型的方法 |
com.itheima.service |
目标方法所在的固定包路径 | 仅匹配这个包下的类中的方法,子包下的类不会被匹配 |
第一个* |
匹配该包下的任意类(包括普通类、实现类等) | 若指定类名(如UserService),则仅匹配该类中的方法 |
第二个* |
匹配类中的任意方法名 | 若指定方法名(如findById),则仅匹配该方法 |
(..) |
匹配方法的任意参数列表 | 无参数、1个参数、多个参数都能匹配;若指定参数(如(Long)),则仅匹配该参数类型的方法 |
2. AOP核心概念
(1) 连接点:JoinPoint
可以被AOP控制的方法(所有方法都可以算作是连接点,暗含方法执行时的相关信息)
连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法。

(2) 通知:Advice
指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

(3) 切入点:PointCut
匹配连接点的条件,通知仅会在切入点方法执行时被应用**(实际被AOP控制的方法)**
在通知当中,我们所定义的共性功能到底要应用在哪些方法上?此时就涉及到了切入点pointcut概念。切入点指的是匹配连接点的条件。通知仅会在切入点方法运行时才会被应用。

(4) 切面:Aspect
描述通知与切入点的对应关系(通知+切入点)
当通知和切入点结合在一起,就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。

(5) 目标对象:Target
通知所应用的对象
目标对象指的就是通知所应用的对象,我们就称之为目标对象。

三、AOP进阶
1. 通知类型
(1) Spring中AOP的通知类型:
-
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
-
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
-
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
-
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
-
@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
(2) 代码实现
java
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
(3) 解决切入点表达式重复的问题
① 解决方案
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
② 代码实现
java
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){
}
//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行
log.info("around after ...");
return result;
}
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
需要注意的是:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:
全类名.方法名(),具体形式如下:
java
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("com.itheima.aspect.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
}
}
2. 通知顺序
(1) 修改执行顺序的方式
使用Spring提供的@Order注解
(2) 代码示例
java
@Slf4j
@Component
@Aspect
@Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect4 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect4 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect4 -> after ...");
}
}
通知的执行顺序大家主要知道两点即可:
-
不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
-
可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序
3. 切入点表达式
(1) execution
① 语法格式:
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
java
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分(第一个、第三个一般会省略)
-
访问修饰符:可省略(比如: public、protected)
-
包名.类名: 可省略(一般不省略)
-
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
② 示例:
java
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
③ 可以使用通配符描述切入点
-
*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 -
..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
④ 切入点表达式的语法规则:
-
方法的访问修饰符可以省略
-
返回值可以使用`*`号代替(任意返回值类型)
-
包名可以使用`*`号代替,代表任意包(一层包使用一个`*`)
-
使用`..`配置包名,标识此包以及此包下的所有子包
-
类名可以使用`*`号代替,标识任意类
-
方法名可以使用`*`号代替,表示任意方法
-
可以使用 `*` 配置参数,一个任意类型的参数
-
可以使用`..` 配置参数,任意个任意类型的参数
⑤ 切入点表达式示例
- 省略方法的修饰符号
java
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
- 使用
*代替返回值类型
java
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
- 使用
*代替参数
java
execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))
- 使用
..省略参数
java
execution(* com..*.*(..))
(2) @annotation
① 实现步骤
-
编写自定义注解
-
在业务类要做为连接点的方法上添加自定义注解
② 代码实现
自定义注解:MyLog
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
业务类:DeptServiceImpl
java
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
}
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
@Override
public void save(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
}
@Override
public Dept getById(Integer id) {
return deptMapper.getById(id);
}
@Override
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
}
}
切面类
java
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.itheima.anno.MyLog)")
public void before(){
log.info("MyAspect6 -> before ...");
}
//后置通知
@After("@annotation(com.itheima.anno.MyLog)")
public void after(){
log.info("MyAspect6 -> after ...");
}
}
4. 连接点
(1) 定义
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
-
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
-
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
(2) 代码实现
java
@Slf4j
@Component
@Aspect
public class MyAspect7 {
@Pointcut("@annotation(com.itheima.anno.MyLog)")
private void pt(){}
//前置通知
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ...");
}
//后置通知
@Before("pt()")
public void after(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取目标类名
String name = pjp.getTarget().getClass().getName();
log.info("目标类名:{}",name);
//目标方法名
String methodName = pjp.getSignature().getName();
log.info("目标方法名:{}",methodName);
//获取方法执行时需要的参数
Object[] args = pjp.getArgs();
log.info("目标方法参数:{}", Arrays.toString(args));
//执行原始方法
Object returnValue = pjp.proceed();
return returnValue;
}
}
四、AOP案例
1. 需求
需求:将案例中增、删、改相关接口的操作日志记录到数据库表中
- 就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。
2. 思路分析
(1) 既然要基于AOP面向切面编程的方式来完成的功能,那么我们要使用 AOP五种通知类型当中的哪种通知类型?
- 答案:环绕通知
方法返回值,是在原始方法执行后才能获取到的。
方法的运行时长,需要原始方法运行之前记录开始时间,原始方法运行之后记录结束时间。通过计算获得方法的执行耗时。
(2) 切入点表达式我们该怎么写?
- 答案:使用annotation来描述表达式
要匹配业务接口当中所有的增删改的方法,而增删改方法在命名上没有共同的前缀或后缀。此时如果使用execution切入点表达式也可以,但是会比较繁琐。 当遇到增删改的方法名没有规律时,就可以使用 annotation切入点表达式
3. 步骤
(1) 准备工作:
-
引入AOP的起步依赖
-
导入资料中准备好的数据库表结构,并引入对应的实体类
(2) 编码实现:
-
自定义注解@Log
-
定义切面类,完成记录操作日志的逻辑
4. 代码实现
(1) 实体类
java
//操作日志实体类
@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; //操作耗时
}
(2) Mapper接口
java
@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);
}
(3) 自定义注解@Log
java
/**
* 自定义Log注解
*/
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
(4) 修改业务实现类,在增删改业务方法上添加@Log注解
java
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
@Log
public void update(Emp emp) {
emp.setUpdateTime(LocalDateTime.now()); //更新修改时间为当前时间
empMapper.update(emp);
}
@Override
@Log
public void save(Emp emp) {
//补全数据
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//调用添加方法
empMapper.insert(emp);
}
@Override
@Log
public void delete(List<Integer> ids) {
empMapper.delete(ids);
}
//省略其他代码...
}
(5) 定义切面类,完成记录操作日志的逻辑
java
@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 - 当前登录员工ID
//获取请求头中的jwt令牌, 解析令牌
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 begin = System.currentTimeMillis();
//调用原始目标方法运行
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
//方法返回值
String returnValue = JSONObject.toJSONString(result);
//操作耗时
Long costTime = end - begin;
//记录操作日志
OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("AOP记录操作日志: {}" , operateLog);
return result;
}
}
核心逻辑:
① 为什么 HttpServletRequest 能通过 @Autowired 直接注入?
- Spring Web 环境中,每个 HTTP 请求都会分配一个独立的线程处理,Spring 会把当前请求的
HttpServletRequest绑定到这个线程的ThreadLocal(线程局部变量)中。 @Autowired注入的HttpServletRequest其实是 Spring 提供的一个 "代理对象",它会从当前线程的ThreadLocal中获取真正的、对应本次请求的HttpServletRequest实例,因此能正确获取到本次请求的 token。
② 为什么要把目标方法的返回值(result)通过 JSONObject.toJSONString() 转换成 JSON 字符串。
- 你的操作日志表(
operate_log)里,记录返回值的字段(比如return_value),在数据库中定义的类型大概率是VARCHAR或TEXT(文本类型)------这种字段只能存字符串,不能直接存 Java 对象、集合、数字等复杂类型。
举个例子:假设目标方法返回的是一个 User 对象(包含 id=1、name="张三"、age=20),如果不转换,直接把这个 User 对象存到数据库的 return_value 字段里,会发生什么?
- Java 会默认把对象转成 "内存地址字符串"(比如
com.itheima.pojo.User@7852e922),存到数据库里完全看不懂; - 更严重的是,如果返回值是
List<User>集合、Map字典,直接存还可能触发数据库插入异常(因为类型不匹配)。
而通过 JSONObject.toJSONString(result) 转换后:User 对象会变成 {"id":1,"name":"张三","age":20}(JSON 字符串),List<User> 会变成 [{"id":1,"name":"张三"},{"id":2,"name":"李四"}]------ 这些都是标准字符串,既能正常存入数据库,又能直观看到内容。