Day13 Web后端开发进阶:Spring AOP
什么是AOP
- AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),可简单理解为就是面向特定方法编程。
- 场景:案例中部分业务方法运行较慢,定位执行耗时较长的接口,此时需要统计每一个业务方法的执行耗时。
- 优势:
- 减少重复代码
- 代码无侵入
- 提高开发效率
- 维护方便
- 提示 AOP是一种编程思想,而在Spring框架中对这种思想进行的实现,那我们要学习的就是Spring AOP。




目录
- AOP基础
- AOP进阶
- AOP案例
1. AOP基础
① AOP快速入门
- 需求:统计所有业务层方法的执行耗时。
- ①导入依赖:在pom.xml中引入AOP的依赖
- ②编写AOP程序:针对于特定的方法根据业务需要进行编程

AOP快速入门程序
导入springboot-aop-quickstart模块,修改包名和数据库连接参数,编写AOP快速入门程序。

- ①导入依赖:在pom.xml中引入AOP的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- ②编写AOP程序:针对于特定的方法根据业务需要进行编程
cn/zjy/aop/RecordTimeApsect.java
package cn.zjy.aop;
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.stereotype.Component;
@Slf4j
@Aspect
@Component
public class RecordTimeApsect {
@Around("execution(* cn.zjy.service.impl.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//1.记录方法运行的开始时间
long begin = System.currentTimeMillis();
//2.执行原始的方法
Object result = pjp.proceed();
//3,记录方法运行的结束时间,记录耗时
long end = System.currentTimeMillis();
log.info("方法 {} 运行耗时:{}ms",pjp.getSignature().getName(),(end - begin));
return result;
}
}


② AOP核心概念
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
- 目标对象:Target,通知所应用的对象

执行流程



小结

2. AOP进阶
- 通知类型
- 通知顺序
- 切入点表达式
- 连接点
① 通知类型--方法
根据通知方法执行时机的不同,将通知类型分为以下常见的五类:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning: 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
注意
- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
cn/zjy/aop/MyAspect1.java
package cn.zjy.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class MyAspect1 {
//前置通知-目标方法运行之前运行
@Before("execution(* cn.zjy.service.impl.*.*(..))")
public void before() {
log.info("前置通知-目标方法运行之前 before...");
}
//环绕通知-目标方法运行之前、后运行
@Around("execution(* cn.zjy.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
log.info("环绕通知-目标方法运行之前 around...before...");
Object result =pjp.proceed();
log.info("环绕通知-目标方法运行之后 around... after .... ");
return result;
}
//后置通知-目标方法运行之后运行,无论是否出现异常都会执行
@After("execution(* cn.zjy.service.impl.*.*(..))")
public void after() {
log.info("后置通知-目标方法运行之后 after...");
}
// 返回后通知-目标方法运行之后运行,如果出现异常不会运行
@AfterReturning("execution(* cn.zjy.service.impl.*.*(..))")
public void afterReturning() {
log.info("返回后通知-目标方法运行之后,如果出现异常不会运行 afterReturning...");
}
//异常后通知-目标方法运行之后运行,只有出现异常才会运行
@AfterThrowing("execution(* cn.zjy.service.impl.*.*(..))")
public void afterThrowing() {
log.info("异常后通知-目标方法运行之后,只有出现异常才会运行 afterThrowing...");
}
}

@PointCut
注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。

