1. 程式人生 > >曹工說Spring Boot原始碼(20)-- 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操作日誌

曹工說Spring Boot原始碼(20)-- 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操作日誌

# 寫在前面的話 相關背景及資源: [曹工說Spring Boot原始碼(1)-- Bean Definition到底是什麼,附spring思維導圖分享](https://www.cnblogs.com/grey-wolf/p/12044199.html) [曹工說Spring Boot原始碼(2)-- Bean Definition到底是什麼,咱們對著介面,逐個方法講解](https://www.cnblogs.com/grey-wolf/p/12051957.html ) [曹工說Spring Boot原始碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下](https://www.cnblogs.com/grey-wolf/p/12070377.html) [曹工說Spring Boot原始碼(4)-- 我是怎麼自定義ApplicationContext,從json檔案讀取bean definition的?](https://www.cnblogs.com/grey-wolf/p/12078673.html) [曹工說Spring Boot原始碼(5)-- 怎麼從properties檔案讀取bean](https://www.cnblogs.com/grey-wolf/p/12093929.html) [曹工說Spring Boot原始碼(6)-- Spring怎麼從xml檔案裡解析bean的](https://www.cnblogs.com/grey-wolf/p/12114604.html ) [曹工說Spring Boot原始碼(7)-- Spring解析xml檔案,到底從中得到了什麼(上)](https://www.cnblogs.com/grey-wolf/p/12151809.html) [曹工說Spring Boot原始碼(8)-- Spring解析xml檔案,到底從中得到了什麼(util名稱空間)](https://www.cnblogs.com/grey-wolf/p/12158935.html) [曹工說Spring Boot原始碼(9)-- Spring解析xml檔案,到底從中得到了什麼(context名稱空間上)](https://www.cnblogs.com/grey-wolf/p/12189842.html) [曹工說Spring Boot原始碼(10)-- Spring解析xml檔案,到底從中得到了什麼(context:annotation-config 解析)](https://www.cnblogs.com/grey-wolf/p/12199334.html) [曹工說Spring Boot原始碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)](https://www.cnblogs.com/grey-wolf/p/12203743.html) [曹工說Spring Boot原始碼(12)-- Spring解析xml檔案,到底從中得到了什麼(context:component-scan完整解析)](https://www.cnblogs.com/grey-wolf/p/12214408.html) [曹工說Spring Boot原始碼(13)-- AspectJ的執行時織入(Load-Time-Weaving),基本內容是講清楚了(附原始碼)](https://www.cnblogs.com/grey-wolf/p/12228958.html) [曹工說Spring Boot原始碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation整合](https://www.cnblogs.com/grey-wolf/p/12283544.html) [曹工說Spring Boot原始碼(15)-- Spring從xml檔案裡到底得到了什麼(context:load-time-weaver 完整解析)](https://www.cnblogs.com/grey-wolf/p/12288391.html) [曹工說Spring Boot原始碼(16)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【上】)](https://www.cnblogs.com/grey-wolf/p/12314954.html) [曹工說Spring Boot原始碼(17)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【中】)](https://www.cnblogs.com/grey-wolf/p/12317612.html) [曹工說Spring Boot原始碼(18)-- Spring AOP原始碼分析三部曲,終於快講完了 (aop:config完整解析【下】)](https://www.cnblogs.com/grey-wolf/p/12322587.html) [曹工說Spring Boot原始碼(19)-- Spring 帶給我們的工具利器,建立代理不用愁(ProxyFactory)](https://www.cnblogs.com/grey-wolf/p/12359963.html) [工程程式碼地址](https://gitee.com/ckl111/spring-boot-first-version-learn ) [思維導圖地址](https://www.processon.com/view/link/5deeefdee4b0e2c298aa5596) 工程結構圖: ![](https://img2018.cnblogs.com/blog/519126/201912/519126-20191215144930717-1919774390.png) # 概要 本篇是獨立的,和前面幾篇aop相關分析沒有特別關聯,但是使用了上一篇提到的工具類。 [曹工說Spring Boot原始碼(19)-- Spring 帶給我們的工具利器,建立代理不用愁(ProxyFactory)](https://www.cnblogs.com/grey-wolf/p/12359963.html) 之前也使用類似的思路,實現過完整sql日誌記錄。 [曹工雜談--使用mybatis的同學,進來看看怎麼在日誌列印完整sql吧,在資料庫可執行那種](https://www.cnblogs.com/grey-wolf/p/12130803.html) 這兩天在搬磚,有個需求,是統計類的。一般來說,統計類的東西,比如要統計:使用者總數,使用者的新增總數,當天每個小時為維度的新增數量,各個渠道的新增使用者數量;這些,可能都得在redis裡維護,然後某個使用者註冊時,去把所有這些redis結構+1。 但這種程式碼,一般入口很多,修改這些值的地方很多,編碼時很容易發生遺漏,或者編碼錯誤,導致最後統計資料不準確。資料不準確,當然是bug,問題是,這種bug還不好排查。 如果能夠記錄下redis操作日誌就好了。 以下,是我已經實現的效果,這是一次請求中的一次redis操作,可以看到,是put方法。 ![](https://img2018.cnblogs.com/blog/519126/202002/519126-20200228081057455-1436295322.png) # 實現思路 我們用的是spring boot 2.1.7,直接整合的RedisTemplate。當然,只要是使用RedisTemplate即可,和spring boot沒多大關係。 我看了下我們平時是怎麼去操作redis 的hash結構的,大概程式碼如下: ```java @Autowired @Qualifier("redisTemplate") private RedisTemplate redisTemplate; HashOperations ops = redisTemplate.opsForHash(); ops.put(key, hashKey,fieldValue); ``` 一般就是,先通過opsForHash,拿到HashOperations,再去操作hash結構。 我現在的想法就是,在執行類似ops的put的方法之前,把那幾個引數記錄到日誌裡。 要想讓ops記錄我們的日誌,我們只能攔截其每個方法,這一步就得使用一個代理物件,去替換掉真實的物件。 但是,怎麼才能讓redisTemplate.opsForHash()返回的ops,是我們代理過的物件呢? 所以,這一步,還得在生成redisTemplate的地方下功夫,讓其生成一個redisTemplate的代理物件,這個代理物件,攔截opsForHash方法。 總結下,需要做兩件事: 1. 對redisTemplate做代理,攔截opsForHash方法; 2. 在拿到第一步的原有的ops物件後,對ops物件做代理,攔截其put方法等。 # 程式碼實現 ##原有程式碼 ```java @Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setValueSerializer(new CustomGenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new CustomHashKeyRedisSerializer()); template.setKeySerializer(RedisSerializer.string()); template.setHashValueSerializer(new CustomGenericJackson2JsonRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); return template; } ``` ## 代理RedisTemplate ```java @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setValueSerializer(new CustomGenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new CustomHashKeyRedisSerializer()); template.setKeySerializer(RedisSerializer.string()); template.setHashValueSerializer(new CustomGenericJackson2JsonRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTarget(template); proxyFactory.setProxyTargetClass(true); proxyFactory.addAdvice(new MethodInterceptor() { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //攔截opsForHash boolean b = invocation.getMethod().getName().equals("opsForHash"); if (b) { // todo,下一步再完善這裡 } return invocation.proceed(); } }); //這裡獲取到針對template的代理物件,並返回 Object proxy = proxyFactory.getProxy(); return (RedisTemplate) proxy; } ``` 大家可以仔細看上面的程式碼,利用了前一講我們學習了的ProxyFactory,來生成代理;使用它呢,比較方便,不用管底層它是用jdk動態代理,還是cglib代理,spring已經幫我們處理好了。 總之,上面這段,就是把redisTemplate給換了。我們具體要在攔截了opsForHash裡,做什麼動作呢?我們再看。 ## 代理opsForHash的返回結果 ```java @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setValueSerializer(new CustomGenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new CustomHashKeyRedisSerializer()); template.setKeySerializer(RedisSerializer.string()); template.setHashValueSerializer(new CustomGenericJackson2JsonRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTarget(template); proxyFactory.setProxyTargetClass(true); proxyFactory.addAdvice(new MethodInterceptor() { @Override public Object invoke(MethodInvocation invocation) throws Throwable { boolean b = invocation.getMethod().getName().equals("opsForHash"); if (b) { // 1. 這一步,拿到原有的opsForHash的返回結果 HashOperations hashOperations = (HashOperations) invocation.proceed(); //2. 下邊,對hashOperations進行代理 ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTarget(hashOperations); proxyFactory.setProxyTargetClass(false); proxyFactory.setInterfaces(HashOperations.class); //3. 我們這個代理幹什麼事呢,就是加了一個方法前的攔截器,記錄日誌 proxyFactory.addAdvice(new MethodBeforeAdviceInterceptor(new MethodBeforeAdvice() { // 使用fastjson格式化了引數,並記錄到日誌 @Override public void before(Method method, Object[] args, Object target) { log.info("method:{},args:{}",method.getName(), JSON.toJSONString(args, SerializerFeature.PrettyFormat)); } })); // 這裡返回針對hashOperations的代理 return proxyFactory.getProxy(); } return invocation.proceed(); } }); Object proxy = proxyFactory.getProxy(); return (RedisTemplate) proxy; } ``` # 總結 我這個攔截比較粗,現在是把get類的日誌也打出來了。大家可以判斷下method的名稱,來自行過濾掉。 ![](https://img2018.cnblogs.com/blog/519126/202002/519126-20200228083132374-1411938924.png) ok,本篇先到這裡。下講繼續講Spring ProxyFactory的