javaweb-Day11-Spring AOP

TJCcc 发布于 3 天前 5 次阅读


前言

AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架的两大核心基石(IoC + AOP)之一。它弥补了 OOP 在横切关注点(如日志、事务、权限、监控等)上的代码冗余问题。本文将深入 Spring AOP 底层机制、核心概念、通知进阶及实战细节,帮你构建完整的 AOP 知识体系。


1. AOP 底层原理(代理模式)

Spring AOP 本质上是 动态代理 的封装。它不修改源代码,而是在运行期通过代理对象增强目标方法。

Spring 底层采用两种代理策略,由 DefaultAopProxyFactory 根据目标类自动抉择:

代理方式实现机制适用条件性能特点
JDK 动态代理实现 InvocationHandler,通过反射生成目标接口的代理类目标类实现了至少一个接口反射开销较大,随 JDK 版本升级不断优化(JDK 8+ 性能接近 CGLIB)
CGLIB 动态代理基于 ASM 字节码框架,生成目标类的子类(继承机制),重写父类方法目标类未实现接口,或强制使用 proxyTargetClass=true方法调用通过 FastClass 机制,性能较高,但代理类创建较慢

Spring Boot 2.x+ 默认策略:如果目标类有接口,优先使用 JDK 动态代理;若想强制使用 CGLIB,可在配置中设置 spring.aop.proxy-target-class=true(Spring Boot 中默认已为 true 以增强兼容性)。

核心执行流程

调用方 -> 代理对象(Proxy) -> 拦截器链(MethodInterceptor) -> 目标方法(Target)

Spring AOP 并非 AspectJ 的编译时织入(字节码修改),而是运行期织入


2. AOP 核心概念详解(务必厘清)

术语英文解释(Spring 语境)
切面Aspect横切关注点的模块化,通常是一个带有 @Aspect 注解的类,内部包含通知和切入点。
连接点Join Point程序执行过程中能够被拦截的。在 Spring AOP 中,仅指方法的执行(区别于 AspectJ 的字段访问、构造器等)。方法调用前后、抛出异常时都是连接点。
切入点Pointcut一组连接点的集合,通过表达式(如 execution)定义哪些方法需要被增强。换言之,切入点定义了“在哪里”执行通知。
通知Advice切面在特定连接点执行的具体动作,定义了“做什么”和“何时做”(方法前、后、环绕等)。
引入Introduction允许切面为目标类添加新方法或属性(不常用)。
织入Weaving将切面应用到目标对象并创建代理对象的过程。Spring AOP 在运行期完成织入。
目标对象Target Object被增强的原始业务对象。

3. 通知类型(Advice Types)及其详解

Spring 支持五种通知注解,对应不同的拦截时机:

注解执行时机适用场景注意事项
@Before目标方法执行之前权限校验、参数预处理、日志记录若抛出异常,会阻止目标方法执行
@After目标方法执行之后(无论是否抛出异常,类似 finally资源释放、清理缓存无法获取返回值(若异常则无返回值)
@AfterReturning目标方法正常返回后执行对返回值进行二次处理、记录成功日志可通过 returning 属性绑定返回值参数
@AfterThrowing目标方法抛出异常后执行异常监控、发送告警、回滚补偿可通过 throwing 属性绑定异常参数
@Around环绕通知,包裹整个目标方法,可自定义执行时机性能监控(耗时统计)、重试机制、事务管理必须接收 ProceedingJoinPoint,手动调用 proceed();否则目标不执行

代码示例

@Aspect
@Component
public class LogAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed(); // 执行目标方法
        long cost = System.currentTimeMillis() - start;
        System.out.println("【Around】耗时: " + cost + "ms");
        return result;
    }

    @Before("execution(* com.example.service.*.*(..))")
    public void beforeLog(JoinPoint jp) {
        System.out.println("【Before】方法即将执行: " + jp.getSignature().getName());
    }

    @After("execution(* com.example.service.*.*(..))")
    public void afterLog() {
        System.out.println("【After】方法执行完毕(finally逻辑)");
    }

    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "retVal")
    public void afterReturningLog(Object retVal) {
        System.out.println("【AfterReturning】返回值: " + retVal);
    }

    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
    public void afterThrowingLog(Exception ex) {
        System.out.println("【AfterThrowing】异常: " + ex.getMessage());
    }
}

4. 通知执行顺序(高频面试考点)

4.1 同一切面类中不同通知的顺序

在一个切面类中,如果多个通知匹配同一个连接点,其执行顺序遵循 “环绕前置 -> Before -> 目标方法 -> 环绕后置 -> After -> AfterReturning/AfterThrowing” 的嵌套逻辑。

具体时序

  1. @Aroundproceed() 之前的代码
  2. @Before
  3. 目标方法执行
  4. @Aroundproceed() 之后的代码
  5. @After(无论如何都执行)
  6. @AfterReturning(正常返回) @AfterThrowing(抛出异常)

同一切面内通知执行顺序不依赖代码声明顺序,而是严格按照上述织入顺序。但为了可读性,建议按逻辑顺序排列。

4.2 不同切面类中同一通知的顺序(如多个 @Before)

当多个切面类同时作用于同一目标方法时,需要通过 @Order 注解(或实现 Ordered 接口)来控制执行顺序。

规则

  • @Order 数值越小,优先级越高
  • 前置通知(@Before):优先级高的先执行
  • 后置通知(@After / @AfterReturning):优先级高的后执行(先进后出,类似于栈的嵌套)。

示例

@Aspect
@Component
@Order(1) // 优先级高
public class TransactionAspect { ... }

@Aspect
@Component
@Order(2) // 优先级低
public class LogAspect { ... }

