【问题标题】:Why Spring-proxy uses delegate pattern instead of inheritance+super?为什么 Spring-proxy 使用委托模式而不是继承+超级?
【发布时间】:2018-01-22 16:07:46
【问题描述】:

众所周知,在没有AspectJ的情况下,bean的方法的自调用在Spring中是行不通的。

例如,请参阅this question

我认为这是因为 Spring 创建的代理使用 delagate 模式调用目标对象的方法。像这样:

class MyClass {

    @Autowired
    private MyClass self; // actually a MyProxy instance

    @Transactional // or any other proxy magic
    public void myMethod() {}

    public void myOtherMethod() {
        this.myMethod(); // or self.myMethod() to avoid self-invokation problem
    }
}

class MyProxy extends MyClass { // or implements MyInterface if proxyMode is not TARGET_CLASS and MyClass also implements MyInterface

    private final MyClass delegate;

    @Override
    public void myMethod() {
        // some proxy magic: caching, transaction management etc
        delegate.myMethod();
        // some proxy magic: caching, transaction management etc
    }

    @Override
    public void myOtherMethod() {
        delegate.myOtherMethod();
    }
}

我说的对吗?

使用此代码:

public void myOtherMethod() {
    this.myMethod();
}

this.myMethod() 将绕过代理(所以所有@Transactional@Cacheable 魔术)因为它只是内部委托的调用......所以我们应该注入一个MyClass bean(实际上是MyProxy 实例) 在MyClass 中调用self.myMethod()。这是可以理解的。

但是为什么代理是这样实现的呢? 为什么它不只是扩展目标类,覆盖所有公共方法并调用super 而不是delegate? 像这样:

class MyProxy extends MyClass {
    // private final MyClass delegate; // no delegate
    @Override
    public void myMethod() {
        // some proxy magic: caching, transaction management etc
        super.myMethod();
        // some proxy magic: caching, transaction management etc
    }
    @Override
    public void myOtherMethod() {
        super.myOtherMethod();
    }
}

它应该解决自调用问题,其中this.myMethod()绕过代理,因为在这种情况下this.myMethod(),从MyClass.myOtherMethod()调用(我们记得MyClass bean实际上是MyProxy实例),将调用覆盖的child的方法(MyProxy.myMethod())。

那么,我的主要问题是为什么不这样实现?

【问题讨论】:

  • 我不是在问为什么 Java 动态代理 是以它们的方式实现的。我在问为什么 Spring AOP 代理 以它们的方式实现。据我所知,他们在代理模式 TARGET_CLASS 中使用 CGLIB 或 Javassist,而不是 Java 动态代理。是的,我的问题确实如此。多年以来,每次使用自注入我都会哭(这是容易出错的丑陋模式,LTW 也有自己的问题)。所以我真的想知道为什么 Spring 开发人员不使用super 来实现它(是有原因还是只是“它发生了”)。而且我敢肯定,许多 DEV 多年来一次又一次地问自己同样的问题
  • 没有。默认情况下,Spring 在代理接口时使用 Java 动态代理。只有当你需要代理不实现接口的类时,你才需要CGLIB。或者,您可以将其切换为也将 CGLIB 用于接口。无论如何,两者都非常相似,因为它们都是动态代理并且不支持通过this 进行内部调用。顺便说一句,我怎么知道你使用的是哪种模式?您没有显示任何配置。我只能从你的代码中猜测,也许。 BTW2,你收到的近距离投票不是我的,我只是提到它。欢迎您提供我关于 AspectJ 的提示(那里没有代理)。
  • 仅供参考,代理模式 TARGET_CLASS (CGLIB) 是 Spring Boot 2.0 中的默认模式。此外,有问题的代码故意不使用任何接口,因此无论如何它将在任何 Spring 版本中使用 CGLIB。 PS。冷静点,我不担心近距离投票。我知道这个问题并不完全适合 SO。我有点希望 Juergen Hoeller(也许是世界上唯一一个能给出有意义答案的人)真的回答它。 :-) 这只是一次尝试,但也许..
  • 确实给了你一个有意义的答案:如果你不喜欢 Spring AOP 的工作方式,请使用 AspectJ。没有动态代理(CGLIB 和 Java 都没有),不用担心。关于为什么很多年前有人可能以这种或那种方式做出设计决定的哲学讨论或猜测在 SO 上真的不合适。我的看法是:Java 动态代理存在于 JRE 中,CGLIB 也存在,所以 Springsource 只是使用它们。 P.S.:你没有标记问题spring-boot,那么我怎么知道它的默认值的相关性呢?我不使用Spring,我只是碰巧知道一些。
  • 是的,你确实给出了答案,但在一个完全不同的问题上(我根本没有问)。 :-) 问题是为什么CGLIB 代理使用Delegation pattern 而不是Inheritance 实现(这应该解决this 问题)。这不是哲学讨论!这是针对弹簧专家的真正精确的问题。而且在 cmets 中的讨论也对 SO 不利,所以让我们关闭它。如果你真的想讨论 - 我们去聊天吧。谢谢。

