Day13 Web后端开发进阶:Spring AOP


什么是AOP

图1 统计每一个业务方法的执行耗时
图2 AOP示意
图3 AOP示意
图4 将一大把蒜苔切成几段或将一大把蒜苔在不同的几个特定段位做一些加工--横切

目录

1. AOP基础

① AOP快速入门

图5 Spring AOP快速入门:统计所有业务层方法的执行耗时

AOP快速入门程序

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

图6 导入springboot-aop-quickstart模块
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
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;
    }
}
图7 AOP快速入门程序验证
图8 小结

② AOP核心概念

图9 AOP核心概念:

执行流程

图10 执行流程
图11 AOP执行流程中的动态代理对象
图12 切面、连接点、切入点

小结

图13 小结

2. AOP进阶

① 通知类型--方法

根据通知方法执行时机的不同,将通知类型分为以下常见的五类:

注意

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...");
    }
}
图14 通知类型--方法 验证

@PointCut

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

图15 @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...");
    }
}
图16 通知类型--方法 验证2

小结

图17 小结

② 通知顺序

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测试

图18 通知顺序验证1
图19 通知顺序验证2
图20 通知顺序

③ 切入点表达式

图21 切入点表达式常见形式

切入点表达式-execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

可以使用通配符描述切入点
图22 切入点表达式-execution
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...");
    }
}
图23 切入点表达式-execution验证1
图24 切入点表达式-execution验证2
图25 切入点表达式-execution验证3
小结

execution切入点表达式的完整语法 ?

通配符有哪些 ?

书写建议

图26 @execution小结

切入点表达式-@annotation

图26 @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);
    }
图28 @annotation验证

④ 连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

图29 连接点
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));
    }
}
图30 连接点验证1
图31 连接点验证2

3. AOP案例

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

图32 案例分析

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

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中添加、修改、删除部门后,日志文件中记录如下

图33 案例切入点验证
图34 案例初步测试验证

使用原视频的的代码

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
    }
}
图35 切入点注解

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

图36 案例初步测试验证2

② 获取当前登录员工id

员工登录成功后,哪里存储的有当前登录员工的信息?

如何从jwt令牌中获取到当前登录的员工信息?

TokenFilter中已经解析了令牌的信息,如何将其传递给AOP程序、Controller、Service呢?

图37 如何获取当前登录员工id

ThreadLocal

图38 ThreadLocal
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());
    }
}
图39 ThreadLocal相互隔离

获取当前登录员工

图40 获取当前登录员工具体步骤

具体操作步骤:

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中,用【林冲】用户名登录,添加、修改、删除部门后,日志文件中记录如下

图41 日志文件记录验证

过滤器获取用户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中,用【吴用】用户名登录,添加、修改、删除部门后,日志文件中记录如下

图42 日志文件记录验证2

小结

1.什么是ThreadLocal?

2.ThreadLocal的应用场景

图43 ThreadLocal小结

返回