写在前面的话

相关背景及资源:

曹工说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的内容。

曹工说Spring Boot源码(20)-- 码网灰灰,疏而不漏,如何记录Spring RedisTemplate每次操作日志的更多相关文章

  1. Spring Boot分布式系统实践【基础模块构建3.3】注解轻松实现操作日志记录

    日志注解 前言 spring切面的编程,spring中事物处理.日志记录常常与pointcut相结合 * * Pointcut 是指那些方法需要被执行"AOP",是由"P ...

  2. 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享

    写在前面的话&&About me 网上写spring的文章多如牛毛,为什么还要写呢,因为,很简单,那是人家写的:网上都鼓励你不要造轮子,为什么你还要造呢,因为,那不是你造的. 我不是要 ...

  3. 曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  4. 曹工说Spring Boot源码(22)-- 你说我Spring Aop依赖AspectJ,我依赖它什么了

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  5. 曹工说Spring Boot源码(23)-- ASM又立功了,Spring原来是这么递归获取注解的元注解的

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  6. 曹工说Spring Boot源码(24)-- Spring注解扫描的瑞士军刀,asm技术实战(上)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  7. 曹工说Spring Boot源码(25)-- Spring注解扫描的瑞士军刀,ASM + Java Instrumentation,顺便提提Jar包破解

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎

    曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎 写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean De ...

  9. 曹工说Spring Boot源码(27)-- Spring的component-scan,光是include-filter属性的各种配置方式,就够玩半天了.md

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. Activiti服务任务(serviceTask)

    Activiti服务任务(serviceTask) 作者:Jesai 都有一段沉默的时间,等待厚积薄发 应用场景: 当客户有这么一个需求:下一个任务我需要自动执行一些操作,并且这个节点不需要任何的人工 ...

  2. render()到底渲染的什么?

    1.格式 render(request,"xx.html",{"xx": xx}) 2.本质 通过模板语言动态渲染字符串(HTML文件) 注意: 1.HTML文 ...

  3. QTP测试web时:打开ie浏览器进行录制但qtp录制脚本为空

    1. 关闭ie的保护模式:设置——internet选项——安全——取消勾选“启用保护模式” 这一步很关键,之前试过很多步骤,只有这个成功了. 修改后如果可行即可.如果不行再进行下面操作: 2.关闭杀毒 ...

  4. 【Oracle】分区表详解

    此文从以下几个方面来整理关于分区表的概念及操作: 1.表空间及分区表的概念     2.表分区的具体作用     3.表分区的优缺点     4.表分区的几种类型及操作方法     5.对表分区的维护 ...

  5. LeetCode 858 镜面反射

    题目 有一个特殊的正方形房间,每面墙上都有一面镜子.除西南角以外,每个角落都放有一个接受器,编号为 0, 1,以及 2. 正方形房间的墙壁长度为 p,一束激光从西南角射出,首先会与东墙相遇,入射点到接 ...

  6. Nito.AsyncEx 这个库

    有一个非常聪明的小伙子 (我高度赞扬) 叫 Stephen Cleary ,他写了一个很棒的 Extension 集,共同参与开发的还有 Stephen Toub (他显然是经验丰富的),所以我充分信 ...

  7. 使用C++进行声明式编程

            声明式编程(英语:Declarative programming)是一种编程范型,与命令式编程相对立.它描述目目标性质,让计算机明白目标,而非流程.声明式编程不用告诉电脑问题领域,从而 ...

  8. 解题笔记——NIT 遥远的村庄

    某个小镇有 N 个村庄,村庄编号1-N,给出 M 条单向道路,不存在环,即不存在 村庄A可以到达村庄B 且 村庄B也可以到达村庄A的情况.如果村庄A与村庄B之间存在一条单向道路,则说村庄A和村庄B之间 ...

  9. Mac 解决IDEA无法启动的问题

    今天重装了IDEA,发现IDEA启动不了,点击IDEA无反应,使用命令行启动idea,然后看看具体的错误,具体的启动方案就是:打开finder->应用程序->idea.app->右键 ...

  10. vue的v-if和v-show的区别

    引言 vue这两条指令,在面试vue经常会被问道它们的区别,今天我也好好查看了以下文档,做做笔记. 相同点 它们都用于条件渲染,都可以隐藏和显示DOM元素. v-if 通过判断条件成立与否,适当的销毁 ...