cn/zjy/aop/MyAspect1.java 修改如下
package cn.zjy.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class MyAspect1 {
@Pointcut("execution(* cn.zjy.service.impl.*.*(..))")
private void pt() {}
//前置通知-目标方法运行之前运行
//@Before("execution(* cn.zjy.service.impl.*.*(..))")
@Before("pt()")
public void before() {
log.info("使用@Pointcut注解统一注解切入点");
log.info("前置通知-目标方法运行之前 before...");
}
//环绕通知-目标方法运行之前、后运行
//@Around("execution(* cn.zjy.service.impl.*.*(..))")
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
log.info("环绕通知-目标方法运行之前 around...before...");
Object result =pjp.proceed();
log.info("环绕通知-目标方法运行之后 around... after .... ");
return result;
}
//后置通知-目标方法运行之后运行,无论是否出现异常都会执行
//@After("execution(* cn.zjy.service.impl.*.*(..))")
@After("pt()")
public void after() {
log.info("后置通知-目标方法运行之后 after...");
}
// 返回后通知-目标方法运行之后运行,如果出现异常不会运行
//@AfterReturning("execution(* cn.zjy.service.impl.*.*(..))")
@AfterReturning("pt()")
public void afterReturning() {
log.info("返回后通知-目标方法运行之后,如果出现异常不会运行 afterReturning...");
}
//异常后通知-目标方法运行之后运行,只有出现异常才会运行
//@AfterThrowing("execution(* cn.zjy.service.impl.*.*(..))")
@AfterThrowing("pt()")
public void afterThrowing() {
log.info("异常后通知-目标方法运行之后,只有出现异常才会运行 afterThrowing...");
}
}

小结

② 通知顺序
- 当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
- 执行顺序:
- 不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
- 用 @Order(数字) 加在切面类上来控制顺序
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
- 不同切面类中,默认按照切面类的类名字母排序:
cn/zjy/aop/MyAspect2.java
package cn.zjy.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//前置通知
@Before("execution(* cn.zjy.service.impl.*.*(..))")
public void before(){
log.info("MyAspect2 -> 前置通知 before ...");
}
//后置通知
@After("execution(* cn.zjy.service.impl.*.*(..))")
public void after(){
log.info("MyAspect2 -> 后置通知 after ...");
}
//环绕通知
@Around("execution(* cn.zjy.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
log.info("MyAspect2 -> 环绕通知之前 around... before...");
Object result =pjp.proceed();
log.info("MyAspect2 -> 环绕通知之后 around... after .... ");
return result;
}
}
cn/zjy/aop/MyAspect3.java
package cn.zjy.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect3 {
//前置通知
@Before("execution(* cn.zjy.service.impl.*.*(..))")
public void before(){
log.info("MyAspect3 ->前置通知 before ...");
}
//后置通知
@After("execution(* cn.zjy.service.impl.*.*(..))")
public void after(){
log.info("MyAspect3 -> 后置通知 after ...");
}
//环绕通知
@Around("execution(* cn.zjy.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
log.info("MyAspect3 -> 环绕通知之前 around... before...");
Object result =pjp.proceed();
log.info("MyAspect3 -> 环绕通知之后 around... after .... ");
return result;
}
}
cn/zjy/aop/MyAspect4.java
package cn.zjy.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect4 {
//前置通知
@Before("execution(* cn.zjy.service.impl.*.*(..))")
public void before(){
log.info("MyAspect4 -> 前置通知 before ...");
}
//后置通知
@After("execution(* cn.zjy.service.impl..*(..))")
public void after(){
log.info("MyAspect4 -> 后置通知 after ...");
}
//环绕通知
@Around("execution(* cn.zjy.service.impl.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
log.info("MyAspect4 -> 环绕通知之前 around... before...");
Object result =pjp.proceed();
log.info("MyAspect4 -> 环绕通知之后 around... after .... ");
return result;
}
}
注释掉 MyAspect1和RecordTimeApsect的//@Aspect测试



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

切入点表达式-execution
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
- 其中带 ? 的表示可以省略的部分
- 访问修饰符:可省略(比如: public、protected)
- 包名.类名: 可省略 建议不省略
- throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点
- *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
- ..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

cn/zjy/aop/MyAspect5.java
package cn.zjy.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect5 {
//@Before("execution(void cn.zjy.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Before("execution(void delete(java.lang.Integer))")//包名.类名强烈不建议省略
//@Before("execution(* cn.zjy.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Before("execution(* com.*.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//@Before("execution(* cn.zjy.service.impl.*.delete(java.lang.Integer))")
//@Before("execution(* cn.zjy.service.impl.*.*(java.lang.Integer))")
//@Before("execution(* cn.zjy.service.impl.*.*(*))")
//@Before("execution(* cn.zjy.service.impl.*.del*(*))")
//@Before("execution(* cn.zjy.service.impl.*.*e(*))")
//@Before("execution(* cn..service.impl.DeptServiceImpl.*(..))")
//@Before("execution(* cn.zjy.service.*.*(..))")
//匹配list与delete方法
//@Before("execution(* cn.zjy.service.impl.DeptServiceImpl.list(..)) ||"+
// "execution(* cn.zjy.service.impl.DeptServiceImpl.delete(..))")
@Before("@annotation(cn.zjy.anno.LogOperation)")
public void before(){
log.info("MyAspect5->before...");
}
}



