关注

Spring AOP 完整讲解

一、AOP 基础概念

1. 什么是 AOP

AOP:面向切面编程(Aspect Oriented Programming) 核心思想:横向抽取通用公共逻辑,不侵入业务代码,实现解耦。 传统 OOP 是纵向继承;AOP 是横向切面,统一拦截增强。

2. 解决什么问题

业务代码中大量重复逻辑:日志、权限校验、事务、接口限流、参数校验、性能监控、异常统一处理。 如果写在每个接口:代码冗余、难维护、污染业务; AOP 统一抽成切面,运行时动态切入目标方法,无侵入。

3. AOP 核心术语

  1. JoinPoint 连接点 程序中可以被切面拦截的点,Spring 中仅支持方法执行作为连接点(类、字段、构造器不支持)。
  2. Pointcut 切入点 匹配规则,筛选哪些连接点需要被拦截(表达式匹配方法)。
  3. Advice 通知 切面要执行的增强逻辑,分 5 种时机:
    • @Before:前置通知,方法执行前
    • @AfterReturning:正常返回后(无异常)
    • @AfterThrowing:抛出异常后
    • @After:最终通知,无论是否异常都执行(类似 finally)
    • @Around:环绕通知,包裹目标方法,可控制执行、返回值、异常
  4. Aspect 切面 切面类 = 切入点 + 通知,存放增强逻辑。
  5. Target 目标对象 被切面代理的原始业务对象。
  6. Proxy 代理对象 Spring 运行时生成的代理类,调用方法时先走切面逻辑。
  7. Weaving 织入 将切面逻辑植入目标对象的过程;Spring AOP 是运行时织入

二、Spring AOP 底层两种代理实现

Spring 会自动选择代理方式,无需手动指定:

1. JDK 动态代理

  • 条件:目标类实现了至少一个接口
  • 原理:基于接口生成代理类,只能拦截接口定义的方法
  • 限制:无法代理类中未在接口声明的方法

2. CGLIB 字节码代理

  • 条件:目标类没有实现任何接口
  • 原理:继承目标类生成子类作为代理,重写所有非 final 方法
  • SpringBoot 2.x+ 默认使用 CGLIB 代理

切换代理方式配置(application.yml)

yaml

spring:
  aop:
    proxy-target-class: true # 强制全部使用CGLIB

三、注解式 AOP

步骤 1:引入依赖

xml

<!-- Spring AOP 核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

步骤 2:开启 AOP

启动类添加注解:

java

运行

@SpringBootApplication
@EnableAspectJAutoProxy // 开启自动代理,支持AOP
public class App {}

步骤 3:完整切面示

java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Date;

@Aspect // 标记为切面类
@Component // 交给Spring管理
public class LogAspect {

    // 1. 定义切入点:匹配controller包下所有类所有方法
    @Pointcut("execution(* com.demo.controller.*.*(..))")
    public void pointCut() {}

    // 前置通知
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("方法执行前:" + new Date());
        // 获取方法信息
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs(); // 获取入参
    }

    // 正常返回通知,可接收返回值
    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturn(Object result) {
        System.out.println("方法正常返回,返回值:" + result);
    }

    // 异常通知,捕获异常
    @AfterThrowing(value = "pointCut()", throwing = "ex")
    public void afterThrow(Exception ex) {
        System.out.println("方法抛出异常:" + ex.getMessage());
    }

    // 最终通知,一定执行
    @After("pointCut()")
    public void after() {
        System.out.println("方法执行完毕(finally)");
    }

    // 环绕通知(功能最强,可控制目标方法是否执行)
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        // 执行目标方法
        Object res = pjp.proceed();
        long cost = System.currentTimeMillis() - start;
        System.out.println("方法耗时:" + cost + "ms");
        return res;
    }
}

通知执行顺序

正常无异常: @Around前置 → @Before → 目标方法 → @AfterReturning → @After → @Around后置

出现异常: @Around前置 → @Before → 目标方法抛异常 → @AfterThrowing → @After

四、Pointcut 切入点表达式详解

1. execution 执行表达式

语法: execution(返回值 包.类.方法(参数)) 通配符:

  • *:任意返回值、任意类、任意方法、单个参数
  • ..:任意多层包、任意多个参数

示例:

java

// 匹配com.demo.service下所有类所有方法
execution(* com.demo.service.*.*(..))

// 匹配service包及所有子包
execution(* com.demo.service..*.*(..))

