為什麼私有方法上的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,一個在努力讓自己變得更優秀的普通攻城獅。自己閲歷有限、學識淺薄,如有發現文章不妥之處,非常歡迎加我提出,我一定細心推敲加以修改。

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

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