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


寫在前面的話

相關背景及資源:

曹工說Spring Boot源碼(1)-- Bean Definition到底是什么,附spring思維導圖分享

曹工說Spring Boot源碼(2)-- Bean Definition到底是什么,咱們對着接口,逐個方法講解

曹工說Spring Boot源碼(3)-- 手動注冊Bean Definition不比游戲好玩嗎,我們來試一下

曹工說Spring Boot源碼(4)-- 我是怎么自定義ApplicationContext,從json文件讀取bean definition的?

曹工說Spring Boot源碼(5)-- 怎么從properties文件讀取bean

曹工說Spring Boot源碼(6)-- Spring怎么從xml文件里解析bean的

曹工說Spring Boot源碼(7)-- Spring解析xml文件,到底從中得到了什么(上)

曹工說Spring Boot源碼(8)-- Spring解析xml文件,到底從中得到了什么(util命名空間)

曹工說Spring Boot源碼(9)-- Spring解析xml文件,到底從中得到了什么(context命名空間上)

曹工說Spring Boot源碼(10)-- Spring解析xml文件,到底從中得到了什么(context:annotation-config 解析)

曹工說Spring Boot源碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)

曹工說Spring Boot源碼(12)-- Spring解析xml文件,到底從中得到了什么(context:component-scan完整解析)

曹工說Spring Boot源碼(13)-- AspectJ的運行時織入(Load-Time-Weaving),基本內容是講清楚了(附源碼)

曹工說Spring Boot源碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎么和Spring Instrumentation集成

曹工說Spring Boot源碼(15)-- Spring從xml文件里到底得到了什么(context:load-time-weaver 完整解析)

曹工說Spring Boot源碼(16)-- Spring從xml文件里到底得到了什么(aop:config完整解析【上】)

曹工說Spring Boot源碼(17)-- Spring從xml文件里到底得到了什么(aop:config完整解析【中】)

曹工說Spring Boot源碼(18)-- Spring AOP源碼分析三部曲,終於快講完了 (aop:config完整解析【下】)

曹工說Spring Boot源碼(19)-- Spring 帶給我們的工具利器,創建代理不用愁(ProxyFactory)

工程代碼地址 思維導圖地址

工程結構圖:

概要

本篇是獨立的,和前面幾篇aop相關分析沒有特別關聯,但是使用了上一篇提到的工具類。

曹工說Spring Boot源碼(19)-- Spring 帶給我們的工具利器,創建代理不用愁(ProxyFactory)

之前也使用類似的思路,實現過完整sql日志記錄。

曹工雜談--使用mybatis的同學,進來看看怎么在日志打印完整sql吧,在數據庫可執行那種

這兩天在搬磚,有個需求,是統計類的。一般來說,統計類的東西,比如要統計:用戶總數,用戶的新增總數,當天每個小時為維度的新增數量,各個渠道的新增用戶數量;這些,可能都得在redis里維護,然后某個用戶注冊時,去把所有這些redis結構+1。

但這種代碼,一般入口很多,修改這些值的地方很多,編碼時很容易發生遺漏,或者編碼錯誤,導致最后統計數據不准確。數據不准確,當然是bug,問題是,這種bug還不好排查。

如果能夠記錄下redis操作日志就好了。

以下,是我已經實現的效果,這是一次請求中的一次redis操作,可以看到,是put方法。

實現思路

我們用的是spring boot 2.1.7,直接集成的RedisTemplate。當然,只要是使用RedisTemplate即可,和spring boot沒多大關系。

我看了下我們平時是怎么去操作redis 的hash結構的,大概代碼如下:

@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String,Object> redisTemplate;

HashOperations<String, HK, HV> 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方法等。

代碼實現

原有代碼

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String,Object> 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

@Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String,Object> 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<String, Object>) proxy;
    }

大家可以仔細看上面的代碼,利用了前一講我們學習了的ProxyFactory,來生成代理;使用它呢,比較方便,不用管底層它是用jdk動態代理,還是cglib代理,spring已經幫我們處理好了。

總之,上面這段,就是把redisTemplate給換了。我們具體要在攔截了opsForHash里,做什么動作呢?我們再看。

代理opsForHash的返回結果

@Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String,Object> 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<String, Object>) proxy;
    }

總結

我這個攔截比較粗,現在是把get類的日志也打出來了。大家可以判斷下method的名稱,來自行過濾掉。

ok,本篇先到這里。下講繼續講Spring ProxyFactory的內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM