为什么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 中隐藏一些功能(方法)怎么办
- 组合优于继承(许多开发人员推荐)
众所周知,在没有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 中隐藏一些功能(方法)怎么办
- 组合优于继承(许多开发人员推荐)