12. AOP(记录日志)

AOP 学习笔记

一、AOP 基础认知

1. 什么是 AOP

  • 定义:AOP(Aspect Oriented Programming)即面向切面编程,核心是面向特定方法编程,将重复逻辑与业务逻辑分离,实现功能增强。
  • 本质:不修改原始业务代码,通过动态代理技术对目标方法进行增强,解决代码重复、侵入性强的问题。

2. AOP 核心优势

  • 减少重复代码:将日志、计时等共性逻辑抽取到切面类
  • 代码无侵入:不修改原始业务方法,降低耦合度
  • 提高开发效率:专注业务逻辑开发,共性功能统一实现
  • 维护方便:共性功能集中管理,修改时无需改动多个业务类

3. 核心概念

概念 定义
连接点(JoinPoint) 可被 AOP 控制的方法(如业务层所有方法),封装了方法执行时的相关信息
通知(Advice) 抽取的重复逻辑(共性功能),体现为具体方法(如计时、日志记录)
切入点(PointCut) 匹配连接点的条件(通过表达式描述),决定通知应用于哪些方法
切面(Aspect) 通知与切入点的对应关系(通知+切入点),所在类为切面类(@Aspect 标识)
目标对象(Target) 通知所应用的原始业务对象
代理对象 Spring AOP 底层通过动态代理生成,用于增强目标对象的方法

4. 底层原理

Spring AOP 基于动态代理技术实现:程序运行时自动为目标对象生成代理对象,在代理对象中嵌入通知逻辑,实现对原始方法的增强。

二、AOP 入门实战

1. 需求

统计部门管理业务层方法的执行耗时。

2. 实现步骤

(1)导入依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
(2)编写切面类
java 复制代码
@Component
@Aspect // 标识为切面类
@Slf4j
public class RecordTimeAspect {
    // 环绕通知 + 切入点表达式(匹配目标方法)
    @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        // 1. 记录开始时间
        long begin = System.currentTimeMillis();
        // 2. 执行原始业务方法
        Object result = pjp.proceed();
        // 3. 记录结束时间并计算耗时
        long end = System.currentTimeMillis();
        log.info("方法执行耗时: {}毫秒", end - begin);
        // 4. 返回原始方法结果
        return result;
    }
}
(3)测试效果

启动服务后,调用业务接口,控制台会输出对应方法的执行耗时。

3. 常见应用场景

  • 记录操作日志
  • 权限控制
  • 事务管理(Spring 事务底层基于 AOP 实现)
  • 性能监控(如方法耗时统计)

三、AOP 进阶知识

1. 通知类型

Spring AOP 提供 5 种通知类型,覆盖方法执行的不同阶段:

通知注解 执行时机 注意事项
@Around(环绕通知) 目标方法执行前 + 执行后 需调用 proceed() 执行原始方法,必须返回结果
@Before(前置通知) 目标方法执行前 无返回值,不能阻止目标方法执行
@After(后置通知) 目标方法执行后(无论是否抛出异常) 无返回值
@AfterReturning 目标方法正常执行完成后(无异常) 可获取方法返回值
@AfterThrowing 目标方法抛出异常后 可获取异常信息
通知执行顺序(无异常情况)

@Around(前) → @Before → 目标方法 → @Around(后) → @AfterReturning → @After

异常情况

@Around(前) → @Before → 目标方法(抛异常) → @AfterThrowing → @After(@Around 后续逻辑不执行)

2. 切入点表达式

用于描述需要匹配的目标方法,核心有 2 种形式:

(1)execution 表达式(常用)
  • 语法 :execution(访问修饰符? 返回值 包名.类名.?方法名(参数) throws 异常?)

  • 通配符说明:

    • *:匹配单个任意符号(如任意返回值、单个包、单个方法名)
    • ..:匹配多个连续任意符号(如任意层级包、任意个数/类型的参数)
  • 示例:

    java 复制代码
    // 匹配 com.itheima.service 包下所有类的所有方法
    execution(* com.itheima.service.*.*(..))
    // 匹配 DeptService 接口中所有以 find 开头的方法
    execution(* com.itheima.service.DeptService.find*(..))
    // 匹配 delete 方法(参数为 Integer 类型)
    execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
(2)@annotation 表达式(灵活匹配)