小结
execution切入点表达式的完整语法 ?
- execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
通配符有哪些 ?
- *:单个独立的任意符号
- .. :多个连续的任意符号
书写建议
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx。
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用..,使用*匹配单个包。

切入点表达式-@annotation

cn/zjy/anno/LogOperation.java
package cn.zjy.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
}
cn/zjy/service/impl/DeptServiceImpl.java 修改代码 list和delete方法加注@LogOperation
@LogOperation
@Override
public List<Dept> list() {
// int i = 1/0;
List<Dept> deptList = deptMapper.list();
return deptList;
}
@LogOperation
@Override
public void delete(Integer id) {
deptMapper.delete(id);
}

④ 连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
- 对于其它四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型

cn/zjy/aop/MyAspect6.java 针对借口AOP
package cn.zjy.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//环绕通知
@Around("execution(* cn.zjy.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("环绕通知之前 around before");
Object ret = pjp.proceed();
log.info("环绕通知之后 around after");
//1.获取目标对象
Object target =pjp.getTarget();
log.info("环绕通知之后 获取目标对象:{}", target);
//2.获取目标类
String className =pjp.getTarget().getClass().getName();
log.info("环绕通知之后 获取目标类:{}",className);
//3.获取目标方法
String methodName=pjp.getSignature().getName();
log.info("环绕通知之后 获取目标方法:{}",methodName);
//4,获取目标方法参数
Object[] args=pjp.getArgs();
log.info("环绕通知之后 获取目标方法参数:{}", Arrays.toString(args));
//5,获取目标方法的返回值
log.info("环绕通知之后 获取目标方法的返回值:{}", ret.toString());
return ret;
}
//前置通知
@Before("execution(* cn.zjy.service.*.*(..))")
public void before(JoinPoint joinPoint) {
log.info("前置通知: before");
//1.获取目标对象
Object target =joinPoint.getTarget();
log.info("前置通知: 获取目标对象:{}", target);
//2.获取目标类
String className =joinPoint.getTarget().getClass().getName();
log.info("前置通知: 获取目标类:{}",className);
//3.获取目标方法
String methodName=joinPoint.getSignature().getName();
log.info("前置通知: 获取目标方法:{}",methodName);
//4,获取目标方法参数
Object[]args=joinPoint.getArgs();
log.info("前置通知: 获取目标方法参数:{}", Arrays.toString(args));
}
}


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

