【问题标题】:Why does self-invocation not work for Spring proxies (e.g. with AOP)?为什么自调用不适用于 Spring 代理(例如使用 AOP)?
【发布时间】:2019-10-30 01:06:53
【问题描述】:

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

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 proxy spring-aop


    【解决方案1】:

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


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

    您对它如何工作的(错误)假设

    您对动态代理的工作方式有误解,因为它们不像您的示例代码那样工作。让我将对象 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,这不足为奇。由于多态性,您的“代理”(它不是真正的代理,而是静态编译的子类)的自我调用工作。这由 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();
      }
    }
    

    看到区别了吗?因此控制台日志更改为:

    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 的其他部分使用动态代理,甚至是使用 JDK 或 CGLIB 代理的非 Spring 应用程序时看到的行为。

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

    解决方案

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

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

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

    【讨论】:

    • 嗨。我当然读过。我不明白,为什么要引入这种自我调用限制?有什么好处吗?如果我代理一个方法,直觉上我希望这段代码会被执行。即使它是一个特性,为什么说 AspectJ 应该用于自调用,如果可以通过子类获得相同的结果?
    • 带有委托的示例看起来像装饰器,但意图不同。代理和装饰器都是结构模式。而且它们都在包装另一个对象。
    • 代理和装饰器都有一个委托。原始代理模式正是我们在 JDK 动态代理中看到的,即委托和代理都实现相同的接口。装饰器模式也可以做到这一点,但有一个基础装饰器和任意数量的扩展基础装饰器的具体装饰器,即结构与代理不同。我的示例代码是 CGLIB 代理所做的,即扩展一个具体类。代理和装饰器都可以添加功能。这不会使 Spring 代理装饰器,但这是相当学术性的,并没有多大帮助。
    • 作为对@krund 的较晚回复:我的回答已经解释了:动态代理在运行时(!)而不是在编译时扩展原始对象,就像您在 IDE 中编写的子类然后一起编译它与基类。您总是只会在运行时创建一个子类实例,但 Spring 的方式是创建原始对象,然后创建一个附加代理。因此,自调用(通过this 调用方法)不能按您期望的方式工作,使用代理模式是不可能的。但正如我所说,AspectJ 不使用代理,所以如果您需要自调用,则可以使用它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多