Spring AOP 核心知识笔记

Spring AOP 核心知识笔记

一、AOP 核心思想与实现原理

AOP(面向切面编程)的核心是抽取重复语句,与 IOC(抽取重复定义对象)共同构成 Spring 两大核心特性,用于解决代码复用与解耦问题。

实现原理

Spring AOP 通过动态代理技术实现功能增强,具体分为两种方式:

  • JDK 动态代理:基于接口实现,要求目标对象必须实现接口。
  • CGLIB 动态代理:基于继承实现,无需目标对象实现接口(默认优先使用 JDK 代理,无接口时自动切换为 CGLIB)。

AOP 核心优势

  • 减少重复代码(如日志、权限校验等通用逻辑)
  • 提高开发效率(通用功能统一维护)
  • 代码无侵入(不修改原始业务代码)
  • 维护方便(通用逻辑修改仅需改一处)

二、AOP 核心概念

概念(英文) 核心定义
连接点(JoinPoint) 被 "添加功能" 的方法
通知(Advice) 添加的 "通用功能" 本身(如日志记录、权限校验,是具体的方法逻辑)
切入点(PointCut) 筛选 "需要加功能的方法" 的规则
切面(Aspect) 通知与切入点的组合,明确 "哪个功能(通知)加到哪些方法(切入点)上"
目标对象(Target) 被添加功能的原始对象(如 UserService 类的实例)

三、通知类型(Advice Type)

不同通知对应不同的执行时机,核心区别在于 "执行阶段" 和 "异常时是否运行"。

注解 通知类型 执行时机 异常时运行状态 正常时运行状态
@Around 环绕通知 目标方法前后均执行 执行(需捕获异常,或触发 finally 逻辑) 执行
@Before 前置通知 目标方法执行前触发 执行(仅在方法执行前,不受后续异常影响) 执行
@After 后置通知 目标方法执行后触发 执行(类似 finally,异常后必触发) 执行(类似 finally,正常后必触发)
@AfterReturning 返回后通知 目标方法正常返回后触发 不执行 执行(可获取方法返回值)
@AfterThrowing 异常后通知 目标方法抛出异常后触发 执行(可捕获指定异常类型) 不执行

各通知类型特殊说明

1. @Around(环绕通知)
  • 唯一需传入 ProceedingJoinPoint 参数的通知,需通过 proceed() 方法执行原始目标方法;
  • 返回值必须为 Object,用于接收原始方法的返回值;
  • 唯一能主动捕获并处理目标方法异常的通知;
2. @After(后置通知)
  • 优先级最低,无论目标方法正常执行或抛出异常,最终都会触发
  • 常用于资源释放类操作(如关闭数据库连接、IO 流)。
3. @AfterReturning & @AfterThrowing
  • 二者互斥触发 :目标方法正常返回时 @AfterReturning 执行,抛出异常时 @AfterThrowing 执行;
  • @AfterReturning 可通过 returning 属性绑定并获取方法返回值;
  • @AfterThrowing 可通过 throwing 属性指定捕获的异常类型,仅匹配对应异常时触发。

通知执行流程

1. 正常流程

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

2. 异常流程

@Before@Around(前逻辑)→ 目标方法(抛异常)→ @Around(异常捕获 /finally)→ @AfterThrowing@After

四、通知执行顺序(多切面场景)

当多个切面作用于同一目标方法时,执行顺序通过以下规则控制:

1. 默认规则(无 @Order 时)

切面类的类名首字母排序(ASCII 码顺序):

  • 目标方法 的通知:字母排名靠前的切面先执行(如 AspectAAspectB 先执行前置通知)。
  • 目标方法 的通知:字母排名靠前的切面后执行(如 AspectAAspectB 后执行后置通知)。

2. 自定义规则(用 @Order 注解)

在切面类上添加 @Order(数字) 控制顺序(数字越小优先级越高):

  • 目标方法 的通知:数字小的切面先执行(如 @Order(1)@Order(2) 先执行前置通知)。
  • 目标方法 的通知:数字小的切面后执行(如 @Order(1)@Order(2) 后执行后置通知)。

五、切入点表达式(PointCut Expression)

作用:定义 "哪些方法需要加入通知",核心有两种形式:execution(按方法签名匹配)和 @annotation(按注解匹配)。