标签: java spring proxy spring-aop


【解决方案1】:

您认为 Spring AOP 为其代理使用委托的假设是正确的。这也是documented

使用CGLIB,理论上你可以使用proxy.invokeSuper()来达到你想要的效果,即通过代理的方法拦截器实现的切面注册自调用(我这里使用的是Spring的嵌入式CGLIB版本,因此包名):

package spring.aop;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

class SampleClass {
  public void x() {
    System.out.println("x");
    y();
  }

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

  public static void main(String[] args) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(SampleClass.class);
    enhancer.setCallback(new MethodInterceptor() {
      @Override
      public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
        throws Throwable {
        if(method.getDeclaringClass() == Object.class)
          return proxy.invokeSuper(obj, args);
        System.out.println("Before proxy.invokeSuper " + method.getName());
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After proxy.invokeSuper " + method.getName());
        return result;
      }
    });
    SampleClass proxy = (SampleClass) enhancer.create();
    proxy.x();
  }
}

控制台日志:

Before proxy.invokeSuper x
x
Before proxy.invokeSuper y
y
After proxy.invokeSuper y
After proxy.invokeSuper x

这正是您想要的。但是,当您有几个方面时,问题就开始了:事务、日志记录等等。你如何确保它们一起工作?

  • 选项 1:每个方面都有自己的代理。除非您根据方面优先级将代理相互嵌套,否则这显然不起作用。但是将它们相互嵌套意味着继承,即一个代理必须从另一个代理从外向内继承。尝试代理一个 CGLIB 代理,它不起作用,你得到异常。此外,CGLIB 代理非常昂贵并且使用 perm-gen 内存,请参阅 this CGLIB primer 中的描述。

  • 选项 2:使用组合而不是继承。组合更灵活。拥有一个可以根据需要向其注册方面的代理解决了继承问题,但也意味着委托:代理在执行实际真实对象的代码之前/之后以正确的顺序在运行时注册方面并调用它们的方法(或不执行,如果@Around 建议永远不会调用proceed())。请参阅 Spring 手册中有关 manually registering aspects to a proxy 的此示例:

// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();

至于 为什么 Spring 开发人员选择了这种方法,以及是否可以使用单一代理方法,但仍确保自调用像我的小 CGLIB 示例“日志记录”中一样工作方面”上面,我只能推测。您可以在开发人员邮件列表中询问他们或查看源代码。原因可能是 CGLIB 代理的行为应该类似于默认的 Java 动态代理,以便在两种接口类型之间无缝切换。也许是另一个原因。

我并不是要对我的 cmets 粗鲁,只是直截了当,因为您的问题确实不适合 StackOverflow,因为它不是一个可以找到解决方案的技术问题。这是一个历史设计问题,而且本质上是哲学问题,因为使用 AspectJ 可以解决实际问题之下的技术问题(自我调用)。但也许您仍想深入研究 Spring 源代码,将 Spring AOP 实现从委托更改为 proxy.invokeSuper() 并提交拉取请求。不过,我不确定这样的重大更改是否会被接受。

【讨论】:

  • 谢谢!这是一个非常有用的答案,我会接受它,因为它真的回答了这个问题!尤其是这部分“尝试代理 CGLIB 代理,它不起作用,您会遇到异常。此外,CGLIB 代理非常昂贵并且使用 perm-gen 内存。”当然代理应该是可嵌套的,否则它将不符合通用用例的条件。因此,仅此事实就足以理解为什么“调用超级”方法不起作用。所以,谢谢!
  • 关于问题对 StackOverflow 的适用性。这当然是可以讨论的,也许你是对的,但是 SO 充满了这样的问题:例如stackoverflow.com/questions/20129762/…。问题和回答的赞成票数量表明人们需要它。一方面,这个问题可以被认为是哲学问题,但另一方面 - 可以给出一个非常具体的答案,这有助于理解为什么考虑的方法可能不好(以及为什么以其他方式实施)。
  • 别人做某事并不一定就意味着它是好的。请注意,我的回答在某些方面是投机性的,我对做出有根据的猜测而不是确定地知道没有很好的感觉。但我确实同意我喜欢帮助您并回答您的问题的感觉,所以感谢您的积极反馈。在创建示例代码时,我还学到了一些关于 CGLIB 的东西——我以前从未直接使用过——所以对我来说这也是值得的。但这也是一个相当自私的动机,因此不能证明这些关于 SO 的问题是合理的。 ;-)
【解决方案2】:

此外,在以下情况下您将无法使用Inheritance + super

  • 如果 RealSubject 是最终的,那么代理将无法扩展它怎么办
  • 如果 Proxy 需要扩展 RealSubject 以外的东西,该怎么办
  • 如果您需要在 RealSubject 中隐藏某些功能(方法)怎么办
  • 优先组合而不是继承(许多开发人员推荐)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-10-24
    • 2019-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-07
    • 1970-01-01
    相关资源
    最近更新 更多