深坑啊!同一個Spring AOP的坑,我一天踩了兩次!

HollisChuang發表於2020-11-17

GitHub 18k Star 的Java工程師成神之路,不來了解一下嗎!

GitHub 18k Star 的Java工程師成神之路,真的不來了解一下嗎!

GitHub 18k Star 的Java工程師成神之路,真的真的不來了解一下嗎!

前幾天,我剛剛釋出過一篇文章《自定義註解!絕對是程式設計師裝逼的利器!!》,介紹過如何使用Spring AOP + 自定義註解來提升程式碼的優雅性。

很多讀者看完之後表示用起來很爽,但是後臺也有人留言說自己配置了Spring的AOP之後,發現切面不生效。

其實,這個問題我在用的過程中也遇到過,而且還是同一個問題一天之內遇到了兩次。

說明這個問題很容易被忽略,並且這個問題帶來的後果可能是極其嚴重的。那麼,我們就來簡單回顧一下問題是怎麼樣的。

問題重現

最初我定義了一個註解,希望可以方便統一的對一些資料庫操作做快取。於是就有了以下程式碼:

首先,定義一個註解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {

    /**
     * 策略名稱,需要保證唯一
     *
     * @return
     */
    public String keyName();

    /**
     * 超時時長,單位:秒
     *
     * @return
     */
    public int expireTime();

}

然後自定義一個切面,對所有使用了該註解的方法進行切面處理:

@Aspect
@Component
public class StrategyCacheAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);
    @Around("@annotation(com.hollis.cache.StrategyCache)")
    public Object cache(ProceedingJoinPoint pjp) throws Throwable {
        // 先查快取,如果快取中有值,直接返回。如果快取中沒有,先執行方法,再將返回值儲存到快取中。
    }
}

然後就可以使用該註解了,使用方法如下:

@Component
public class StrategyService extends BaseStrategyService  {

    public PricingResponse getFactor(Map<String, String> pricingParams) {
        // 做一些引數校驗,以及異常捕獲相關的事情
        return this.loadFactor(tieredPricingParams);
    }

    @Override
    @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2)
    private PricingResponse loadFactor(Map<String, String> pricingParams) {
        //程式碼執行
    }
}

以上,對loadFactor方法增加了切面,為了方便使用,我們還定義了一個getFactor方法,設定為public,方便外部呼叫。

但是,在除錯過程中,我發現我們設定在loadFactor方法上面的切面並沒有成功,無法執行切面類。

於是開始排查問題具體是什麼。

問題排查

為了排查這個問題,首先是把所有的程式碼檢查一遍,看看切面的程式碼是不是有問題,有沒有可能有手誤打錯了字之類的。

但是發現都沒有。於是就想辦法找找問題。

接下來我把loadFactor的訪問許可權從private改成public,發現沒有效果。

然後我嘗試著在方法外直接呼叫loadFactor而不是getFactor。

發現這樣做就可以成功的執行到切面裡面了。

發現這一現象的時候,我突然恍然大悟,直捶大腿。原來如此,原來如此,就應該是這樣的。

我突然就想到了問題的原因。其實原因挺簡單的,也是我之前瞭解到過的原理,但是在問題剛剛發生的時候我並沒有想到這裡,而是通過debug,發現這個現象之後我才突然想到這個原理。

那麼,就來說說為什麼會發生這樣的問題。

代理的呼叫方式

我們發現上面的問題關鍵在於loadFactor方法被呼叫的方式不同。我們知道,方法的呼叫通常有以下幾種方式:

1、在類內部,通過this進行自呼叫:

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

2、在類外部,通過該類的物件進行呼叫

public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

類關係及呼叫過程中如下圖:

-w485

如果是靜態方法,也可以通過類直接呼叫。

3、在類外部,通過該類的代理物件進行呼叫:

public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

類關係及呼叫過程中如下圖:

-w613

那麼,Spring的AOP其實是第三種呼叫方式,就是通過代理物件呼叫,只有這種呼叫方式,才能夠在真正的物件的執行前後,能夠讓代理物件也執行相關程式碼,才能起到切面的作用。

而對於使用this的方式呼叫,這種只是自呼叫,並不會使用代理物件進行呼叫,也就無法執行切面類。

問題解決

那麼,我們知道了,想要真正的執行代理,那麼就需要通過代理物件進行呼叫而不是使用this呼叫的方式。

那麼,這個問題的解決辦法也就是想辦法通過代理物件來呼叫目標方法即可。

這種問題的解決網上有很多種辦法,這裡介紹一個相對簡單的。其他的更多的辦法大家可以在網上找到一些案例。搜尋關鍵詞"AOP 自呼叫"即可。

獲取代理物件進行呼叫

我們需要修改一下前面的StrategyService的程式碼,修改成以下內容:

@Component
public class StrategyService{

    public PricingResponse getFactor(Map<String, String> pricingParams) {
        // 做一些引數校驗,以及異常捕獲相關的事情
        // 這裡不使用this.loadFactor而是使用AopContext.currentProxy()呼叫,目的是解決AOP代理不支援方法自呼叫的問題
        if (AopContext.currentProxy() instanceof StrategyService) {
            return ((StrategyService)AopContext.currentProxy()).loadFactor(tieredPricingParams);
        } else {
            // 部分實現沒有被代理過,則直接進行自呼叫即可
            return loadFactor(tieredPricingParams);
        }
    }

    @Override
    @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2)
    private PricingResponse loadFactor(Map<String, String> oricingParams) {
        //程式碼執行
    }
}

即使用AopContext.currentProxy()獲取到代理物件,然後通過代理物件呼叫對應的方法。

還有個地方需要注意,以上方式還需要將Aspect的expose-proxy設定成true。如果是配置檔案修改:

 <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>

如果是SpringBoot,則修改應用啟動入口類的註解:

@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {

}

總結

以上,我們分析並解決了一個Spring AOP不支援方法自呼叫的問題。

AOP失敗這個問題,其實還是很嚴重的,因為如果發生非預期的失效,那麼直接問題就是沒有執行切面方法,更嚴重的後果可能是諸如事務未生效、日誌未列印、快取未查詢等各種問題。

所以,還是建議大家看完此文之後,統查一下自己的程式碼,是否存在方法自呼叫的情況。這種情況下,任何切面都是無法生效的!

關於作者:Hollis,一個對Coding有著獨特追求的人,阿里巴巴技術專家,《程式設計師的三門課》聯合作者,《Java工程師成神之路》系列文章作者。

相關文章