1. execution(按方法签名匹配)

通过方法返回值、包名、类名、方法名、参数定位方法,语法格式:

plaintext 复制代码
execution(访问修饰符 返回值 包名.类名(或接口名).方法名(方法参数) throws 异常)
关键说明
  • 可省略部分

    1. 访问修饰符(如 public、protected,一般省略);
    2. 包名.类名(省略后匹配范围更宽,一般不省略);
    3. throws 异常(指定方法声明的异常,一般省略)。
  • 通配符用法

    通配符 作用 示例
    * 匹配单个任意符号 execution(* com.service.*.add*(..))
    .. 匹配多个连续任意符号 execution(* com..*.find*(String, ..))
  • 组合逻辑 :支持用 &&(且)、||(或)、!(非)组合表达式,如:

    plaintext 复制代码
    // 匹配 com.service 包下所有类的 add 或 update 开头的方法
    execution(* com.service.*.add*(..)) || execution(* com.service.*.update*(..))
切入点复用(@Pointcut)

在切面类中定义一个空方法,用 @Pointcut 标注表达式,后续通知可直接引用该方法,避免重复写表达式:

java 复制代码
// 1. 定义切入点(空方法 + @Pointcut)
@Pointcut("execution(* com.service.UserService.*(..))")
private void userServicePointCut() {}

// 2. 通知引用切入点
@Before("userServicePointCut()")
public void beforeAdvice(JoinPoint joinPoint) {
    // 通知逻辑
}

2. @annotation(按注解匹配)

通过 "方法是否被特定注解标记" 来匹配,适用于无统一方法名但需加相同功能的场景。

步骤
  1. 创建自定义注解 :添加 @Retention(RUNTIME)(运行时有效)和 @Target(METHOD)(仅作用于方法):

    java 复制代码
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Log { // 自定义注解名,如 @Log
    }
  2. 标记目标方法:在需要加通知的方法上添加自定义注解:

    java 复制代码
    @Service
    public class UserService {
        // 用 @Log 标记该方法,会被 AOP 拦截
        @Log
        public void addUser(User user) {
            // 业务逻辑
        }
    }
  3. 定义切入点 :通过 @annotation(注解全类名) 匹配被标记的方法:

    java 复制代码
    @Aspect
    @Component
    public class LogAspect {
        // 切入点:匹配被 @Log 注解标记的方法
        @Pointcut("@annotation(com.annotation.Log)")
        private void logPointCut() {}
    
        // 环绕通知引用切入点
        @Around("logPointCut()")
        public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
            // 日志逻辑
            return joinPoint.proceed(); // 执行原始方法
        }
    }

六、连接点(JoinPoint)API

JoinPoint 是 Spring 对 "方法执行连接点" 的抽象,用于获取方法运行时的关键信息(如参数、方法名、目标对象)。

1. 类型区别

通知类型 必须使用的连接点类型 说明
@Around ProceedingJoinPoint JoinPoint 子类,支持通过 proceed() 执行原始方法
@Before/@After JoinPoint 非环绕通知的通用类型,无 proceed() 方法

2. 常用 API

API 方法 返回值类型 核心作用
getTarget() Object 获取被代理的原始目标对象(如 UserService 实例)
getSignature().getName() String 获取目标方法名(如 "addUser")
getArgs() Object[] 获取目标方法入参数组(如 [user 对象])
getSignature().getDeclaringTypeName() String 获取方法所在类的全类名(如 "com.service.UserService")
(MethodSignature)getSignature() MethodSignature 强转后可获取 Method 对象(反射操作)
proceed()(仅 ProceedingJoinPoint) Object 执行原始目标方法(@Around 必备)

七、AOP 实战案例:记录操作日志

需求:记录方法执行时的操作人、操作时间、全类名、方法名、参数、返回值、执行时长

