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


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工程師成神之路》系列文章作者。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM