写在开头
本⽂讨论如何优雅的记录操作⽇志,并且实现了⼀个SpringBoot Starter(取名log-record-starter),⽅便的使⽤注解记录操作⽇志,并将⽇志数据推送到指定数据管道(消息队列等)
本⽂灵感来源于美团技术团队的⽂章:。⽂中使⽤的部分定义描述和⽰例来源于美团原⽂,请知悉。
本⽂作为《萌新写开源》的开篇,先把项⽬成品介绍给⼤家,之后的⽂章会详细介绍,如何⼀步步将个⼈项⽬做成⼀个⼤家都能参与的开源项⽬(如何写SpringBootStarter,如何上传到Maven仓库,如何设计和使⽤注解和切⾯等),⿇烦⼤家多多点赞⽀持,这是我更新的动⼒。请⼤家放⼼,公众号还会持续更新,我没有忘掉密码。:)——蛮三⼑酱本⽂⽬录:
什么是操作⽇志?
Java中常见的操作⽇志实现⽅式实战:通过注解实现操作⽇志的记录
什么是操作⽇志?
定义:操作⽇志主要是指对某个对象进⾏新增操作或者修改操作后记录下这个新增或者修改,操作⽇志要求可读性⽐较强,因为它主要是给⽤户看的,⽐如订单的物流信息,⽤户需要知道在什么时间发⽣了什么事情。再⽐如,客服对⼯单的处理记录信息。以我们系统内部使⽤的⼀个CRM系统举例,⾥⾯每个联系⼈的资料都会有操作历史:
这些数据就是操作系统⽇志,这些数据通常会以结构化数据的形式存储在数据库中,对于开发来说,这种⽇志的代码逻辑通常是⾮常规律,⽐如读取变化前和变化后的数据,获取当前操作⼈和操作时间等等。
常见的操作⽇志实现⽅式
在⼩型项⽬中,这种⽇志记录的操作通常会以提供⼀个接⼝或整个⽇志记录Service来实现。那么放到多⼈共同开发的项⽬中,除了封装⼀个⽅法,还有什么更好的办法来统⼀实现操作⽇志的记录?下⾯就要讨论下在Java中,常见的操作⽇志实现⽅式。
当你需要给⼀个⼤型系统从头到尾加上操作⽇志,那么除了上述的⼿动处理⽅式,也有很多种整体设计⽅案:
1. 使⽤Canal监听数据库记录操作⽇志
Canal应运⽽⽣,它通过伪装成数据库的从库,读取主库发来的binlog,⽤来实现数据库增量订阅和消费业务需求。可以看我的这篇⽂章:
这个⽅式有点是和业务逻辑完全分离,缺点也很⼤,需要使⽤到MySQL的Binlog,向DBA申请就有点困难。如果涉及到修改第三⽅接⼝,那么就⽆法监听别⼈的数据库了。所以调⽤RPC接⼝时,就需要额外的在业务代码中增加记录代码,破坏了“和业务逻辑完全分离”这个基本原则,局限性⼤。
2. 通过⽇志⽂件的⽅式记录
log.info(\"订单已经创建,订单编号:{}\
log.info(\"修改了订单的配送地址:从“{}”修改到“{}”, \"⾦灿灿⼩区\银盏盏⼩区\")
这种⽅式,需要⼿动的设定好操作⽇志和其他⽇志的区别,⽐如给操作⽇志单独的Logger。并且,对于操作⼈的记录,需要在函数中额外的写⼊请求的上下⽂中。后期这种⽇志还需要在SLS等⽇志系统中做额外的抽取。
3. 通过 LogUtil 的⽅式记录⽇志
LogUtil.log(orderNo, \"订单创建\⼩明\")
LogUtil.log(orderNo, \"订单创建,订单号\"+\"NO.11089999\⼩明\")
String template = \"⽤户%s修改了订单的配送地址:从“%s”修改到“%s”\"
LogUtil.log(orderNo, String.format(tempalte, \"⼩明\⾦灿灿⼩区\银盏盏⼩区\"), \"⼩明\")
这种⽅式会导致业务的逻辑⽐较繁杂,最后导致 LogUtils.logRecord() ⽅法的调⽤存在于很多业务的代码中,⽽且类似 getLogContent() 这样的⽅法也散落在各个业务类中,对于代码的可读性和可维护性来说是⼀个灾难。
4. ⽅法注解实现操作⽇志
@OperationLog(bizType = \"bizType\public Response 我们可以在注解的操作⽇志上记录固定⽂案,这样业务逻辑和业务代码可以做到解耦,让我们的业务代码变得纯净起来。 美团的原⽂给出了注解记录⽇志的详细架构设计⽅案,并且贴出了部分源码。但是⽂中并没有完整的开源项⽬,由于⾃⼰也很感兴趣,并且公司的业务正好也有类似需求,所以我花了点时间,实现了⼀版最简易的版本,⽀持将操作⽇志传递到消息队列中。 实战:通过注解实现操作⽇志的记录 ⼤楼不是⼀天建成的,美团博客中描述的⽅案应该在公司内部已经⾮常成熟了,我也没有那么多精⼒⼀⼝⽓吃成⼀个胖⼦,我们从最基础的版本写起。我给⾃⼰的这个项⽬,或者说依赖包起名为log-record-starter,⼀⽅⾯遵循springboot-starter命名规范,⼀⽅⾯也表明项⽬的⽤处,记录⽇志。 开启项⽬之前,先问问⾃⼰ Q:你这个依赖包,⼜是⼀个冗余的造轮⼦吧?市⾯上这种东西是不是已经够多了? A:本着有现成轮⼦绝不造轮⼦的原则,我在Github和其他⽹站进⾏了⼀系列的相关搜索,Github有⼏个类似的实现项⽬,不过都以个⼈实现为主,没有⼀个具有⼀定影响⼒的成熟项⽬。基于我在⾃⼰的业务项⽬中拥有实际的场景需求,并且⽬前还没有满⾜我需求的现成可接⼊依赖,我才开始这个依赖包的代码编写。Q:我⽤了你这个依赖包,是不是很复杂?之后你不维护了的话,是不是坑我们这些吃螃蟹的?A:依赖包的维护问题⼀直是⼀个⼤问题,本着最⼩依赖,尽量可扩展的原则。本库特点如下: 使⽤SpringBoot Starter,接⼊只需要简单引⼊⼀个依赖。 通过Spring Spel表达式拿到参数,对你的业务逻辑没有侵⼊性。默认使⽤RabbitMq传递⽇志消息,⽇志操作解耦。 之后会引⼊其他数据源,例如Kafka等(毕竟还要给三歪的项⽬⽤)。 好了,这就是我想说在前⾯的话。下⾯就是该项⽬的使⽤介绍和应⽤场景介绍。 Log-record-starter ⼀句话介绍 本项⽬⽀持⽤户使⽤注解的⽅式从⽅法中获取操作⽇志,并推送到指定数据源 只需要简单的加上⼀个@OperationLog便可以将⽅法的参数,返回结果甚⾄是异常堆栈通过消息队列发送出去,统⼀处理。 @OperationLog(bizType = \"bizType\public Response 使⽤⽅法 只需要简单的三步: 第⼀步: SpringBoot项⽬中引⼊依赖 这⾥先打断⼀下,由于Maven公共仓库,是全球唯⼀托管的,个⼈开发的项⽬要提交上去,需要复杂的审核流程,我搞了⼀会没搞定,就先将包传到了Github Package上(实际就是Github的私有Maven库),所以⼤家引⼊依赖后,是不会直接拉到包的,需要配置下你的Maven settings.xml⽂件。(之后我肯定想办法发到公共仓库,呜呜呜~)配置很简单,两步,⼀步是去Github登录,到⾃⼰的Settings中,申请⼀个token,拿到⼀串字符串。 第⼆步,找到你的settings.xml⽂件,添加上: activeProfiles> 还搞不定的同学,这⾥是Github官⽅中⽂教程: 重启下你的IDEA,能看到下⾯这个,应该你的settings.xml⽣效了。⽬前我的版本号是1.0.0,之后会更新,未来最新版本号在我仓库查询:第⼆步: 在Spring配置⽂件中添加RabbitMq数据源配置 在⾃⼰公司⾥由于阿⾥封装了⾃⼰的MQ叫做MetaQ,并没有对外开源,所以这⾥先接⼊了RabbitMQ,也算是⽐较通⽤,图个⽅便。未来会接其他数据源。RabbitMq的安装在这⾥不展开了,实在是不想把篇幅拉得太⼤,⼤家可以⾃⾏⾕歌下,⽐如“Docker安装RabbitMq”类似的⽂章,⼏分钟就可以设置安装好。 log-record.rabbitmq.host=localhostlog-record.rabbitmq.port=5672 log-record.rabbitmq.username=adminlog-record.rabbitmq.password=xxxxxxxxlog-record.rabbitmq.queue-name=logrecordlog-record.rabbitmq.routing-key= log-record.rabbitmq.exchange-name=logrecord 第三步: 在你⾃⼰的项⽬中,在需要记录⽇志的⽅法上,添加注解。 @OperationLog(bizType = \"bizType\public Response (必填)bizType:业务类型 (必填)bizId:唯⼀业务ID(⽀持SpEL表达式) (必填)pipeline:数据管道,⽬前只有QUEUE⼀个数据管道,后续可考虑接⼊更多数据源(⾮必填)msg:需要传递的其他数据(⽀持SpEL表达式)(⾮必填)tag:⾃定义标签 代码⼯作原理 由于采⽤的是SpringBoot Starter⽅式,所以只要你是⽤的是SpringBoot,会⾃动扫描到依赖包中的类,并⾃动通过Spring进⾏配置和管理。该注解通过在切⾯中解析SpEL参数(啥事SpEL?快去⾕歌下,之后要讲),将数据发往数据源。⽬前仅⽀持RabbitMq,发送的消息体如下:⽅法处理正常发送消息体: [LogDTO(logId=3771ff1e-e5ff-4251-a534-31dab5b666b3, bizId=str, bizType=testType1, exception=null, operateDate=Sat Nov 06 20:08:54 CST 2021, success=true, msg={\"testList\":[\"1\ ⽅法处理异常发送消息体: [LogDTO(logId=d162b2db-2346-4144-8cd4-aea900e4682b, bizId=str, bizType=testType1, exception=testError, operateDate=Sat Nov 06 20:09:24 CST 2021, success=false, msg={\"testList\":[\"1\ LogDTO是定义的消息结构: logId:⽣成的UUID bizId:注解中传递的bizId bizType:注解中传递的bizType exception:若⽅法执⾏失败,写⼊执⾏的异常信息operateDate:操作执⾏的当前时间success:⽅式是否执⾏成功msg:注解中传递的tagtag:注解中传递的tag 我还加上了重复注解的⽀持,可以在⼀个⽅法上同时加多个@OperationLog,下图是最终使⽤效果,可以看到,有⼏个@OperationLog,就能同时发送多条⽇志:项⽬具体的实现原理和细节,放在下⼀篇⽂章详细讲。(肯定会填坑) 应⽤场景 以下罗列了⼀些实际的应⽤场景,包括我业务中实际使⽤,并且已经上线使⽤的场景。 ⼀、特定操作记录⽇志:如⽂章最上⾯⼀张CRM系统的图描述的那样,在⽤户进⾏了编辑操作后,拿到⽤户操作的数据,执⾏⽇志写⼊。 ⼆、特定操作触发通知:由于我的业务是接⼿了好⼏个仓库,并且这⼏个仓库的操作串成了⼀条完成链路,我需要在链路的某个节点触发给⽤户的提醒,如果写硬编码也可以实现,但是远不如在⽅法上使⽤注解发送消息来得⽅便。例如下⽅在下单⽅法调⽤后发送消息。 三、特定操作更新数据表:我的业务中,⼏个系统互相吞吐数据,订单的⼀部分数据存留在外部系统⾥,我们最终⽬标想要将其中⼀个系统替代掉,所以需要拦截他们的数据,恰好⼏个系统是使⽤LINK作为⽹关的,我们将数据请求拦截⼀层,并将拦截的⽅法使⽤该⼆⽅库进⾏全部参数的发送,将数据同步写⼊我们⾃⼰的数据库中,实现”双写“。四、跨多应⽤数据聚合操作:和”三“类似,在多个应⽤中,如果需要做⾏为相同的业务逻辑,完全可以在各个系统中将数据发送到同⼀个消息队列中,再进⾏统⼀处理。 附录:Demo 最后,肯定有⼩伙伴希望有⼀个完整的使⽤Demo,这就奉上!完整Demo项⽬:log-record-starter: 总结 本⽂带⼤家了解了操作⽇志在Java中的⼏种实现⽅式,并且初步介绍了⾃⼰的实现代码,在之后的⽂章⾥,我会把实现的细节,包括如何部署到Maven仓库等⼀⼀和⼤家唠唠,记得留下你的点赞和收藏~ 我是⽬前在阿⾥搬砖的⼯程师蛮三⼑酱。公众号:后端技术漫谈 持续的创作离不开你的点赞和转发分享! 因篇幅问题不能全部显示,请点此查看更多更全内容