如果有 @Before@Around,先执行高优先级切面的 @Around 前置部分,再执行该切面的 @Before,再进入低优先级切面的包裹,以此类推。整体遵循多层洋葱模型


5. 切入点表达式(Pointcut Expressions)

切入点表达式用于精准定位需要织入的方法。Spring 支持多种指示器(PCD),最常用的是 execution@annotation

5.1 execution(基础且最强大)

语法格式:

execution(修饰符? 返回类型 声明类型? 方法名(参数) 异常?)

全量示例及通配符详解

表达式示例含义
execution(public * com.example.UserService.*(..))UserService 类中所有公共方法,任意返回类型,任意参数
execution(* com.example.service..*.*(..))service 包及其子包下所有类的所有方法
execution(* find*(..))所有以 "find" 开头的方法,任意参数
execution(* save*(String, ..))以 save 开头,第一个参数为 String,后面任意参数的方法
execution(* update*(*))以 update 开头,且恰好一个参数(任意类型)的方法

参数匹配详解

  • (..) :零个或多个任意参数。
  • (*) :一个任意类型的参数。
  • (String, Integer) :必须恰好两个参数,且类型分别为 String 和 Integer。
  • (String, ..) :第一个参数为 String,后面可有可无任意参数。

5.2 @annotation(自定义注解驱动)

通过自定义注解来标记切点,实现更灵活、非侵入式的增强。这是企业级开发中最优雅的实践方式。

Step 1:定义自定义注解

@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
    String module() default "";
    boolean recordParams() default true;
}

Step 2:定义切面,匹配该注解

@Aspect
@Component
public class CustomAnnotationAspect {

    // 匹配任何标注了 @LogExecution 的方法
    @Pointcut("@annotation(logExecution)")
    public void pointcut(LogExecution logExecution) {}

    @Around("pointcut(logExecution)")
    public Object around(ProceedingJoinPoint pjp, LogExecution logExecution) throws Throwable {
        String module = logExecution.module();
        System.out.println("模块: " + module + ", 开始执行...");
        if (logExecution.recordParams()) {
            Object[] args = pjp.getArgs();
            System.out.println("参数: " + Arrays.toString(args));
        }
        return pjp.proceed();
    }
}

Step 3:在业务方法上使用

@Service
public class UserService {
    @LogExecution(module = "用户管理")
    public void deleteById(Long id) {
        // 业务逻辑
    }
}

其他常用指示器@within(匹配标注了指定注解的类中的所有方法)、bean(userService)(匹配指定 Bean 名称)。


6. 连接点(JoinPoint)的深入使用

在通知方法中,我们可以通过参数获取连接点的上下文信息。这是排查问题和获取运行时元数据的关键。

  • JoinPoint:用于 @Before@After@AfterReturning@AfterThrowing
  • ProceedingJoinPoint:用于 @Around(它是 JoinPoint 的子接口,增加了 proceed() 方法)。

常用 API

@Before("execution(* com.example..*.*(..))")
public void inspect(JoinPoint jp) {
    // 1. 获取方法签名
    MethodSignature signature = (MethodSignature) jp.getSignature();
    Method method = signature.getMethod(); // 获取 Method 对象(反射)

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

    // 3. 获取目标对象(原始 Bean)
    Object target = jp.getTarget();

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

    // 5. 获取方法参数
    Object[] args = jp.getArgs();

    // 6. 获取声明的参数名称(需开启 -parameters 编译选项或使用 ASM)
    String[] paramNames = signature.getParameterNames();
}

实战技巧:在 @Around 中,如果需要在 proceed() 前后修改参数,可以修改 args 数组后传入 pjp.proceed(args),实现在不修改原始代码的情况下动态修改入参。


7. 最佳实践与避坑指南

问题解决方案
内部方法调用不生效AOP 基于代理,同类中 methodA() 调用 @Transactional@AroundmethodB() 不会走代理。解决:注入自身代理(@Autowired)或通过 AopContext.currentProxy()
切面执行多次检查是否配置了多个代理(如 Spring 代理 + AspectJ 自动代理),或切面类未被 @Component 扫描但又被重复定义了。
@AfterReturning 拿不到返回值检查 returning 属性名称是否与参数名完全一致
@AfterThrowing 捕获不到异常检查 throwing 属性参数类型必须匹配异常类型(或为 Throwable),且该异常未被方法签名忽略。
切入点表达式过于宽泛尽量缩小扫描范围(如限制在 service 包下),避免对框架内部类(如 BeanFactory)进行代理,导致性能下降或栈溢出。
CGLIB 代理无法访问 final 方法代理类无法重写 final 方法,目标方法若非 public 需注意代理限制。

8. 总结速查表

关注点核心结论
底层原理运行期动态代理(JDK Proxy / CGLIB),非编译时织入
核心三要素切面(Aspect) = 切入点(Pointcut) + 通知(Advice)
通知执行顺序(单切面)Around前置 -> Before -> 目标 -> Around后置 -> After -> AfterReturning/AfterThrowing
通知执行顺序(多切面)@Order 值小者优先;Before 正序,After 逆序(嵌套执行)
最常用切入点execution(* com.xxx.service..*.*(..))
企业级优雅切点@annotation(自定义注解),配合参数绑定,松耦合且直观
获取方法信息ProceedingJoinPoint(Around) / JoinPoint(其他)

掌握以上内容,你不仅能熟练使用 Spring AOP 解决横切问题,还能在面试中清晰阐述其原理与底层细节。写 AOP 时多想想“代理对象 vs 目标对象”,多思考“何时使用注解 vs execution”,会让你的代码更加健壮优雅。

唯有极致沉淀,才能造就辉煌。
最后更新于 2026-08-20