文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Java 框架 Spring AOP 详细分析介绍

一、AOP 核心概念:解决何种问题?

在软件开发中,像日志记录、事务管理、安全校验这样的功能,会散布在许多业务模块中。这些功能被称为横切关注点 (Cross-Cutting Concerns)

  • 传统 OOP 的困境:会导致“代码纠缠”和“代码分散”,使得核心业务逻辑变得不清晰,难以维护。
    public void transfer(Account from, Account to, double amount) {
        // 1. 开始事务 (分散的横切逻辑)
        beginTransaction();
        try {
            // 2. 核心业务逻辑
            from.debit(amount);
            to.credit(amount);
            // 3. 提交事务 (分散的横切逻辑)
            commitTransaction();
        } catch (Exception e) {
            // 4. 回滚事务 (分散的横切逻辑)
            rollbackTransaction();
            // 5. 记录日志 (分散的横切逻辑)
            log.error("Transfer failed", e);
            throw e;
        }
    }
    

AOP (Aspect-Oriented Programming) 的出现,就是为了解决这一问题。它允许我们将这些横切关注点模块化,然后通过声明的方式定义它们应该应用在程序的哪些地方。最终,由 AOP 框架在运行时或编译时,自动将这些模块化的逻辑织入 (Weave) 到指定的位置。

核心术语

  • Aspect (切面):横切关注点的模块化。它是一个类,上面标注了 @Aspect 注解。它包含了 AdvicePointcut
  • Join Point (连接点):程序执行过程中一个明确的点,如方法调用、异常抛出、字段修改等。Spring AOP 仅支持方法执行 (Method Execution) 这一种连接点。
  • Advice (通知):切面在特定连接点采取的动作。它定义了“做什么”和“何时做”。
  • Pointcut (切入点):一个匹配连接点的表达式。它定义了“在哪里”应用通知。这是 AOP 的核心,决定了通知应该织入到哪个或哪些方法上。
  • Weaving (织入):将切面应用到目标对象,从而创建代理对象的过程。Spring AOP 在运行时完成织入。
  • Target Object (目标对象):被一个或多个切面所通知的对象。也就是被织入横切逻辑的核心业务对象。
  • AOP Proxy (AOP 代理):由 AOP 框架创建的对象,它是目标对象的增强版本。在 Spring AOP 中,代理可以是 JDK 动态代理CGLIB 代理

二、Spring AOP 的实现原理:动态代理

Spring AOP 的本质是在 IoC 容器管理 Bean 生命周期的过程中,通过动态代理技术,在合适的时机(BeanPostProcessor)为 Bean 创建代理对象

2.1 两种代理机制

  1. JDK Dynamic Proxy (基于接口的代理)

    • 机制:通过 java.lang.reflect.ProxyInvocationHandler 实现。
    • 条件目标类实现了至少一个接口
    • 特点:代理对象会实现目标类所实现的所有接口。外部调用通过接口进行,代理对象拦截所有接口方法调用。
    • 性能:生成代理类较快。
  2. CGLIB Proxy (基于子类的代理)

    • 机制:通过操作字节码,生成目标类的一个子类,并重写其中的方法。
    • 条件:目标类没有实现任何接口,或者配置了强制使用 CGLIB (proxy-target-class="true")。
    • 特点:代理对象是目标类的子类。它不能代理 final 类或 final 方法。
    • 性能:生成的代理类在运行时调用更快,但生成过程稍慢。

Spring 的默认策略:如果目标对象实现了接口,则使用 JDK 动态代理;否则使用 CGLIB。但可以通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB。

2.2 核心源码:代理的创建时机

代理对象的创建发生在 Bean 生命周期的初始化后阶段,由 BeanPostProcessor 完成。

核心类是 AnnotationAwareAspectJAutoProxyCreator,它实现了 BeanPostProcessor 接口。

