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 实现日志、权限、事务等非业务功能,提高代码复用性和可维护性
相关推荐
我命由我123452 小时前
微信小程序 - 页面返回并传递数据(使用事件通道、操作页面栈)
开发语言·前端·javascript·微信小程序·小程序·前端框架·js
一水鉴天2 小时前
整体设计 定稿 备忘录仪表盘方案 之1 初稿之8 V5版本的主程序 之2: 自动化导航 + 定制化服务 + 个性化智能体(豆包助手)
前端·人工智能·架构
vortex52 小时前
【Web开发】从WSGI到Servlet再到Spring Boot
前端·spring boot·servlet
于谦2 小时前
git提交信息也能自动格式化了?committier快速体验
前端·javascript·代码规范
小高0072 小时前
React 避坑指南:彻底搞定不必要的重新渲染
前端·javascript·react.js
浩浩酱2 小时前
【TS】any的问题及与unknown的区别
前端·typescript
dagouaofei2 小时前
手术室护理年终PPT怎么做?
前端·python·html·powerpoint
技术爬爬虾2 小时前
为什么React的漏洞能攻破服务器?Next.js与RSC入门基础
前端·数据库·安全
JS_GGbond2 小时前
浏览器三大核心API:LocalStorage、Fetch API、History API详解
前端·javascript