为什么自调用不适用于 Spring 代理(例如使用 AOP)?

kru*_*und 7 proxy spring spring-aop

请解释一下,为什么在目标上执行代理上的自调用而不是代理?如果是故意的,那又是为什么呢?如果通过子类创建代理,则可以在每次方法调用之前执行一些代码,即使在自调用时也是如此。我试过了,我有自我调用的代理

public class DummyPrinter {
    public void print1() {
        System.out.println("print1");
    }

    public void print2() {
        System.out.println("print2");
    }

    public void printBoth() {
        print1();
        print2();
    }
}
Run Code Online (Sandbox Code Playgroud)
public class PrinterProxy extends DummyPrinter {
    @Override
    public void print1() {
        System.out.println("Before print1");
        super.print1();
    }

    @Override
    public void print2() {
        System.out.println("Before print2");
        super.print2();
    }

    @Override
    public void printBoth() {
        System.out.println("Before print both");
        super.printBoth();
    }
}
Run Code Online (Sandbox Code Playgroud)
public class Main {
    public static void main(String[] args) {
        DummyPrinter p = new PrinterProxy();
        p.printBoth();
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Before print both
Before print1
print1
Before print2
print2
Run Code Online (Sandbox Code Playgroud)

这里每个方法都在代理上调用。为什么在文档中提到在自调用的情况下应该使用 AspectJ?

kri*_*aex 11

请阅读Spring手册中的这一章,然后您就会明白。甚至在那里使用了术语“自调用”。如果您仍然不明白,请随时提出后续问题,只要它们在上下文中即可。


更新:好的,现在在我们确定您真的阅读了该章节并重新阅读了您的问题并分析了您的代码之后,我发现这个问题实际上非常深刻(我什至赞成)并且值得更详细地回答。

你关于它是如何工作的(错误的)假设

您的误解是关于动态代理的工作方式,因为它们不像您的示例代码那样工作。让我将对象 ID(哈希码)添加到日志输出中,以便在您自己的代码中进行说明:

package de.scrum_master.app;

public class DummyPrinter {
  public void print1() {
    System.out.println(this + " print1");
  }

  public void print2() {
    System.out.println(this + " print2");
  }

  public void printBoth() {
    print1();
    print2();
  }
}
Run Code Online (Sandbox Code Playgroud)
package de.scrum_master.app;

public class PseudoPrinterProxy extends DummyPrinter {
  @Override
  public void print1() {
    System.out.println(this + " Before print1");
    super.print1();
  }

  @Override
  public void print2() {
    System.out.println(this + " Before print2");
    super.print2();
  }

  @Override
  public void printBoth() {
    System.out.println(this + " Before print both");
    super.printBoth();
  }

  public static void main(String[] args) {
    new PseudoPrinterProxy().printBoth();
  }
}
Run Code Online (Sandbox Code Playgroud)

控制台日志:

de.scrum_master.app.PseudoPrinterProxy@59f95c5d Before print both
de.scrum_master.app.PseudoPrinterProxy@59f95c5d Before print1
de.scrum_master.app.PseudoPrinterProxy@59f95c5d print1
de.scrum_master.app.PseudoPrinterProxy@59f95c5d Before print2
de.scrum_master.app.PseudoPrinterProxy@59f95c5d print2
Run Code Online (Sandbox Code Playgroud)

看?总是有相同的对象 ID,这并不奇怪。由于多态性,您的“代理”(它不是真正的代理,而是静态编译的子类)的自调用有效。这由 Java 编译器负责。

它是如何工作的

现在请记住我们在这里谈论的是动态代理,即在运行时创建的子类和对象:

  • JDK 代理为实现接口的类工作,这意味着实现这些接口的类是在运行时创建的。在这种情况下,无论如何都没有超类,这也解释了为什么它只适用于公共方法:接口只有公共方法。
  • CGLIB 代理也适用于没有实现任何接口的类,因此也适用于受保护的和包范围的方法(虽然不是私有的,因为你不能覆盖它们,因此术语私有)。
  • 然而,关键的一点是,在上述两种情况下,创建代理时原始对象已经(并且仍然)存在,因此不存在多态性。情况是我们有一个动态创建的代理对象委托给原始对象,即我们有两个对象:代理和委托

我想这样说明:

package de.scrum_master.app;

public class DelegatingPrinterProxy extends DummyPrinter {
  DummyPrinter delegate;

  public DelegatingPrinterProxy(DummyPrinter delegate) {
    this.delegate = delegate;
  }

  @Override
  public void print1() {
    System.out.println(this + " Before print1");
    delegate.print1();
  }

  @Override
  public void print2() {
    System.out.println(this + " Before print2");
    delegate.print2();
  }

  @Override
  public void printBoth() {
    System.out.println(this + " Before print both");
    delegate.printBoth();
  }

  public static void main(String[] args) {
    new DelegatingPrinterProxy(new DummyPrinter()).printBoth();
  }
}
Run Code Online (Sandbox Code Playgroud)

看到不同?因此,控制台日志更改为:

de.scrum_master.app.DelegatingPrinterProxy@59f95c5d Before print both
de.scrum_master.app.DummyPrinter@5c8da962 print1
de.scrum_master.app.DummyPrinter@5c8da962 print2
Run Code Online (Sandbox Code Playgroud)

这是您在使用动态代理的 Spring AOP 或 Spring 的其他部分或什至使用 JDK 或 CGLIB 代理的非 Spring 应用程序中看到的行为。

这是功能还是限制?我作为 AspectJ(不是 Spring AOP)用户认为这是一个限制。也许其他人可能认为这是一项功能,因为由于在 Spring 中实现代理使用的方式,您原则上可以(取消)在运行时动态注册方面建议或拦截器,即每个原始对象(委托)都有一个代理,但是对于每个代理,都有一个在调用委托的原始方法之前和/或之后调用的拦截器的动态列表。这在非常动态的环境中可能是一件好事。我不知道你想多久使用一次。但是在 AspectJ 中,您还有if()切入点指示符,您可以使用它在运行时确定是否应用某些建议(拦截器的 AOP 语言)。

解决方案

为了解决这个问题,你可以做的是:

  • 切换到原生 AspectJ,使用Spring 手册中描述的加载时编织。或者,您也可以使用编译时编织,例如通过 AspectJ Maven 插件。

  • 如果你想坚持使用 Spring AOP,你需要让你的 bean 代理感知,即间接地也感知 AOP,这从设计的角度来看并不理想。我不推荐它,但它很容易实现:只需自我注入对组件的引用,例如@Autowire MyComponent INSTANCE,然后始终使用该 bean 实例调用方法:INSTANCE.internalMethod()。这样,所有调用都将通过代理并触发 Spring AOP 方面。