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

时间:2018-01-22 16:07:46

标签: java spring proxy spring-aop

众所周知,没有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实例) ,将调用重写子方法(MyProxy.myMethod())。

所以,我的主要问题是为什么没有这样实现?

2 个答案:

答案 0 :(得分:2)

您认为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动态代理类似,以便在两者之间切换接口类型无缝。也许原因是另一个原因。

我并不是故意在评论中粗鲁,只是直截了当,因为你的问题实际上不适合StackOverflow,因为它不是一个技术问题,有人可以找到解决方案。这是一个历史设计问题,而且本质上是哲学问题,因为使用AspectJ可以在实际问题下解决您的技术问题(自我调用)。但是,您可能仍想深入了解Spring源代码,将Spring AOP实现从委托更改为proxy.invokeSuper()并提交pull请求。不过,我不确定会接受这样一个突破性的变化。

答案 1 :(得分:0)

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

  • 如果RealSubject是最终版本,那么代理将无法扩展它
  • 如果代理需要扩展RealSubject以外的其他内容,该怎么办
  • 如果您需要在RealSubject内部隐藏一些功能(方法),该怎么办
  • 优先考虑继承而不是继承(许多开发人员建议)