写点什么

AOP+MybatisPlus 优化特殊的日志模块

作者:4ye
  • 2021 年 12 月 03 日
  • 本文字数:4323 字

    阅读完需:约 14 分钟

今天 4ye 来和小伙伴们分享下我在项目中利用 AOP + MybatisPlus 对项目进行重构,优化系统中特殊的日志模块的故事啦 😄 (PS:ES 写了一半 ~ 只能先来这个了)


其实这篇文章我是从吐槽部分开始写的 哈哈哈(不知不觉就吐槽了近千字 🤣)不过咋们还是从技术部分开始吧 ~


功能描述

很久之前(大概有一年了叭,痛苦面具 🙃),老大让我把老项目中的日志模块移植到新项目中,这个日志模块的主要功能就是针对大部分表,做下面的操作


删除成功时,将被删除的数据记录到相应的 log 表

当修改或者插入数据成功时,将这些数据记录到相应的 log 表


Log 表 就是在 普通表 的基础上,新增几个字段,如操作 ID 和操作方法。


操作 ID:类似请求 ID 。


操作方法: 表示这个行为是 CRUD 中 CUD 的哪一个。


比如在请求 001 中删除表 A 中的某条数据,则在 LOG_A 中会记录下这个 A 数据



看到这里,你是否也觉得这 log 表很奇怪?


别急,最后再吐槽下 哈哈,先来说说这个技术 🐷

简单思考

我们可以发现这些这些东西和业务无关,可以直接用 AOP 来实现,老项目中也是用了 AOP。(听君一席话,听君一席话 哈哈哈 🤣)


先简单思考下会遇到什么问题 👇


比如:


  1. 新增数据到相应的 log 表,意味着有很多简单的插入语句要写?还要考虑批量操作 🐖

  2. 操作 id 是怎么生成的?是直接用 uuid ,还是数据库自增 id?

  3. 怎么获取删除前的数据信息 、更新后的数据、插入后的数据 等


这里就不卖关子啦 直接来看这个日志模块的设计

操作 ID

先来看这个 操作 ID 的生成,这里就没啥特别的 直接定义一个 注解 如 @GenerateRequestID ,加在需要被拦截的方法上,然后我们在 AOP 中拦截它即可。功能如下 👇



可以看到这个 操作 ID 是直接使用 Oracle 的 Sequence 去生成的,是有序的 ,能直接看出这批数据操作的先后顺序。


在增强这部分功能时,我发现之前 旧版本居然没用 AOP 去设置这个 操作 ID,而是手动在需要的地方加,而且还定义了很多的 Key 来存储生成的这个 操作 ID,可以发现对这个 ThreadLocal 也很不熟悉呀! 🙃


伪代码如下 😱 (坏代码 …… )


public void a() {    BigDecimal transactionId = sysLogMapper.generateTransactionId();    ThreadLocalUtil.setValue(SysLogConstants.xxx_LOG_TRANSACTION_ID, transactionId);    ThreadLocalUtil.setValue(SysLogConstants.aaa_LOG_TRANSACTION_ID, transactionId);
// 业务方法
ThreadLocalUtil.remove(SysLogConstants.xxx_LOG_TRANSACTION_ID); ThreadLocalUtil.remove(SysLogConstants.aaa_LOG_TRANSACTION_ID);}
复制代码


心细的小伙伴会不会有个小疑问,为什么我是在 请求结束 时才去清除这个 操作 ID,而不是在 AOP 的 After 操作中去做的😄


其实这里是有个小插曲的,一开始我是在 AOP 的 @After 操作中去删除这个 操作 ID 的,但是呢 🙄 ,有同事将我改好的日志模块中的部分功能添加到之前的老项目中,而且他直接将这个注解加在 service 上,结果系统出现了 bug 🙄,还把我拉过去讨论 😒 ,这就很无语了……



不过这个 bug 是不难发现的,毕竟这个注解如果加在 service 层面,会存在 service 调用 service 的情况,这样不仅会出现第一个 service 中生成的 操作 ID 被第二个 service 覆盖,而且在第二个 service 结束后,操作 ID 会被清除掉,但是这个字段是不允许为 null 的,所以就报错了。


