為什麼私有方法上的Spring Cache註解不生效?
本文首發於公眾號【看點程式碼再上班】,歡迎圍觀,第一時間獲取最新文章。
目錄
大家好,我是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