為什麼私有方法上的Spring Cache註解不生效?

語言: CN / TW / HK

本文首發於公眾號【看點程式碼再上班】,歡迎圍觀,第一時間獲取最新文章。

目錄

Spring Cache "錯用"

錯在哪裡?

從Spring Cache原理解釋為什麼私有方法不能加快取

從Spring AOP原理解釋為什麼私有方法上不能加快取

類內部方法呼叫不支援加快取

結語


​大家好,我是tin,這是我的第12篇原創文章

Spring Cache "錯用"

背景是這樣的, 一個同事開發的一個功能模組程式碼,大概是查詢一個下游的內容介面,查詢到資料並轉發給端側介面。這個功能模組流量非常大,下游的內容介面的內容資料量也有限(內容id數有限,量級在幾個w),同事也意識到了需要在自己服務內對下游內容介面加本地快取。做法和下面圖一模一樣:

這個程式碼釋出到線上,當端側訪問入口開放時,我們後端服務就開始瘋狂告警,都是內容介面不負重壓、響應超時的告警。

然後我們先去看的呼叫鏈,發現內容介面QPS已經飆升到10W!

為什麼?明明已經加了介面快取,按我們加快取的預期,很多請求應該打到快取上,而不應該再查下游內容接口才對啊?或許很多人都這麼認為,但錯了就是錯了。

像上圖的錯誤是很“低階”的,如果都這麼使用,對於稍微不健壯的下游系統,將是災難,如果真到那樣子,今年的績效也就好不到哪了。

錯在哪裡?

上述程式碼對spring cache使用,總結來說有下面的錯誤:

  • 1、在私有方法上加快取
  • 2、類內部方法呼叫加快取

這些問題都是比較致命的,我們很多使用快取的朋友根本不知道也不清楚不能這麼使用。下面我把問題一一地分析透了。

從Spring Cache原理解釋為什麼私有方法不能加快取

Spring Cache通過註解,並藉助Spring AOP實現快取。開啟原始碼包,定位到我們的@Cacheable註解位置:

cache的實現都在context的org.springframework.cache包下。我的spring版本是5.3.14,其他版本也基本一樣。

matches方法判斷類或者方法中有沒有cache相關的快取註解,這個是怎麼判斷的呢?我們一路跟進去,cas.getCacheOperations(method, targetClass)),到了AbstractFallbackCacheOperationSource類的getCacheOperations方法:

attributeCache是一個ConcurrentHashmap,只是把computeCacheOperations(method, targetClass)計算得到的結果快取一下,下次再進來就不用消耗cpu重新計算獲取。

computeCacheOperations方法中,真正解析方法上cache註解的地方在findCacheOperations方法:

一路跟進去,當我們看到SpringCacheAnnotationParser類的parseCacheAnnotations方法時,就到看了spring把cache的相關注解進行解析,並把註解包裝為CacheOperation類

看到這裡就明白了吧,spring把類和方法上的cache註解包裝起來並放到一個集合Collection中,在aop切面上通過判斷是否有CacheOperation作為切入點。 然後,到這裡我想重點說一下的是,spring cache已經做了判斷,不支援非public方法上的快取註解,邏輯在哪裡呢?細心的可以發現:

很顯然的不支援非public方法,即使是protected方法都不行,更不用說private了!

這裡再多說一下,看完cache的切入點程式碼,我們也很容易找到方法攔截器:

我們從execute方法一路跟進去,可以看到最後是在CacheAspectSupport類的execute方法實現對接快取的讀取或者更新。

如果我們引入了三方的cache,比如Caffeine,那麼,底層就是使用Caffeine.Cache來儲存的。

如果想知道如何使用三方快取工具,可以看我另外一篇文章:

《人人都說好的Spring Cache!用起來!【文末送書】》 結論:

spring cache通過

AbstractFallbackCacheOperation#computeCacheOperations方法顯式地不支援非public方法的註解快取。

從Spring AOP原理解釋為什麼私有方法上不能加快取

