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

Rus*_*nko 6 java proxy spring spring-aop

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

看到这个问题例如,

我认为这是因为 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我对吗?

有了这个代码:

public void myOtherMethod() {
    this.myMethod();
}
Run Code Online (Sandbox Code Playgroud)

this.myMethod()将绕过代理(所以全部@Transactional@Cacheable魔法),因为它只是内部委托的调用...所以我们应该在内部注入一个MyClassbean(实际上是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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

kri*_*aex 3

您关于 Spring AOP 使用委托作为其代理的假设是正确的。这也有记录

使用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();
  }
}
Run Code Online (Sandbox Code Playgroud)

控制台日志:

Before proxy.invokeSuper x
x
Before proxy.invokeSuper y
y
After proxy.invokeSuper y
After proxy.invokeSuper x
Run Code Online (Sandbox Code Playgroud)

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

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

  • 选项 2:使用组合而不是继承。构图更加灵活。拥有一个可以根据需要向其注册方面的代理解决了继承问题,但也意味着委托:代理在运行时注册方面并在执行实际对象的代码之前/之后以正确的顺序调用其方法(或者不执行,如果@Around从来没有接到建议proceed())。请参阅 Spring 手册中有关手动将方面注册到代理的示例:

// 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();
Run Code Online (Sandbox Code Playgroud)

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

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