通过自定义注解标记目标方法,适合无规则的方法匹配:

  1. 定义自定义注解:

    java 复制代码
    @Target(ElementType.METHOD) // 仅作用于方法
    @Retention(RetentionPolicy.RUNTIME) // 运行时生效
    public @interface LogOperation {}
  2. 在目标方法上添加注解:

    java 复制代码
    @Service
    public class DeptServiceImpl implements DeptService {
        @Override
        @LogOperation // 标记需要增强的方法
        public void delete(Integer id) {
            deptMapper.delete(id);
        }
    }
  3. 切面类中引用注解:

    java 复制代码
    @Before("@annotation(com.itheima.anno.LogOperation)")
    public void before(JoinPoint joinPoint) {
        log.info("前置通知:记录操作日志");
    }
切入点表达式复用

使用 @Pointcut 抽取公共表达式,避免重复编写:

java 复制代码
@Aspect
@Component
public class MyAspect {
    // 抽取公共切入点表达式
    @Pointcut("execution(* com.itheima.service.*.*(..))")
    private void pt() {}

    // 引用公共表达式
    @Before("pt()")
    public void before() {
        log.info("前置通知...");
    }
}

3. 通知顺序控制

当多个切面类匹配同一个目标方法时,通过以下方式控制执行顺序:

  1. 默认规则:按切面类名的字母顺序排序(前置通知:字母靠前先执行;后置通知:字母靠前后执行)

  2. @Order 注解(推荐) :在切面类上添加 @Order(数字),数字越小,优先级越高

    java 复制代码
    @Aspect
    @Component
    @Order(1) // 优先级高于 Order(2) 的切面
    public class MyAspect1 { ... }

四、AOP 案例:操作日志记录

1. 需求

记录系统中增删改接口的操作日志,包含:操作人、操作时间、类名、方法名、参数、返回值、执行时长,存入数据库。

2. 实现步骤

(1)准备工作
  1. 数据库表设计:

    sql 复制代码
    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(1000) comment '方法参数',
        return_value varchar(2000) comment '返回值',
        cost_time int comment '方法执行耗时(ms)'
    ) comment '操作日志表';
  2. 实体类 OperateLog + Mapper 接口(提供 insert 方法)

(2)编码实现
  1. 自定义注解 @LogOperation(标记需要记录日志的接口方法)

  2. 切面类实现:

    java 复制代码
    @Aspect
    @Component
    public class OperationLogAspect {
        @Autowired
        private OperateLogMapper operateLogMapper;
    
        @Around("@annotation(com.itheima.anno.LogOperation)")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            // 1. 记录开始时间
            long startTime = System.currentTimeMillis();
            // 2. 执行原始方法
            Object result = joinPoint.proceed();
            // 3. 计算耗时
            long costTime = System.currentTimeMillis() - startTime;
    
            // 4. 构建日志对象
            OperateLog log = new OperateLog();
            log.setOperateEmpId(CurrentHolder.getCurrentId()); // 从 ThreadLocal 获取当前登录人ID
            log.setOperateTime(LocalDateTime.now());
            log.setClassName(joinPoint.getTarget().getClass().getName()); // 目标类名
            log.setMethodName(joinPoint.getSignature().getName()); // 目标方法名
            log.setMethodParams(Arrays.toString(joinPoint.getArgs())); // 方法参数
            log.setReturnValue(result.toString()); // 返回值
            log.setCostTime(costTime);
    
            // 5. 保存日志到数据库
            operateLogMapper.insert(log);
            return result;
        }
    }
  3. 在 Controller 层增删改方法上添加 @LogOperation 注解

3. 关键技术:ThreadLocal 共享登录信息

  • 作用:在同一线程(同一请求)中共享数据(如当前登录人ID),实现线程隔离

  • 工具类实现 :

    java 复制代码
    public class CurrentHolder {
        private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();
    
        // 设置当前登录人ID
        public static void setCurrentId(Integer empId) {
            CURRENT_LOCAL.set(empId);
        }
    
        // 获取当前登录人ID
        public static Integer getCurrentId() {
            return CURRENT_LOCAL.get();
        }
    
        // 移除数据(避免内存泄漏)
        public static void remove() {
            CURRENT_LOCAL.remove();
        }
    }
  • 使用场景:在 Token 过滤器中解析登录人ID并存入 ThreadLocal,在 AOP 中直接获取。

四、核心总结

  1. AOP 核心思想是分离共性逻辑与业务逻辑,通过切面类统一管理增强功能
  2. 5 种通知类型覆盖方法执行全生命周期,@Around 功能最强大(可控制原始方法执行)
  3. 切入点表达式两种形式:execution(按方法签名匹配)、@annotation(按注解匹配)
  4. 多切面执行顺序通过 @Order 控制,数字越小优先级越高
  5. 实际开发中常用 AOP 实现日志、权限、事务等非业务功能,提高代码复用性和可维护性
相关推荐
有味道的男人7 分钟前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft1 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly1 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝1 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦1 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡1 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95272 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人2 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载2 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码