DEV Community

Liu yu
Liu yu

Posted on

Spring AOP 代理

核心概念:Spring AOP 和代理模式

Spring AOP(面向切面编程)是基于代理模式实现的。代理模式的核心思想是通过一个中间对象(代理)来控制对实际对象(目标对象)的访问。Spring AOP 使用代理来实现横切关注点(如日志、事务管理、性能监控等)的功能,而不需要直接修改目标对象的代码。

在你的代码示例中,Spring AOP 使用代理包装了目标对象(SimplePojo),使得方法调用不再直接发生在目标对象上,而是通过代理对象进行。这会导致行为上的差异,下面逐步解析。


1. 没有代理的情况(直接调用)

在没有代理的情况下,代码的行为非常直观:

public class SimplePojo implements Pojo {
    public void foo() {
        // 直接调用 this.bar()
        this.bar();
    }

    public void bar() {
        // 一些逻辑...
    }
}

public class Main {
    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        pojo.foo(); // 直接调用 SimplePojo 的 foo 方法
    }
}
Enter fullscreen mode Exit fullscreen mode

行为分析:

  • pojo 是一个 SimplePojo 对象的直接引用。
  • 调用 pojo.foo() 时,JVM 直接执行 SimplePojo 类的 foo 方法。
  • foo 方法中,this.bar() 是对当前对象的直接方法调用,调用的是 SimplePojobar 方法。
  • 没有任何额外的逻辑插入,所有方法调用都在同一个对象内部完成,行为完全由目标对象的代码决定。

2. 有代理的情况(通过代理调用)

当使用 Spring AOP 的代理时,情况发生了变化。以下是代码片段:

public class Main {
    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        Pojo pojo = (Pojo) factory.getProxy();
        pojo.foo(); // 通过代理调用 foo 方法
    }
}
Enter fullscreen mode Exit fullscreen mode

行为分析:

  • ProxyFactory 创建了一个代理对象,包装了 SimplePojo 实例。
  • pojo 不再是 SimplePojo 的直接引用,而是一个代理对象(实现 Pojo 接口)。
  • 当调用 pojo.foo() 时,实际上调用的是代理对象的 foo 方法,而不是直接调用 SimplePojofoo 方法。
  • 代理对象会根据配置的 Advice(通知)(如 RetryAdvice)在方法调用前后插入额外的逻辑。例如,RetryAdvice 可能会在方法失败时重试。
  • 因此,方法调用会经过以下步骤:
    1. 客户端调用代理的 foo 方法。
    2. 代理执行前置通知(如果有)。
    3. 代理将调用委托给目标对象(SimplePojo)的 foo 方法。
    4. 目标对象的 foo 方法执行,并可能调用 this.bar()
    5. 代理执行后置通知(如果有)。

3. 为什么加代理就不一样了?

代理的引入改变了方法调用的行为,具体原因如下:

(1)方法调用的拦截

  • 无代理pojo.foo() 直接调用 SimplePojofoo 方法,JVM 直接执行目标对象的代码。
  • 有代理pojo.foo() 调用的是代理对象的 foo 方法。代理对象可以拦截这个调用,执行额外的逻辑(由 Advice 定义),然后决定是否将调用传递给目标对象。
  • 例如,RetryAdvice 可能在 foo 方法失败时自动重试,这是在无代理情况下不可能实现的。

(2)this 的行为差异

  • SimplePojofoo 方法中,this.bar() 是对当前对象的直接调用。
  • 无代理this 指向 SimplePojo 实例,因此 this.bar() 直接调用 SimplePojobar 方法。
  • 有代理this 仍然指向 SimplePojo 实例(目标对象),而不是代理对象。因此,this.bar() 不会经过代理,而是直接调用目标对象的 bar 方法。这意味着 bar 方法的调用不会被代理拦截,也无法应用 Advice(如 RetryAdvice)。
  • 如果你希望 bar 方法也被代理拦截,必须通过代理对象调用 bar(例如,通过 pojo.bar() 而不是 this.bar()),或者使用其他技术(如 AspectJ 编译时织入)。

(3)代理带来的额外功能

  • 代理允许在方法调用前后插入横切逻辑(Advice),例如:
    • 前置通知(Before Advice):在方法调用前执行某些操作(如日志记录)。
    • 后置通知(After Advice):在方法调用后执行某些操作(如清理资源)。
    • 环绕通知(Around Advice):完全控制方法的执行,甚至可以决定是否调用目标方法。
  • 在示例中,RetryAdvice 可能是一个环绕通知,用于在方法失败时重试。这种功能是直接调用无法实现的。

(4)代理的类型

Spring AOP 支持两种代理方式:

  • JDK 动态代理:基于接口,代理对象实现目标对象的接口(如 Pojo)。这是示例中使用的代理方式(factory.addInterface(Pojo.class))。
  • CGLIB 代理:基于子类,适用于没有接口的类。Spring 会根据情况自动选择合适的代理方式。
  • 无论是哪种代理,调用都会先到达代理对象,而不是直接到达目标对象。

4. 为什么需要理解代理的语义?

正如文档开头所说,“Spring AOP 是基于代理的”,理解代理的语义至关重要,因为:

  • 方法调用的路径不同:通过代理调用和直接调用的行为不同,代理可能会插入额外的逻辑。
  • 内部调用问题:如前所述,this.bar() 不会经过代理,这可能导致 Advice 不生效。开发者需要明确哪些方法调用会被代理拦截。
  • 性能影响:代理引入了额外的调用层,可能会对性能产生轻微影响。
  • 配置复杂性:正确配置代理(选择接口、Advice、代理类型等)需要理解代理的工作原理。

5. 举个例子说明差异

假设 RetryAdvice 是一个重试逻辑,代码如下:

public class RetryAdvice implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        int retries = 3;
        while (retries > 0) {
            try {
                return invocation.proceed(); // 调用目标方法
            } catch (Exception e) {
                retries--;
                if (retries == 0) throw e;
                System.out.println("Retrying...");
            }
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

无代理:

  • 调用 pojo.foo() 直接执行 SimplePojofoo 方法。
  • 如果 foo 方法抛出异常,不会触发任何重试逻辑。

有代理:

  • 调用 pojo.foo() 时,调用被 RetryAdvice 拦截。
  • 如果 foo 方法抛出异常,RetryAdvice 会捕获异常并重试最多 3 次。
  • 但如果 foo 方法内部调用 this.bar()bar 方法不会被代理拦截,因此不会触发重试逻辑。

6. 总结

  • 无代理:方法调用直接发生在目标对象上,行为由目标对象的代码决定。
  • 有代理:方法调用先到达代理对象,代理可以插入额外的逻辑(如重试、日志、事务等),然后再决定是否调用目标对象。
  • 差异原因:代理模式允许拦截和增强方法调用,但内部调用(this.method())不会经过代理,导致 Advice 不生效。
  • 解决内部调用问题:可以通过依赖注入获取代理对象,或者使用 AspectJ(编译时或加载时织入)来解决。

希望这个解析清晰地说明了为什么加了代理后行为会不同!如果有进一步的问题,欢迎继续问我。

Top comments (0)