步骤

  1. 导入依赖(Spring Boot 项目):

    xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  2. 创建日志实体类(对应数据库表):

    java 复制代码
    @Data
    public class OperateLog {
        private Long id;
        private String operator; // 操作人
        private LocalDateTime operateTime; // 操作时间
        private String className; // 全类名
        private String methodName; // 方法名
        private String args; // 方法参数(JSON 格式)
        private String returnValue; // 返回值(JSON 格式)
        private Long costTime; // 执行时长(毫秒)
    }
  3. 创建自定义注解 @Log(标记需要记录日志的方法):

    java 复制代码
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Log {}
  4. 定义切面类 LogAspect

    java 复制代码
    @Aspect
    @Component
    public class LogAspect {
        @Autowired
        private HttpServletRequest request; // 用于获取登录用户(如从 Token 中解析)
        @Autowired
        private OperateLogMapper operateLogMapper; // 数据库操作 mapper
        @Autowired
        private ObjectMapper objectMapper; // 用于参数/返回值转 JSON
    
        // 切入点:匹配被 @Log 标记的方法
        @Pointcut("@annotation(com.annotation.Log)")
        private void logPointCut() {}
    
        // 环绕通知:记录日志
        @Around("logPointCut()")
        public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
            // 1. 前置逻辑:记录开始时间、获取基础信息
            long startTime = System.currentTimeMillis();
            OperateLog operateLog = new OperateLog();
            
            // 获取操作人(示例:从请求头 Token 解析,实际需结合登录逻辑)
            String token = request.getHeader("Token");
            String operator = parseOperatorFromToken(token); // 自定义方法:从 Token 取用户名
            operateLog.setOperator(operator);
            
            // 获取全类名、方法名
            String className = joinPoint.getSignature().getDeclaringTypeName();
            String methodName = joinPoint.getSignature().getName();
            operateLog.setClassName(className);
            operateLog.setMethodName(methodName);
            
            // 获取方法参数(转 JSON)
            Object[] args = joinPoint.getArgs();
            String argsJson = objectMapper.writeValueAsString(args);
            operateLog.setArgs(argsJson);
            
            // 2. 执行原始方法
            Object result = null;
            try {
                result = joinPoint.proceed(); // 执行目标方法
                // 3. 正常返回:记录返回值
                String resultJson = objectMapper.writeValueAsString(result);
                operateLog.setReturnValue(resultJson);
            } finally {
                // 4. 后置逻辑:记录执行时长、操作时间,插入数据库(finally 确保必执行)
                long costTime = System.currentTimeMillis() - startTime;
                operateLog.setCostTime(costTime);
                operateLog.setOperateTime(LocalDateTime.now());
                operateLogMapper.insert(operateLog); // 插入日志表
            }
            
            return result;
        }
    
        // 自定义方法:从 Token 解析操作人(示例,实际需结合 JWT 等逻辑)
        private String parseOperatorFromToken(String token) {
            // 省略 Token 解析逻辑,返回用户名
            return "admin";
        }
    }
  5. 标记目标方法 :在需要记录日志的业务方法上添加 @Log

    java 复制代码
    @Service
    public class UserService {
        @Log // 标记该方法需要记录操作日志
        public User addUser(User user) {
            // 业务逻辑:新增用户
            return user;
        }
    }

八、切入点表达式书写建议

  1. 规范方法命名 :如查询方法统一用 find 开头、更新用 update 开头,方便 execution 表达式匹配。
  2. 基于接口描述 :优先匹配接口方法(如 execution(* com.service.UserService.*(..))),而非实现类,增强扩展性。
  3. 缩小匹配范围 :避免使用过于宽泛的表达式(如 execution(* com..*(..))),减少不必要的拦截,提升性能。
相关推荐
互亿无线明明2 小时前
在 Go 项目中集成国际短信能力:从接口调试到生产环境的最佳实践
开发语言·windows·git·后端·golang·pycharm·eclipse
虎子_layor2 小时前
Spring 循环依赖与三级缓存:我终于敢说把这事儿讲透了
java·后端·spring
海上彼尚2 小时前
Go之路 - 5.go的流程控制
开发语言·后端·golang
okseekw2 小时前
递归:不止是 “自己调用自己”,看完这篇秒懂
java·后端
温宇飞2 小时前
Drizzle ORM:类型安全的数据库开发
后端
SEO-狼术2 小时前
ASP.NET Zero v15.0.0 adds full .NET
后端·asp.net·.net
艾斯Felix2 小时前
SearXNG使用之引擎连接超时,响应成功但是空数据
后端
木木一直在哭泣2 小时前
我是怎么用 Redis 把 ERP→CRM 同步提速到“几秒钟”的
后端
零日失眠者2 小时前
【Python好用到哭的库】pandas-数据分析神器
后端·python·ai编程