① 将案例中增、删、改相关接口的操作日志记录到数据库表中
pom.xml
若使用 Spring Boot,无需额外引入spring-boot-starter-web已包含 AOP 相关包
<!-- AOP起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建操作日志表
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_emp_id int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(2000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint unsigned comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
cn/zjy/anno/Log.java
package cn.zjy.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
cn/zjy/pojo/OperateLog.java
package cn.zjy.pojo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OperateLog {
private Integer id; //ID
private Integer operateEmpId; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
cn/zjy/mapper/OperateLogMapper.java
package cn.zjy.mapper;
import cn.zjy.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
AI提问词
假如你是一名java开发工程师, 请帮我基于Spring AOP实现记录系统所有增、删、改功能接口的操作日志。具体信息如下:
1. 日志信息包含:操作人、操作时间、目标类的全类名、目标方法的方法名、方法运行时参数、返回值、方法执行时长
2. 功能接口所在包为 cn.zjy.controller
3. 日志表为 operate_log 表,对应的实体类为 OperateLog。
具体表结构如下:
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_emp_id int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(2000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint unsigned comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
实体类如下:
@Data
public class OperateLog {
private Integer id; //ID
private Integer operateEmpId; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
4. 并且已经提供了OperateLogMapper接口来操作 operate_log, 并在其中已经定义好了 insert 方法用来保存日志数据.
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
cn/zjy/aop/OperateLogAspect.java 豆包生成的代码
package cn.zjy.aop;
import cn.zjy.mapper.OperateLogMapper;
import cn.zjy.pojo.OperateLog;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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
@Aspect
@Component
public class OperateLogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
@Autowired
private ObjectMapper objectMapper;
/**
* 环绕通知:拦截cn.zjy.controller包下所有增删改方法
* 匹配以add、save、update、delete开头的方法
*/
@Around("execution(* cn.zjy.controller.*.*(..)) && " +
"(execution(* add*(..)) || (execution(* save*(..)) || execution(* update*(..)) || execution(* delete*(..))))")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 记录方法执行前的信息
LocalDateTime operateTime = LocalDateTime.now();
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String methodParams = getParamsAsString(args);
// 获取当前操作人ID(实际项目中需从登录上下文获取)
Integer operateEmpId = getCurrentUserId();
// 2. 执行目标方法并记录时间
long startTime = System.currentTimeMillis();
Object returnValue = null;
try {
returnValue = joinPoint.proceed(); // 执行原方法
return returnValue;
} finally {
// 3. 记录方法执行后的信息并保存日志
long costTime = System.currentTimeMillis() - startTime;
String returnValueStr = getReturnValueAsString(returnValue);
// 构建日志对象
OperateLog olog = new OperateLog();
// 列编辑:使用Alt + 鼠标左键拖动(Windows),在log前面线下拖动
olog.setOperateEmpId(operateEmpId);
olog.setOperateTime(operateTime);
olog.setClassName(className);
olog.setMethodName(methodName);
olog.setMethodParams(methodParams);
olog.setReturnValue(returnValueStr);
olog.setCostTime(costTime);
// 保存日志
operateLogMapper.insert(olog);
log.info("操作日志:{}",olog);
}
}
/**
* 将方法参数转换为字符串
*/
private String getParamsAsString(Object[] args) {
if (args == null || args.length == 0) {
return "无参数";
}
try {
return objectMapper.writeValueAsString(args);
} catch (JsonProcessingException e) {
// 序列化失败时使用默认的toString方法
return Arrays.toString(args);
}
}
/**
* 将返回值转换为字符串
*/
private String getReturnValueAsString(Object returnValue) {
if (returnValue == null) {
return "无返回值";
}
try {
return objectMapper.writeValueAsString(returnValue);
} catch (JsonProcessingException e) {
return returnValue.toString();
}
}
/**
* 获取当前登录用户ID
* 实际项目中应从SecurityContext、ThreadLocal或Session中获取
*/
private Integer getCurrentUserId() {
// 这里仅为示例,实际需替换为真实的用户ID获取逻辑
// 例如:
// return SecurityContextHolder.getContext().getAuthentication().getPrincipal().getId();
return 1; // 模拟当前登录用户ID
}
}
前端http://localhost:90中添加、修改、删除部门后,日志文件中记录如下


使用原视频的的代码
cn/zjy/aop/OperationLogAspect.java
package cn.zjy.aop;
import cn.zjy.mapper.OperateLogMapper;
import cn.zjy.pojo.OperateLog;
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
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(cn.zjy.anno.Log)")
public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
// 计算耗时
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 构建日志实体
OperateLog olog = new OperateLog();
olog.setOperateEmpId(getCurrentUserId()); // 这里需要你根据实际情况获取当前用户ID
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(joinPoint.getTarget().getClass().getName());
olog.setMethodName(joinPoint.getSignature().getName());
olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
olog.setReturnValue(result != null ? result.toString() : "void");
olog.setCostTime(costTime);
// 保存日志
log.info("记录操作日志: {}", olog);
operateLogMapper.insert(olog);
return result;
}
private Integer getCurrentUserId() {
//return CurrentHolder.getCurrentId();
// 这里仅为示例,实际需替换为真实的用户ID获取逻辑
return 1; // 模拟当前登录用户ID
}
}

