为什么自调用不适用于 Spring 代理(例如使用 AOP)?
Why does self-invocation not work for Spring proxies (e.g. with AOP)?
请解释一下,为什么对代理的自调用在目标上执行而不是代理?如果那是故意的,那为什么呢?如果通过子类化创建代理,则可以在每个方法调用之前执行一些代码,即使是在自调用时也是如此。我试过了,我有自我调用的代理
public class DummyPrinter {
public void print1() {
System.out.println("print1");
}
public void print2() {
System.out.println("print2");
}
public void printBoth() {
print1();
print2();
}
}
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();
}
}
public class Main {
public static void main(String[] args) {
DummyPrinter p = new PrinterProxy();
p.printBoth();
}
}
输出:
Before print both
Before print1
print1
Before print2
print2
这里每个方法都在代理上调用。为什么在文档中提到在自调用的情况下应该使用 AspectJ?
请阅读Spring手册中的this chapter,您就会明白。甚至那里使用了术语“self-invocation”。如果您仍然不明白,请随时提问 follow-up 问题,只要它们符合上下文即可。
更新: 好的,在我们确定您确实阅读了那一章之后 re-reading 您的问题和分析您的代码之后,我发现问题实际上是非常深刻(我什至赞成),值得更详细地回答。
你对它如何运作的(错误的)假设
您对动态代理的工作方式有误解,因为它们不像您的示例代码那样工作。让我将对象 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();
}
}
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();
}
}
控制台日志:
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
看到了吗?始终存在相同的对象 ID,这并不奇怪。由于 多态性 ,Self-invocation 您的“代理”(它不是真正的代理,而是静态编译的子类)的工作原理。这是由 Java 编译器处理的。
它是如何工作的
现在请记住我们在这里谈论的是动态代理,即子类和在运行时创建的对象:
- JDK 代理为 类 实现接口工作,这意味着 类 实现这些接口是在运行时创建的。在这种情况下无论如何都没有超类,这也解释了为什么它只适用于 public 方法:接口只有 public 方法。
- CGLIB 代理也适用于 类 未实现任何接口,因此也适用于受保护和 package-scoped 方法(但不是私有方法,因为您无法覆盖它们,因此称为私有)。
- 不过,关键的一点是,在上述两种情况下,在创建代理时,原始对象已经(并且仍然)存在,因此 没有多态性。情况是我们有一个动态创建的代理对象委托给原始对象,即我们有两个对象:一个代理和一个委托。
我想这样说明:
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();
}
}
看出区别了吗?因此控制台日志更改为:
de.scrum_master.app.DelegatingPrinterProxy@59f95c5d Before print both
de.scrum_master.app.DummyPrinter@5c8da962 print1
de.scrum_master.app.DummyPrinter@5c8da962 print2
这是您在 Spring AOP 或 Spring 的其他部分使用动态代理,甚至 non-Spring 应用程序通常使用 JDK 或 CGLIB 代理时看到的行为。
这是功能还是限制?作为 AspectJ(不是 Spring AOP)用户,我认为这是一个限制。也许其他人可能认为这是一项功能,因为由于在 Spring 中实现代理使用的方式,原则上您可以在运行时动态地(取消)注册方面建议或拦截器,即每个原始对象都有一个代理(委托),但对于每个代理,在调用委托的原始方法之后 and/or 之前都有一个动态的拦截器列表。这在非常动态的环境中可能是一件好事。我不知道您可能希望多久使用一次。但是在 AspectJ 中,您还有 if()
切入点指示符,您可以使用它在运行时确定是否应用某些建议(拦截器的 AOP 语言)。
解决方案
为了解决问题,您可以做的是:
切换到本机 AspectJ,使用 load-time 编织,如 Spring manual 中所述。或者,您也可以使用 compile-time 编织,例如通过 AspectJ Maven 插件。
如果你想坚持使用 Spring AOP,你需要使你的 bean proxy-aware,即间接 AOP-aware,这不太理想一种设计观点。我不推荐它,但它很容易实现:只需 self-inject 对组件的引用,例如@Autowired MyComponent INSTANCE
然后总是使用该 bean 实例调用方法:INSTANCE.internalMethod()
。这样,所有调用都将通过代理并触发 Spring AOP 方面。
请解释一下,为什么对代理的自调用在目标上执行而不是代理?如果那是故意的,那为什么呢?如果通过子类化创建代理,则可以在每个方法调用之前执行一些代码,即使是在自调用时也是如此。我试过了,我有自我调用的代理
public class DummyPrinter {
public void print1() {
System.out.println("print1");
}
public void print2() {
System.out.println("print2");
}
public void printBoth() {
print1();
print2();
}
}
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();
}
}
public class Main {
public static void main(String[] args) {
DummyPrinter p = new PrinterProxy();
p.printBoth();
}
}
输出:
Before print both
Before print1
print1
Before print2
print2
这里每个方法都在代理上调用。为什么在文档中提到在自调用的情况下应该使用 AspectJ?
请阅读Spring手册中的this chapter,您就会明白。甚至那里使用了术语“self-invocation”。如果您仍然不明白,请随时提问 follow-up 问题,只要它们符合上下文即可。
更新: 好的,在我们确定您确实阅读了那一章之后 re-reading 您的问题和分析您的代码之后,我发现问题实际上是非常深刻(我什至赞成),值得更详细地回答。
你对它如何运作的(错误的)假设
您对动态代理的工作方式有误解,因为它们不像您的示例代码那样工作。让我将对象 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();
}
}
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();
}
}
控制台日志:
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
看到了吗?始终存在相同的对象 ID,这并不奇怪。由于 多态性 ,Self-invocation 您的“代理”(它不是真正的代理,而是静态编译的子类)的工作原理。这是由 Java 编译器处理的。
它是如何工作的
现在请记住我们在这里谈论的是动态代理,即子类和在运行时创建的对象:
- JDK 代理为 类 实现接口工作,这意味着 类 实现这些接口是在运行时创建的。在这种情况下无论如何都没有超类,这也解释了为什么它只适用于 public 方法:接口只有 public 方法。
- CGLIB 代理也适用于 类 未实现任何接口,因此也适用于受保护和 package-scoped 方法(但不是私有方法,因为您无法覆盖它们,因此称为私有)。
- 不过,关键的一点是,在上述两种情况下,在创建代理时,原始对象已经(并且仍然)存在,因此 没有多态性。情况是我们有一个动态创建的代理对象委托给原始对象,即我们有两个对象:一个代理和一个委托。
我想这样说明:
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();
}
}
看出区别了吗?因此控制台日志更改为:
de.scrum_master.app.DelegatingPrinterProxy@59f95c5d Before print both
de.scrum_master.app.DummyPrinter@5c8da962 print1
de.scrum_master.app.DummyPrinter@5c8da962 print2
这是您在 Spring AOP 或 Spring 的其他部分使用动态代理,甚至 non-Spring 应用程序通常使用 JDK 或 CGLIB 代理时看到的行为。
这是功能还是限制?作为 AspectJ(不是 Spring AOP)用户,我认为这是一个限制。也许其他人可能认为这是一项功能,因为由于在 Spring 中实现代理使用的方式,原则上您可以在运行时动态地(取消)注册方面建议或拦截器,即每个原始对象都有一个代理(委托),但对于每个代理,在调用委托的原始方法之后 and/or 之前都有一个动态的拦截器列表。这在非常动态的环境中可能是一件好事。我不知道您可能希望多久使用一次。但是在 AspectJ 中,您还有 if()
切入点指示符,您可以使用它在运行时确定是否应用某些建议(拦截器的 AOP 语言)。
解决方案
为了解决问题,您可以做的是:
切换到本机 AspectJ,使用 load-time 编织,如 Spring manual 中所述。或者,您也可以使用 compile-time 编织,例如通过 AspectJ Maven 插件。
如果你想坚持使用 Spring AOP,你需要使你的 bean proxy-aware,即间接 AOP-aware,这不太理想一种设计观点。我不推荐它,但它很容易实现:只需 self-inject 对组件的引用,例如
@Autowired MyComponent INSTANCE
然后总是使用该 bean 实例调用方法:INSTANCE.internalMethod()
。这样,所有调用都将通过代理并触发 Spring AOP 方面。