// 在 AbstractAutowireCapableBeanFactory.initializeBean() 中调用
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        // 1. 为每个 Bean 构建一个 key (通常是 beanName)
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 2. ★ 核心方法:如果需要,则包装 Bean (创建代理)
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // ... (检查是否已经处理过、是否是基础设施Bean等)

    // 1. ★★★ 获取适用于此 Bean 的 Advisor (通知器) ★★★
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // 2. ★★★ 创建代理对象 ★★★
        Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        return proxy;
    }
    // 如果不需要代理,返回原始 Bean
    return bean;
}
  • getAdvicesAndAdvisorsForBean():这个方法会扫描所有 @Aspect 注解的类(即切面),解析其中的 @Pointcut@Before 等注解,并使用 Pointcut 表达式去匹配当前 Bean 的所有方法。如果匹配成功,则返回对应的 Advisor(封装了 Advice 和 Pointcut)链。
  • createProxy():根据配置和目标对象的情况,选择使用 JDK 动态代理或 CGLIB 来创建最终的代理对象。

2.3 代理对象的执行流程

当一个方法在代理对象上被调用时,会触发一个拦截器链(MethodInterceptor)。Spring AOP 将不同的 Advice 类型(@Before, @After等)都封装成对应的 MethodInterceptor

其核心拦截与执行流程,特别是责任链模式的应用,可以通过以下流程图清晰地展现:

ClientAOP ProxyMethodInvocation (Proxied)MethodInterceptor1 (e.g., Before Advice)MethodInterceptor2 (e.g., After Advice)MethodInterceptorN (e.g., Around Advice)Target Objectmethod.invoke()Proxy holds Advisor chainand target objectCreates ReflectiveMethodInvocation(chain, target, method, args)invoke(MethodInvocation)e.g., @Before AdviceExecute before logicproceed() // Pass to next interceptorloop[Through Interceptor Chain]invoke(MethodInvocation)@Around Advice (innermost)1. Pre-logic2. Decides to call proceed()proceed() // Passes to the "next" interceptorAfter last interceptor, the "next" is the target methodinvokeJoinpoint() → method.invoke(target, args)Return result (or throw exception)Returns result (or exception)3. Post-logic (finally block)Can modify result or exceptionReturns resultloop[Back through Interceptor Chain (Reverse)]Returns final resultReturns resultClientAOP ProxyMethodInvocation (Proxied)MethodInterceptor1 (e.g., Before Advice)MethodInterceptor2 (e.g., After Advice)MethodInterceptorN (e.g., Around Advice)Target Object

MethodInvocation.proceed() 是关键。它维护着一个当前拦截器的索引计数器。每次调用 proceed(),计数器递增,并调用链中的下一个拦截器。当所有拦截器都执行完后,最终会调用目标方法本身 (invokeJoinpoint())。然后,结果(或异常)会沿着调用链反向返回,因此 @Around 通知可以在方法调用后执行逻辑。


三、Advice 类型详解

Spring AOP 支持 5 种类型的通知,它们定义了“何时”执行切面代码。

注解时机说明
@Before在目标方法执行之前执行。适用于权限校验、日志记录等。
@AfterReturning在目标方法成功完成之后执行。可以访问方法的返回值。
@AfterThrowing在目标方法抛出异常之后执行。可以访问抛出的异常。
@After (@Finally)在目标方法完成之后执行,无论结果是成功还是异常。类似于 finally 块,用于释放资源等。
@Around环绕目标方法执行。功能最强大的通知。它可以控制是否执行目标方法,何时执行,甚至可以修改参数和返回值。它必须接收一个 ProceedingJoinPoint 参数,并调用其 proceed() 方法来执行目标方法。

@Around 通知示例

@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    // 1. 目标方法执行前的逻辑
    long start = System.currentTimeMillis();
    Object[] args = pjp.getArgs();
    // 可以修改参数...

    try {
        // 2. 决定是否执行目标方法 (可以完全跳过)
        Object result = pjp.proceed(args); // 继续执行链,调用目标方法

        // 3. 目标方法成功执行后的逻辑 (可以修改返回值)
        long end = System.currentTimeMillis();
        logger.info("Method executed in " + (end - start) + "ms");
        return result;
    } catch (Exception e) {
        // 4. 目标方法抛出异常后的逻辑
        logger.error("Method threw an exception", e);
        throw e; // 可以重新抛出原异常,或抛出新的异常
    }
}