上面講到了spring cache自己做了一層限制,不支援非public方法加快取註解,那麼,spring cache為什麼這麼做?如果只是看spring cache原始碼的邏輯,不加這個限制,不也一樣是可以“走得通”麼?  

要解釋這個問題,那就要從Spring AOP原理說起了。

我們先把BookService的快取註解位置調整一下,讓方法能夠正常走快取邏輯:

啟動我們的spring容器,斷點到我們業務程式碼bookService.findByBookNameWithSpringCache(bookName)的地方:

看到了沒,BookService引用是一個代理類,這也側面說明spring cache借用了aop的能力。

問題來了,為什麼是cglib代理?

在我們的常識裡面,spring aop預設都是採用java的動態代理,其次才會使用cglib代理。從spring官網文件也可以證實:

em……我沒有使用介面,所以採用了cglib代理。這個解釋也只算對一半吧。(因為即使你換成了介面實現,最後還是沒能如你所願,還是cglib代理,感興趣的朋友自行嘗試)

還是說下為什麼我們執行一直都是cglib代理的原因吧。

這是spring boot搞的鬼,在我們的啟動類上有一個@SpringBootApplication註解,這是一套組合註解,我們順著這個註解內部的定義,找到@EnableAutoConfiguration,再找到@Import(AutoConfigurationImportSelector.class)

AutoConfigurationImportSelector類的process方法:

這裡面有很多自動裝配資訊,根據AopAutoConfiguration(這個類定義在spring-boot下而不是spring-context下)的定義:

​ AopAutoConfiguration類的主要任務是根據配置引數使用註解@EnableAspectJAutoProxy,註釋也有說明:

該類啟用的條件是:配置引數spring.aop.auto值不為false,我們的spring-configuration-metadata.json中有配置:

AopAutoConfiguration又包含了如下兩個內建配置類,分別對應配置引數spring.aop.proxy-target-class=true/false兩種情況 :

當spring.aop.proxy-target-class預設配置時預設也是true,我們的spring-boot裡面預設就是true,所以預設使用aop的cglib代理。

到這裡,我們就基本知道spring-cache中使用到的aop為何一直使用cglib代理的原因。

說完cglib,終於可以回到主題上了,“為何不能在私有方法上使用cache註解”,如果從aop的角度去分析,那麼答案就是:因為cglib

cglib實現動態代理,其底層採用了ASM位元組碼生成框架,直接對需要代理的類的位元組碼進行操作,生成這個類的一個子類,並重寫了類的所有可以重寫的方法。

由於cglib的代理類使用的是繼承,這也就意味著cglib不能代理final類,同時也不能對private方法進行代理!子類無法重寫private方法啊!

至於cglib是如何生成代理類的,這裡不展開說了,後面有機會再專門出一個文章寫一寫,我們到這裡只要知道,spring cache的實現使用了aop功能,而aop不支援對private私有方法的攔截,所以也就不支援私有方法上的spring cache註解。

類內部方法呼叫不支援加快取

通過上面的分析,spring cahe的快取功能是因為使用了aop,如此可知我們的類是被cglib重新增強代理過的類。

如果是類內部方法呼叫,為什麼就不能生效?

這個問題很簡單,我們在內部呼叫方法的地方打個斷點,一看便知:

是吧,沒有走代理,怎麼能夠使用得上快取功能呢?

結語

我是tin,一個在努力讓自己變得更優秀的普通攻城獅。自己閱歷有限、學識淺薄,如有發現文章不妥之處,非常歡迎加我提出,我一定細心推敲加以修改。

堅持創作不容易,你的正反饋是我堅持輸出的最強大動力,謝謝!

附上原文連結:
http://mp.weixin.qq.com/s?__biz=MzIwMDEzOTYzNA==&mid=2247485083&idx=1&sn=56583bedc66bc515388eaa0a6bcba236&chksm=9680f086a1f7799047869996aa7d54b2b0a618cc125b1d4ca6b73a44b8bd8975c60ad4f55ee6&token=605467227&lang=zh_CN#rd