如何在spring代理中实现自我调用(self-invocation)

问题

在spring中如果在方法上添加了诸如@Transactional@Cacheable@Scheduled@Async或是切面之类的注解,那么这个类就将由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(),这样最终还是直接调用了真实对象delegatemethodB方法,坑爹啊有木有……至于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.

这我还能说什么呢……

那么如果想要实现同一个对象中的自我调用可以通过哪些其他方式呢?

解决方案

在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...
   }
}

当然文档中还提到了另一种解决方案:

使用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有两个主要步骤:

  1. 通过JVM的-javaagent参数设置LTW的织入器类包,以代理JVM默认的类加载器
  2. LTW织入器需要一个 aop.xml文件,在该文件中指定切面类和需要进行切面织入的目标类

具体实现方式参考:
SpringBoot中使用LoadTimeWeaving技术实现AOP功能
使用AspectJ LTW(Load Time Weaving)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章