按理说,直接加在这个 controller 层面就没问题了,但是讨论过后,在同事的建议下,我也同意对它进行小小的升级下,将这个 清除操作 ID 的行为移动到这个 拦截器 中 👇


@Componentpublic class ThreadLocalInterceptor implements HandlerInterceptor {    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        ThreadLocalUtil.clear();    }}
复制代码


感觉这种方式也挺简洁的,在请求结束后直接清掉这个 ThreadLocal 中的内容。🐖

整体设计

接着,我们来看看这个 datalog 的生成。 新版的整体思路如下。😋 (画出来清晰多了)


删除操作

思路如上图~


在删除成功时,要将被删除的数据写到 Log 表 …… 😑


这里有两种情况:(一)根据主键删除 ,这种最简单,我们直接根据主键查出之前的数据即可。(二)根据其他条件删除,这里我们自定义获取主键的方法以便复用上面的方法。


这里就不得不提下这个 MybatisPlus 的好处了~ ,借用 BaseMapper 的 selectBatchIds 方法,我们可以很轻松的


查询出这些数据出来。



因为在使用 MybatisPlus 时,我们会生成相应的 Mapper 方法,而这里就很好地体现了 Java 多态 的特点,我们只需要调用 (BaseMapper) SpringUtil.getBean(XXXMapper.class).selectBatchIds(result) ;即可实现。😄


所以将这个参数挂在 @MethodLog 注解上即可。


@Mapperpublic interface XXXMapper extends BaseMapper<XXX> {
}
复制代码


而在旧版本中,由于没有用到 MybatisPlus ,自然写了很多的 删除语句 😱


而且由于对这个 Spring 的理解不够,出现了把 实现了同一个接口的不同子类注入到一个 Map 中,使用时再从其中获取的行为,最要命的是,这个 map 的 key 不是那些子类的名称,还写了很多 switch case …… 😶 来获取



其实我们直接用 ApplicationContext 的 getBean 就可以获取到了 🐖

更新,插入操作

在更新或者插入成功后,还要将这些数据写到 Log 表 …… 😑


真是太魔幻了…… 再坚持下就快到吐槽环节了😂


这里我们要考虑一个问题了—— 插入数据时,ID 是插入数据前就有的,还是插入数据后才有的?🐷


在项目中我们使用的是这个 Oracle,借助它的 Sequence,我们可以先获取这个 ID (select XX_SEQ.nextval from dual),再设置到这个对象中去,然后再插入 DB 中 。


而在使用 MySQL 时,我们一般都是通过数据库的自增 ID,当数据插入后,再从这个对象中获取到这个 ID 的。


这也决定了我是在 AOP 的 Before 中记录下这些 ID ,还是在 AfterReturning 中去获取的。


其实一开始我是直接在 Before 中去记录的,后来考虑到这种情况后,才把它移到 AfterReturning 中去的,毕竟不管你先生成还是后生成,我都可以后获取,而且第二种模式兼容第一种模式 🐷


更新的话也有两种情况:(一)根据主键更新 ,这种最简单,我们直接根据主键查出之前的数据即可。(二)根据其他条件更新,这里我们自定义获取主键的方法以便复用上面的方法。


一般情况下,我们可以自动去获取这个 id,有些情况比较复杂的,就提供这个手动模式,自己调用 ThreadLocal 并耦合到代码中。


最后使用 Spring 的工具 BeanUtils ,将数据拷贝到日志对象中 BeanUtils.copyProperties(model, logModel); ,再通过 BaseMapper 的 insert 方法,将数据一条条插入到 Log 表。


到此,就完成了这个特殊的日志模块的优化了。



这个模块也为团队节省了 N 倍的开发时间,减少了很多冗余,无效的代码,也提高了这个代码的可维护性,受到同事的肯定当时 哈哈 😝 而且好像从这时候开始,老大找我做了一些通用模块的开发,比如 模板,邮件,Excel 等,有机会再来和小伙伴们分享下~ 😄。



吐槽

终于来到这里了,说实话,我到现在都觉得这个日志模块很 “特别” 🐷,因为这个日志模块不像我平时了解过的那些,比如:


  • 请求的入参,出参

  • 业务

  • 异常

  • JVM,Nginx,Tomcat 等等


