一個Spring AOP的坑!很多人都犯過!
前幾天,我剛剛發(fā)布過一篇文章《自定義注解!絕對是程序員裝逼的利器!!》,介紹過如何使用Spring AOP + 自定義注解來提升代碼的優(yōu)雅性。
很多讀者看完之后表示用起來很爽,但是后臺也有人留言說自己配置了Spring的AOP之后,發(fā)現(xiàn)切面不生效。
其實,這個問題我在用的過程中也遇到過,而且還是同一個問題一天之內(nèi)遇到了兩次。
說明這個問題很容易被忽略,并且這個問題帶來的后果可能是極其嚴重的。那么,我們就來簡單回顧一下問題是怎么樣的。
問題重現(xiàn)
最初我定義了一個注解,希望可以方便統(tǒng)一的對一些數(shù)據(jù)庫操作做緩存。于是就有了以下代碼:
首先,定義一個注解:
- @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 {
- // 先查緩存,如果緩存中有值,直接返回。如果緩存中沒有,先執(zhí)行方法,再將返回值存儲到緩存中。
- }
- }
然后就可以使用該注解了,使用方法如下:
- @Component
- public class StrategyService extends BaseStrategyService {
- public PricingResponse getFactor(Map<String, String> pricingParams) {
- // 做一些參數(shù)校驗,以及異常捕獲相關的事情
- return this.loadFactor(tieredPricingParams);
- }
- @Override
- @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2)
- private PricingResponse loadFactor(Map<String, String> pricingParams) {
- //代碼執(zhí)行
- }
- }
以上,對loadFactor方法增加了切面,為了方便使用,我們還定義了一個getFactor方法,設置為public,方便外部調(diào)用。
但是,在調(diào)試過程中,我發(fā)現(xiàn)我們設置在loadFactor方法上面的切面并沒有成功,無法執(zhí)行切面類。
于是開始排查問題具體是什么。
問題排查
為了排查這個問題,首先是把所有的代碼檢查一遍,看看切面的代碼是不是有問題,有沒有可能有手誤打錯了字之類的。
但是發(fā)現(xiàn)都沒有。于是就想辦法找找問題。
接下來我把loadFactor的訪問權限從private改成public,發(fā)現(xiàn)沒有效果。
然后我嘗試著在方法外直接調(diào)用loadFactor而不是getFactor。
發(fā)現(xiàn)這樣做就可以成功的執(zhí)行到切面里面了。
發(fā)現(xiàn)這一現(xiàn)象的時候,我突然恍然大悟,直捶大腿。原來如此,原來如此,就應該是這樣的。
我突然就想到了問題的原因。其實原因挺簡單的,也是我之前了解到過的原理,但是在問題剛剛發(fā)生的時候我并沒有想到這里,而是通過debug,發(fā)現(xiàn)這個現(xiàn)象之后我才突然想到這個原理。
那么,就來說說為什么會發(fā)生這樣的問題。
代理的調(diào)用方式
我們發(fā)現(xiàn)上面的問題關鍵在于loadFactor方法被調(diào)用的方式不同。我們知道,方法的調(diào)用通常有以下幾種方式:
1、在類內(nèi)部,通過this進行自調(diào)用:
- 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、在類外部,通過該類的對象進行調(diào)用
- 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();
- }
- }
類關系及調(diào)用過程中如下圖:
如果是靜態(tài)方法,也可以通過類直接調(diào)用。
3、在類外部,通過該類的代理對象進行調(diào)用:
- 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();
- }
- }
類關系及調(diào)用過程中如下圖:
那么,Spring的AOP其實是第三種調(diào)用方式,就是通過代理對象調(diào)用,只有這種調(diào)用方式,才能夠在真正的對象的執(zhí)行前后,能夠讓代理對象也執(zhí)行相關代碼,才能起到切面的作用。
而對于使用this的方式調(diào)用,這種只是自調(diào)用,并不會使用代理對象進行調(diào)用,也就無法執(zhí)行切面類。
問題解決
那么,我們知道了,想要真正的執(zhí)行代理,那么就需要通過代理對象進行調(diào)用而不是使用this調(diào)用的方式。
那么,這個問題的解決辦法也就是想辦法通過代理對象來調(diào)用目標方法即可。
這種問題的解決網(wǎng)上有很多種辦法,這里介紹一個相對簡單的。其他的更多的辦法大家可以在網(wǎng)上找到一些案例。搜索關鍵詞"AOP 自調(diào)用"即可。
獲取代理對象進行調(diào)用
我們需要修改一下前面的StrategyService的代碼,修改成以下內(nèi)容:
- @Component
- public class StrategyService{
- public PricingResponse getFactor(Map<String, String> pricingParams) {
- // 做一些參數(shù)校驗,以及異常捕獲相關的事情
- // 這里不使用this.loadFactor而是使用AopContext.currentProxy()調(diào)用,目的是解決AOP代理不支持方法自調(diào)用的問題
- if (AopContext.currentProxy() instanceof StrategyService) {
- return ((StrategyService)AopContext.currentProxy()).loadFactor(tieredPricingParams);
- } else {
- // 部分實現(xiàn)沒有被代理過,則直接進行自調(diào)用即可
- return loadFactor(tieredPricingParams);
- }
- }
- @Override
- @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2)
- private PricingResponse loadFactor(Map<String, String> oricingParams) {
- //代碼執(zhí)行
- }
- }
即使用AopContext.currentProxy()獲取到代理對象,然后通過代理對象調(diào)用對應的方法。
還有個地方需要注意,以上方式還需要將Aspect的expose-proxy設置成true。如果是配置
文件修改:
- <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
如果是SpringBoot,則修改應用啟動入口類的注解:
- @EnableAspectJAutoProxy(exposeProxy = true)
- public class Application {
- }
總結
以上,我們分析并解決了一個Spring AOP不支持方法自調(diào)用的問題。
AOP失敗這個問題,其實還是很嚴重的,因為如果發(fā)生非預期的失效,那么直接問題就是沒有執(zhí)行切面方法,更嚴重的后果可能是諸如事務未生效、日志未打印、緩存未查詢等各種問題。
所以,還是建議大家看完此文之后,統(tǒng)查一下自己的代碼,是否存在方法自調(diào)用的情況。這種情況下,任何切面都是無法生效的!
【本文是51CTO專欄作者Hollis的原創(chuàng)文章,作者微信公眾號Hollis(ID:hollischuang)】