四、Pointcut 表达式与指示器

Pointcut 用于定义通知应该织入的位置。Spring AOP 使用了 AspectJ 的切点表达式语言

4.1 execution 指示器

这是最常用的主指示器,用于匹配方法执行连接点。
语法:execution([modifiers] return-type [declaring-type].method-name(param-list) [throws-pattern])

  • *:匹配任意字符(但只能匹配一个部分)
  • ..:匹配任意字符,可用于包名和参数列表(匹配多个部分)
  • +:匹配指定类型及其子类型(仅用于声明类型)

示例

  • execution(* com.example.service.*.*(..)):匹配 com.example.service 包下任何类的任何方法。
  • execution(* com.example.service.UserService.*(..)):匹配 UserService 接口的所有方法。
  • execution(* save*(..)):匹配所有以 save 开头的方法。
  • execution(* com.example.service..*.*(..)):匹配 com.example.service 包及其所有子包下任何类的任何方法。

4.2 within 指示器

限制匹配到特定类型内的连接点。

  • within(com.example.service.*):匹配 com.example.service 包下的所有方法。
  • within(com.example.service..*):匹配 com.example.service 包及其子包下的所有方法。

4.3 args 指示器

限制匹配到参数符合指定类型的方法。

  • @Around("execution(* *..find*(..)) && args(id,..)"):匹配任何 find 开头且第一个参数为 id 的方法,并可以在通知中访问该参数。

4.4 @annotation 指示器

匹配带有指定注解的方法。

  • @Around("@annotation(com.example.annotation.MyLog)"):匹配任何被 @MyLog 注解标记的方法。

4.5 组合使用

可以使用 && (and), || (or), ! (not) 来组合切点表达式。
@Before("execution(* com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)"):匹配 service 包下所有被 @Transactional 注解的方法。


五、Spring AOP 的局限性

  1. 仅支持方法级别的织入:无法织入字段、构造器、静态初始化块等连接点。
  2. 仅适用于 Spring 容器管理的 Bean:只能对由 Spring IoC 容器初始化的 Bean 进行代理。
  3. 自调用问题:同一个类中的一个方法调用另一个方法,被调用的方法上的 AOP 通知不会生效。因为自调用是通过 this 引用(即目标对象本身)进行的,而不是通过代理对象。
    public class UserService {
        public void a() {
            this.b(); // `this` 是真实对象,不是代理,因此 b() 的 AOP 通知不会生效。
        }
        @Transactional
        public void b() {
            // save to DB
        }
    }
    
  4. 性能考量:虽然现代 JVM 对动态代理优化得很好,但大量使用 AOP 还是会带来轻微的性能开销。

六、最佳实践与总结

  • 选择合适的通知类型:优先使用最不强大的通知类型能满足需求。例如,如果只是记录日志,用 @Before@AfterReturning 而不是 @Around
  • 保持切面轻量:切面中的逻辑应该简单高效,避免耗时的操作,因为它们会影响所有被织入的方法。
  • 精确的切入点表达式:尽量编写精确的表达式,避免过于宽泛的匹配(如 execution(* *(..))),以免意外织入不需要的方法,影响性能和预期行为。
  • 处理自调用:如果需要在同一对象内进行有 AOP 通知的方法调用,可以通过 AopContext.currentProxy() 获取当前代理对象,然后通过它来调用方法(需要配置 exposeProxy = true)。
  • 理解代理机制:清楚你的 Bean 是被 JDK 代理还是 CGLIB 代理,这在某些场景下(如类型转换、获取类信息)可能会有影响。

总结:Spring AOP 是一个强大而实用的框架,它通过动态代理BeanPostProcessor 扩展机制,将 AspectJ 的切面编程模型无缝集成到了 Spring IoC 容器中。它有效地将横切关注点模块化,是实现声明式事务(@Transactional)、安全、日志等功能的基石。理解其原理和局限性,有助于我们更好地在项目中应用它。对于更复杂的需求(如构造器、字段织入),则需要使用完整的 AspectJ

posted @ 2025-09-10 15:55  NeoLshu  阅读(6)  评论(0)    收藏  举报  来源