为什么Spring-proxy使用委托模式而不是继承+super?

Why Spring-proxy uses delegate pattern instead of inheritance+super?

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

例如参见this question

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

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()。可以理解。

但是为什么代理是这样实现的呢? 为什么它不只是扩展目标 class,覆盖所有 public 方法并调用 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实例),将调用重写的子方法(MyProxy.myMethod())。

所以,我的主要问题是为什么不以这种方式实施?

您关于 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:使用组合而不是继承。组成更加灵活。拥有一个可以根据需要向其注册方面的代理解决了继承问题,但也意味着委托:代理注册方面并在运行时以正确的顺序调用它们的方法 before/after 执行实际的真实对象的代码(或不会,如果 @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 示例 "logging aspect" 一样工作,我只能推测。您可以在开发人员邮件列表上询问他们或查看源代码。可能是因为 CGLIB 代理的行为应该类似于默认的 Java 动态代理,以便在两者之间无缝切换接口类型。也许是另一个原因。

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

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

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