而且奇怪的点还不是写入 DB 这个操作,而是针对大量的表做这备份的这个行为,比如 有这个表 A,然后我还要创建相应的日志表 LOG_A,而且只要你修改或者新增数据到表 A,那么 LOG_A 会将你更改后的 A 数据记录下来,删除的话,会将你删除前的 A 数据记录下来,删除成功后才记录到这个 LOG_A 。


而且这个 LOG_A 就比表 A 多了几个字段,比如这个操作行为( CRUD 中 的 CUD)和 操作 ID (即请求 ID)。


这…… 简直就是究极备份了🐖 (我实在想不出哪个词来形容了 哈哈 我只能说太慎重了!和 慎重勇者 的主角有得一比)


这是我新的感悟 哈哈。


还记得那时的我十分抗拒把这个东西搬到新项目中,因为我觉得它很多余,这不就是把数据又写了一遍到另一个表中,而且我觉得用网上的例子就可以了啦,就记录下数据从什么修改成什么就好了,要那么多干嘛🤨。



我当时也憋不住,就向老大表达了我的疑惑,老大和我解释说这个日志模块是为了方便找 bug,找出哪些数据是有问题的,写这篇文章的时候我还特意再去请教了一下,他说他们的老项目用到了,但是具体怎么用也没说 🐖


为了搞清除这个东西的作用,额 我已经问了三个项目组的同事了,终于有个同事使用到了,就是说有一次数据出了问题,看代码看了好久都没有觉得哪里有问题,后来就是靠着这个请求 id,去 db 找那批数据出来分析,才找到问题的。


不过我听完还是觉得一言难尽,什么 bug 连 debug 都不能找出来,得靠分析这批数据了🙃 虽然在新项目中还是有它的身影,但是我想了好久都没有见到它发挥作用,可能这东西还是没用才好吧,甚至觉得带来负作用…… 😮

印象中在重写这个模块时,遇到一个很有意思的问题 如下 👇


@Mapperpublic interface XXXMapper extends BaseMapper<XXX> {
@Override @MethodLog(method = TableAction.D, model = XXX.class, logModel = LogXXX.class, logModelIdName = "xxxId", mapper = XXXMapper.class, logMapper = LogXXXMapper.class ) int deleteById(Serializable id); @Override @MethodLog(method = TableAction.U, model = XXX.class, logModel = LogXXX.class, logModelIdName = "xxxId", mapper = XXXMapper.class, logMapper = LogXXXMapper.class ) int updateById(@Param(Constants.ENTITY) XXX entity);}
复制代码


毕竟我要将 MethodLog 注解挂到方法上,只能这样了。


同时,我才发现 接口在继承别的接口时,也是可以添加 @Override 注解 ,不一定是常见的那种。


还有就是一开始我用 IDEA 帮我重写这个 updateById 方法时,但是它帮我省掉了 @Param(Constants.ENTITY) 注解,导致这个 update 操作无法生效,因为 MybatisPlus 中用了这个 “et” 来统一这个对象的别名~ ,不加的话无法匹配到。

最后

本文就分享到这里啦🐖 最近有好多人生小感悟 嘿嘿 下次再分享啦。之前定的那些文章还在计划里~ 在抓紧更啦!!😝


回忆起做这个日志模块的过程,特别是画一遍这个流程图的感觉真的是太酸爽了 ,也给小伙伴们提供一个思路,简单的 CRUD 就不要自己写啦,能用 AOP+MybatisPlus 去操作的话会简洁很多!


👉 https://github.com/Java4ye/springboot-demo-4ye


喜欢的话可以 点赞 & 关注星标 下公众号 Java4ye 支持下 4ye 呀😝,这样就可以第一时间收到更文消息啦🐷


我是 4ye 咱们下期应该……很快再见!! 😆

发布于: 2021 年 12 月 03 日阅读数: 34
用户头像

4ye

关注

公众号:J a v a 4 y e 2021.07.19 加入

定个小目标,写个三年~ 分享一个普通程序员的技术生涯,生活点滴,让学习成为一种习惯!

评论

发布
暂无评论
AOP+MybatisPlus 优化特殊的日志模块