// 仅匹配返回值void的方法
execution(void com.demo.controller.*.*(..))

// 匹配带一个String参数的方法
execution(* com.demo.*.*(String))

2. @annotation 注解匹配

自定义注解:

java

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {}

切面切入点:只拦截加了@LogRecord注解的方法

java

@Pointcut("@annotation(com.demo.annotation.LogRecord)")

3. within 按类匹配

java

// 匹配controller包下所有类
within(com.demo.controller.*)

4. args 按参数类型匹配

java

// 拦截第一个参数为Long的方法
args(Long,..)

五、JoinPoint / ProceedingJoinPoint 常用 API

仅环绕通知使用 ProceedingJoinPoint,其余通知用 JoinPoint

java

// 获取方法签名
Signature signature = joinPoint.getSignature();
MethodSignature methodSig = (MethodSignature) signature;
Method method = methodSig.getMethod();

// 获取方法入参
Object[] args = joinPoint.getArgs();

// 获取目标对象
Object target = joinPoint.getTarget();

// 获取代理对象
Object proxy = joinPoint.getThis();

// 获取方法名
String methodName = joinPoint.getSignature().getName();

// 环绕专属:执行目标方法
Object result = pjp.proceed();
// 带参数执行(修改入参)
Object result = pjp.proceed(new Object[]{newParam});

六、经典实战场景

场景 1:自定义注解实现接口日志记录

  1. 自定义@LogRecord注解,注解上存模块、操作类型
  2. AOP 拦截该注解,获取注解属性、请求参数、返回值、耗时存入数据库

场景 2:全局接口权限校验

AOP 拦截 Controller 方法,从 Token 获取用户,判断是否拥有接口权限,无权限直接抛出异常

场景 3:统一接口性能监控

环绕通知记录每个接口执行耗时,超时接口打印告警日志

场景 4:统一异常处理补充

AOP 捕获方法异常,记录错误堆栈,配合 @RestControllerAdvice 全局异常返回统一格式

场景 5:分布式事务、本地事务管理(@Transactional 底层就是 AOP)

Spring 事务本质:AOP 环绕通知,方法前开启事务,正常提交、异常回滚

七、AOP 执行顺序与多切面排序

1. 多个切面执行顺序

使用 @Order(int) 指定优先级,数值越小,优先级越高:

  • 进入方法:Order 小的切面先执行
  • 退出方法:Order 小的切面后执行

java

@Aspect
@Component
@Order(1) // 优先级高于Order(2)
public class LogAspect {}

2. 同一切面内通知顺序

Around → Before → 方法执行 → AfterReturning/AfterThrowing → After

八、常见坑与面试重点

1. 内部调用 AOP 失效

问题:同一个类中 A方法() 调用本类 B方法(),B 上的切面不执行。 原因:内部调用使用原始this对象调用,不是代理对象,不会走 AOP 拦截。 解决方案:

  1. 注入自身代理对象:@Autowired private 当前类 self;,用self.B()调用
  2. 拆分方法到不同类
  3. 使用 Spring 上下文获取代理

2. final/static 方法无法被 AOP 拦截

CGLIB 通过继承重写方法,final 不能重写;static 属于类,不属于实例,无法代理。

3. 私有方法不能拦截

Spring AOP 仅拦截 public 方法。

4. @Transactional 失效场景

  1. 方法 private/static/final
  2. 同类内部调用
  3. 异常被 try-catch 捕获,未抛出
  4. 传播行为配置错误

5. JDK 代理只能拦截接口方法

目标类无接口自动切换 CGLIB;SpringBoot 默认 CGLIB。

九、AOP 与 AspectJ 区别

  1. Spring AOP

    • 运行时织入,动态代理实现
    • 仅支持方法连接点,轻量,无需额外编译
    • 开发主流,Spring 内置
  2. 原生 AspectJ

    • 编译期 / 加载期织入,修改字节码
    • 支持方法、构造器、字段等全部连接点
    • 需要 ajc 编译器,较重,一般项目不用

十、完整开发流程总结

  1. 引入 aop starter
  2. 启动类加 @EnableAspectJAutoProxy
  3. 编写切面类:@Aspect + @Component
  4. 定义 @Pointcut 切入点表达式
  5. 编写 5 种通知逻辑,通过 JoinPoint 获取方法信息
  6. @Order 控制多切面顺序
  7. 避开内部调用、private、final 等失效场景

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/zgzldnr/article/details/162914302

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--