如何在spring代理中实现自我调用(self-invocation)
Posted 小杨Vita
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在spring代理中实现自我调用(self-invocation)相关的知识,希望对你有一定的参考价值。
问题
在spring中如果在方法上添加了诸如@Transactional
、@Cacheable
或是切面之类的注解,那么这个类就将由spring生成其代理对象,对指定方法进行相关的包装。当该方法在其他对象中被调用时是可以正常触发代理方法的,然而在本类的方法中进行内部调用时却不会,最终调用的还是原始方法。
class Service
public void methodA()
methodB();
@Transactional
public void methodB()
// do something
也就是说通过methodA
调用methodB
不会走代理方法,这是为什么呢?
原因分析
我们知道spring生成代理对象常见的有两种方式,一种是基于接口的JDK动态代理,一种是基于子类的CGLIB风格代理,可通过proxyTargetClass
属性来控制。JDK文档中的一段话:
Users can control the type of proxy that gets created for FooService using the proxyTargetClass() attribute. The following enables CGLIB-style ‘subclass’ proxies as opposed to the default interface-based JDK proxy approach.
当为其配置了false时强制使用JDK动态代理,代理对象必须实现接口;当配置为true时强制使用CGLIB风格代理,代理方法不可使用final修饰;当没有配置该项时spring将自动选择有效的代理方式来实现。
JDK动态代理情况
JDK动态代理大家应该都比较熟悉了,这儿列出大致实现步骤,进而说明为什么无法触发代理方法。
public interface Subject
public void methodA();
public void methodB();
public class RealSubject implements Subject
public void methodA()
// do something
public void methodB()
// do something
public class InvocationHandlerImpl implements InvocationHandler
/**
* real proxied object
*/
private Object subject;
public InvocationHandlerImpl(Object subject)
this.subject = subject;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
// do something before invocation
Object returnValue = method.invoke(subject, args);
// do something after invocation
return returnValue;
// generate proxy step
Subject realSubject = new RealSubject();
InvocationHandler handler = new InvocationHandlerImpl(realSubject);
ClassLoader loader = realSubject.getClass().getClassLoader();
Class[] interfaces = realSubject.getClass().getInterfaces();
Subject subject = (Subject) Proxy.newProxyInstance(loader, interfaces, handler);
subject.methodA();
我们可以看到真实对象realSubject
其实是放入的handler
中,然后传给Proxy来生成代理对象的。我们用反编译工具看下最终生成的代理对象类:
public final class ProxySubject extends Proxy implements Subject
private static Method m1;
private static Method m2;
public ProxySubject(InvocationHandler paramInvocationHandler)
super(paramInvocationHandler);
public final void methodA()
try
this.h.invoke(this, m1, null);
catch (Error|RuntimeException localError)
throw localError;
public final void methodB()
try
this.h.invoke(this, m2, null);
catch (Error|RuntimeException localError)
throw localError;
static
m1 = Class.forName("xxx.Subject").getMethod("methodA", new Class[0]);
m2 = Class.forName("xxx.Subject").getMethod("methodB", new Class[0]);
为了方便理解上面删除了一些其他不影响的代码。我们可以看到针对代理方法methodA
的调用,本质调用的是h.invoke(this, m1, null)
,也就是handler
中的method.invoke(subject, args)
语句。这儿的subject
为传入的真实对象,因此如果在methodA
方法中调用methodB
,那就直接调用了内部的真实对象的methodB
方法。也就是说代理对象ProxySubject
所包装的方法仅适用于外部调用者直接访问,内部调用是无法再走代理过的方法的。
CGLIB风格代理情况
注意spring中使用的是CGLIB风格的代理,而不是正宗的CGLIB代理,我们先看下正宗的CGLIB代理实现方式。
public class Service
public void methodA()
methodB();
public void methodA()
// do something
public class MyMethodInterceptor implements MethodInterceptor
public Object intercept(Object obj, Method method, Object[] arg, MethodProxy proxy) throws Throwable
// do something before invocation
Object object = proxy.invokeSuper(obj, arg);
// do something after invocation
return object;
// generate proxy step
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Service.class);
enhancer.setCallback(new MyMethodInterceptor());
Service service = (Service)enhancer.create();
service.methodA()
CGLib使用字节码增强器Enhancer
生成代理类的字节码,对应的反编译内容为:
public class Service$$EnhancerByCGLIB$$123aabb extends Service implements Factory
private boolean CGLIB$BOUND;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static final Method CGLIB$g$0$Method;
private static final MethodProxy CGLIB$g$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$f$1$Method;
private static final MethodProxy CGLIB$f$1$Proxy;
static void CGLIB$STATICHOOK1()
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class localClass1 = Class.forName("Service$$EnhancerByCGLIB$$123aabb");
Class localClass2;
Method[] tmp60_57 = ReflectUtils.findMethods(new String[] "methodA", "()V", "methodB", "()V" , (localClass2 = Class.forName("xxx.Service")).getDeclaredMethods());
CGLIB$g$0$Method = tmp60_57[0];
CGLIB$g$0$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "methodA", "CGLIB$g$0");
CGLIB$f$1$Method = tmp60_57[1];
CGLIB$f$1$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "methodB", "CGLIB$f$1");
final void CGLIB$g$0()
super.methodA();
public final void methodA()
MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
if (tmp4_1 == null)
CGLIB$BIND_CALLBACKS(this);
tmp4_1 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 != null)
tmp4_1.intercept(this, CGLIB$g$0$Method, CGLIB$emptyArgs, CGLIB$g$0$Proxy);
else
super.methodA();
final void CGLIB$g$1()
super.methodB();
public final void methodB()
MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
if (tmp4_1 == null)
CGLIB$BIND_CALLBACKS(this);
tmp4_1 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 != null)
tmp4_1.intercept(this, CGLIB$g$0$Method, CGLIB$emptyArgs, CGLIB$g$0$Proxy);
else
super.methodB();
我们重点看下代理方法methodA
调用的是super.methodA()
,而该父类方法执行的是methodB()
,由于子类继承重写了methodB
方法,所以此时将调用子类(代理类)中的methodB
方法。也就是说使用CGLIB方式是支持代理中自我调用的,那为什么spring中不可以呢?原因刚才也提到过,因为spring使用的是CGLIB风格的代理,生成的代理类大致是如下形式:
class MyService extends Service
private final Service delegate;
@Override
public void methodA()
// do some proxy thing
delegate.methodA();
// do some proxy thing
@Override
public void methodB()
// do some proxy thing
delegate.methodB();
// do some proxy thing
有没有发现这种方式灰常像动态代理的实现!通过使用delegate.methodA()
代替super.methodA()
,这样最终还是直接调用了真实对象delegate
的methodB
方法,坑爹啊有木有……至于spring为什么要这么做呢?我在StackOverflow上找到了几个猜测:
The behavior of the Cglib-proxies has nothing to do with the way of how cglib works but with how cglib is used by Spring. Cglib is capable of either delegating or subclassing a call. Have a look at Spring’s DynamicAdvisedInterceptor which implements this delegation. Using the MethodProxy, it could instead perform a super method call.
Spring defines delegation rather than subclassing in order to minimize the difference between using Cglib or Java proxies.
Maybe the reason was that CGLIB proxies should behave similarly to the default Java dynamic proxies so as to make switching between the two for interface types seamless.
为了能在java动态代理和cglib代理无缝切换,减小两者的差异,因此使用这种实现方式……
那么如果想要实现同一个对象中的自我调用,可以通过哪些方式呢?
解决方案
在spring官方文档中给出的建议是:
最好进行代码重构,以便不会发生自我调用,虽然需要你做一些额外工作,但这是最佳的侵入性最低的方式。
也就是说把方法分到不同的类中,当然他们也给出了其他的实现方式。
exposeProxy暴露代理方式
接下来介绍的方法我务必要谨慎地指出,它实在令人可怕。你可以使用该方法彻底将你的类中的逻辑绑定到Spring AOP中。
通过配置exposeProxy
属性来获取代理类,进而在业务逻辑代码中获取代理类。开启方式如@EnableAspectJAutoProxy(exposeProxy = true)
,该属性的介绍为:
Indicate that the proxy should be exposed by the AOP framework as a ThreadLocal for retrieval via the
org.springframework.aop.framework.AopContext
class. Off by default, i.e. no guarantees that AopContext access will work.
也就是说开启后,在线程执行时spring会将当前对象的代理对象放入ThreadLocal
中,通过AopContext
提供的方法你可以在任何时候都能获取到代理对象。使用方式:
public class SimpleService implements Service
public void methodA()
// this works, but... gah!
((Service) AopContext.currentProxy()).methodB();
public void methodB()
// some logic...
exposeProxy开启后如果依然无法获取到currentProxy可以参考这篇文章。
当然文档中还提到了另一种解决方案:
使用AspectJ则不会有自我调用的问题,因为它并不是一种基于代理的AOP框架。
AspectJ
在开启事务@EnableTransactionManagement
、缓存@EnableCaching
或是异步请求@EnableAsync
时,Spring会提供了两种实现模式:proxy 和 AspectJ。
AspectJ的开启方式如@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
。AspectJ有两种织入方式:CTW(Compile Time Weaving,编译期织入) 和 LTW(Load Time Weaving,类加载期织入)。
如果使用CTW,则需要编写 aspect 文件,然后使用 ajc 编译器结合 aspect 文件对源代码进行编译,通常很少使用。
而LTW类加载期织入是通过字节码编辑技术在类加载期将切面织入目标类中。其核心思想是在目标类的class文件被JVM加载前,通过自定义类加载器或者类文件转换器将横切逻辑织入到目标类的class文件中,然后将修改后class文件交给JVM加载。
使用AspectJ LTW有两个主要步骤:
- 通过JVM的
-javaagent
参数设置LTW的织入器类包,以代理JVM默认的类加载器 - LTW织入器需要一个 aop.xml文件,在该文件中指定切面类和需要进行切面织入的目标类
具体实现方式参考:
SpringBoot中使用LoadTimeWeaving技术实现AOP功能
使用AspectJ LTW(Load Time Weaving)
以上是关于如何在spring代理中实现自我调用(self-invocation)的主要内容,如果未能解决你的问题,请参考以下文章