如何在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)

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