注释OperateLogAspect的//@Aspect,前端http://localhost:90中添加666、修改666888、删除部门后,日志文件中记录如下

② 获取当前登录员工id
员工登录成功后,哪里存储的有当前登录员工的信息?
- 给客户端浏览器下发的jwt令牌中
如何从jwt令牌中获取到当前登录的员工信息?
- 获取请求头中传递的jwt令牌,并解析
TokenFilter中已经解析了令牌的信息,如何将其传递给AOP程序、Controller、Service呢?
- Prompt:我在令牌校验过滤器TokenFilter中获取了jwt令牌,并对其进行解析获取当了当前登录员工的ID,如何将这个ID传递给AOP程序、Controller、Service中呢?

ThreadLocal
- ThreadLocal并不是一个Thread,而是Thread的局部变量。
- ThreadLocal为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰。

- ThreadLocal常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量
src/test/java/cn/zjy/ThreadLocalTest.java
package cn.zjy;
public class ThreadLocalTest {
private static ThreadLocal<String> local = new ThreadLocal<>();
public static void main(String[] args) {
local.set("Main Message");
//创建线程
new Thread(new Runnable(){
@Override
public void run(){
local.set("Sub Message");
System.out.println(Thread.currentThread().getName() + ":" + local.get());
}
}).start();
System.out.println(Thread.currentThread().getName() + ": " + local.get());
local.remove();
System.out.println(Thread.currentThread().getName() + ": " + local.get());
}
}

获取当前登录员工

具体操作步骤:
- 定义ThreadLocal操作的工具类,用于操作当前登录员工ID。
- 在TokenFilter中,解析完当前登录员工ID,将其存入ThreadLocal(用完之后需将其删除)。
- 在AOP程序中,从ThreadLocal中获取当前登录员工的ID。
cn/zjy/utils/CurrentHolder.java
package cn.zjy.utils;
public class CurrentHolder {
private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();
public static void setCurrentId(Integer employeeId) {
CURRENT_LOCAL.set(employeeId);
}
public static Integer getCurrentId() {
return CURRENT_LOCAL.get();
}
public static void remove() {
CURRENT_LOCAL.remove();
}
}
cn/zjy/interceptor/TokenInterceptor.java 修改令牌解析代码
//5.如果token存在,校验令牌,如果校验失败->返回错误信息(响应401状态码)
try {
//JwtUtils.parseToken( token);
Claims claims=JwtUtils.parseToken(token);
Integer empId =Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);//存入
log.info("当前登录员工ID:{},将其存入ThreadLocal",empId);
} catch (Exception e) {
log.info("令牌非法,响应401");
response.setStatus(401);
return false;
}
cn/zjy/aop/OperationLogAspect.java 修改getCurrentUserId()方法
private Integer getCurrentUserId() {
return CurrentHolder.getCurrentId();
// 这里仅为示例,实际需替换为真实的用户ID获取逻辑
//return 1; // 模拟当前登录用户ID
}
前端http://localhost:90中,用【林冲】用户名登录,添加、修改、删除部门后,日志文件中记录如下

过滤器获取用户id
cn/zjy/filter/TokenFilter.java 修改令牌解析代码
//5.如果token存在,校验令牌,如果校验失败->返回错误信息(响应401状态码)
try {
// JwtUtils.parseToken( token);
Claims claims=JwtUtils.parseToken(token);
Integer empId =Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);//存入
log.info("当前登录员工ID:{},将其存入ThreadLocal",empId);
} catch (Exception e) {
log.info("令牌非法,响应401");
response.setStatus(401);
return;
}
修改cn/zjy/config/WebConfig.java代码://@Configuration,// @Autowired;前端http://localhost:90中,用【吴用】用户名登录,添加、修改、删除部门后,日志文件中记录如下

小结
1.什么是ThreadLocal?
- ThreadLocal其实是线程的局部变量,为每个线程提供单独一份存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰
2.ThreadLocal的应用场景
- 在同一个线程/同一个请求中,进行数据共享。
