如何使用注解優(yōu)雅的記錄操作日志

寫在開頭
本文討論如何優(yōu)雅的記錄操作日志,并且實(shí)現(xiàn)了一個SpringBoot Starter(取名log-record-starter),方便的使用注解記錄操作日志,并將日志數(shù)據(jù)推送到指定數(shù)據(jù)管道(消息隊列等)
本文靈感來源于美團(tuán)技術(shù)團(tuán)隊的文章:如何優(yōu)雅地記錄操作日志?。文中使用的部分定義描述和示例來源于美團(tuán)原文,請知悉。
本文作為《萌新寫開源》的開篇,先把項(xiàng)目成品介紹給大家,之后的文章會詳細(xì)介紹,如何一步步將個人項(xiàng)目做成一個大家都能參與的開源項(xiàng)目(如何寫SpringBoot Starter,如何上傳到Maven倉庫,如何設(shè)計和使用注解和切面等),麻煩大家多多點(diǎn)贊支持,這是我更新的動力。請大家放心,公眾號還會持續(xù)更新,我沒有忘掉密碼。:)——蠻三刀醬
本文目錄:
什么是操作日志? Java中常見的操作日志實(shí)現(xiàn)方式 實(shí)戰(zhàn):通過注解實(shí)現(xiàn)操作日志的記錄
什么是操作日志?
定義:操作日志主要是指對某個對象進(jìn)行新增操作或者修改操作后記錄下這個新增或者修改,操作日志要求可讀性比較強(qiáng),因?yàn)樗饕墙o用戶看的,比如訂單的物流信息,用戶需要知道在什么時間發(fā)生了什么事情。再比如,客服對工單的處理記錄信息。
以我們系統(tǒng)內(nèi)部使用的一個CRM系統(tǒng)舉例,里面每個聯(lián)系人的資料都會有操作歷史:

這些數(shù)據(jù)就是操作系統(tǒng)日志,這些數(shù)據(jù)通常會以結(jié)構(gòu)化數(shù)據(jù)的形式存儲在數(shù)據(jù)庫中,對于開發(fā)來說,這種日志的代碼邏輯通常是非常規(guī)律,比如讀取變化前和變化后的數(shù)據(jù),獲取當(dāng)前操作人和操作時間等等。
常見的操作日志實(shí)現(xiàn)方式
在小型項(xiàng)目中,這種日志記錄的操作通常會以提供一個接口或整個日志記錄Service來實(shí)現(xiàn)。那么放到多人共同開發(fā)的項(xiàng)目中,除了封裝一個方法,還有什么更好的辦法來統(tǒng)一實(shí)現(xiàn)操作日志的記錄?下面就要討論下在Java中,常見的操作日志實(shí)現(xiàn)方式。
當(dāng)你需要給一個大型系統(tǒng)從頭到尾加上操作日志,那么除了上述的手動處理方式,也有很多種整體設(shè)計方案:
1. 使用Canal監(jiān)聽數(shù)據(jù)庫記錄操作日志
Canal應(yīng)運(yùn)而生,它通過偽裝成數(shù)據(jù)庫的從庫,讀取主庫發(fā)來的binlog,用來實(shí)現(xiàn)數(shù)據(jù)庫增量訂閱和消費(fèi)業(yè)務(wù)需求??梢钥次业倪@篇文章:
這個方式有點(diǎn)是和業(yè)務(wù)邏輯完全分離,缺點(diǎn)也很大,需要使用到MySQL的Binlog,向DBA申請就有點(diǎn)困難。如果涉及到修改第三方接口,那么就無法監(jiān)聽別人的數(shù)據(jù)庫了。所以調(diào)用RPC接口時,就需要額外的在業(yè)務(wù)代碼中增加記錄代碼,破壞了“和業(yè)務(wù)邏輯完全分離”這個基本原則,局限性大。
2. 通過日志文件的方式記錄
log.info("訂單已經(jīng)創(chuàng)建,訂單編號:{}",?orderNo)
log.info("修改了訂單的配送地址:從“{}”修改到“{}”,?"金燦燦小區(qū)",?"銀盞盞小區(qū)")
這種方式,需要手動的設(shè)定好操作日志和其他日志的區(qū)別,比如給操作日志單獨(dú)的Logger。并且,對于操作人的記錄,需要在函數(shù)中額外的寫入請求的上下文中。后期這種日志還需要在SLS等日志系統(tǒng)中做額外的抽取。
3. 通過 LogUtil 的方式記錄日志
LogUtil.log(orderNo,?"訂單創(chuàng)建",?"小明")
LogUtil.log(orderNo,?"訂單創(chuàng)建,訂單號"+"NO.11089999",??"小明")
String?template?=?"用戶%s修改了訂單的配送地址:從“%s”修改到“%s”"
LogUtil.log(orderNo,?String.format(tempalte,?"小明",?"金燦燦小區(qū)",?"銀盞盞小區(qū)"),??"小明")
這種方式會導(dǎo)致業(yè)務(wù)的邏輯比較繁雜,最后導(dǎo)致 LogUtils.logRecord() 方法的調(diào)用存在于很多業(yè)務(wù)的代碼中,而且類似 getLogContent() 這樣的方法也散落在各個業(yè)務(wù)類中,對于代碼的可讀性和可維護(hù)性來說是一個災(zāi)難。
4. 方法注解實(shí)現(xiàn)操作日志
@OperationLog(bizType?=?"bizType",?bizId?=?"#request.orderId",?pipeline?=?DataPipelineEnum.QUEUE)
public?Response ?function(Request?request)?{
??//?方法執(zhí)行邏輯
}
我們可以在注解的操作日志上記錄固定文案,這樣業(yè)務(wù)邏輯和業(yè)務(wù)代碼可以做到解耦,讓我們的業(yè)務(wù)代碼變得純凈起來。
美團(tuán)的原文給出了注解記錄日志的詳細(xì)架構(gòu)設(shè)計方案,并且貼出了部分源碼。但是文中并沒有完整的開源項(xiàng)目,由于自己也很感興趣,并且公司的業(yè)務(wù)正好也有類似需求,所以我花了點(diǎn)時間,實(shí)現(xiàn)了一版最簡易的版本,支持將操作日志傳遞到消息隊列中。
實(shí)戰(zhàn):通過注解實(shí)現(xiàn)操作日志的記錄
大樓不是一天建成的,美團(tuán)博客中描述的方案應(yīng)該在公司內(nèi)部已經(jīng)非常成熟了,我也沒有那么多精力一口氣吃成一個胖子,我們從最基礎(chǔ)的版本寫起。
我給自己的這個項(xiàng)目,或者說依賴包起名為log-record-starter,一方面遵循springboot-starter命名規(guī)范,一方面也表明項(xiàng)目的用處,記錄日志。
開啟項(xiàng)目之前,先問問自己
Q:你這個依賴包,又是一個冗余的造輪子吧?市面上這種東西是不是已經(jīng)夠多了?
A:本著有現(xiàn)成輪子絕不造輪子的原則,我在Github和其他網(wǎng)站進(jìn)行了一系列的相關(guān)搜索,Github有幾個類似的實(shí)現(xiàn)項(xiàng)目,不過都以個人實(shí)現(xiàn)為主,沒有一個具有一定影響力的成熟項(xiàng)目。基于我在自己的業(yè)務(wù)項(xiàng)目中擁有實(shí)際的場景需求,并且目前還沒有滿足我需求的現(xiàn)成可接入依賴,我才開始這個依賴包的代碼編寫。
Q:我用了你這個依賴包,是不是很復(fù)雜?之后你不維護(hù)了的話,是不是坑我們這些吃螃蟹的?
A:依賴包的維護(hù)問題一直是一個大問題,本著最小依賴,盡量可擴(kuò)展的原則。本庫特點(diǎn)如下:
使用SpringBoot Starter,接入只需要簡單引入一個依賴。 通過Spring Spel表達(dá)式拿到參數(shù),對你的業(yè)務(wù)邏輯沒有侵入性。 默認(rèn)使用RabbitMq傳遞日志消息,日志操作解耦。 之后會引入其他數(shù)據(jù)源,例如Kafka等(畢竟還要給三歪的項(xiàng)目用,我沒有被三歪綁架,嗯,絕對沒有)。
好了,這就是我想說在前面的話。下面就是該項(xiàng)目的使用介紹和應(yīng)用場景介紹。
Log-record-starter 一句話介紹
本項(xiàng)目支持用戶使用注解的方式從方法中獲取操作日志,并推送到指定數(shù)據(jù)源
只需要簡單的加上一個@OperationLog便可以將方法的參數(shù),返回結(jié)果甚至是異常堆棧通過消息隊列發(fā)送出去,統(tǒng)一處理。
@OperationLog(bizType?=?"bizType",?bizId?=?"#request.orderId",?pipeline?=?DataPipelineEnum.QUEUE)
public?Response?function(Request?request)?{
??//?方法執(zhí)行邏輯
}
使用方法
只需要簡單的三步:
第一步:SpringBoot項(xiàng)目中引入依賴
????cn.monitor4all
????log-record-starter
????1.0.0
這里先打斷一下,由于Maven公共倉庫,是全球唯一托管的,個人開發(fā)的項(xiàng)目要提交上去,需要復(fù)雜的審核流程,我搞了一會沒搞定,就先將包傳到了Github Package上(實(shí)際就是Github的私有Maven庫),所以大家引入依賴后,是不會直接拉到包的,需要配置下你的Maven settings.xml文件。(之后我肯定想辦法發(fā)到公共倉庫,嗚嗚嗚~)
配置很簡單,兩步,一步是去Github登錄,到自己的Settings中,申請一個token,拿到一串字符串。

第二步,找到你的settings.xml文件,添加上:
activeProfiles>
????github
??
??
????
??????github
??????
????????
??????????central
??????????https://repo1.maven.org/maven2
????????
????????
??????????github
??????????https://maven.pkg.github.com/OWNER/REPOSITORY
??????????
????????????true
??????????
????????
??????
????
??
??
????
??????github
??????這里填寫你的Github用戶名
??????這里填寫你剛才申請的token
????
??
還搞不定的同學(xué),這里是Github官方中文教程:
https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry
重啟下你的IDEA,能看到下面這個,應(yīng)該你的settings.xml生效了。

目前我的版本號是1.0.0,之后會更新,未來最新版本號在我倉庫查詢:
https://github.com/qqxx6661/logRecord
第二步:在Spring配置文件中添加RabbitMq數(shù)據(jù)源配置
在自己公司里由于阿里封裝了自己的MQ叫做MetaQ,并沒有對外開源,所以這里先接入了RabbitMQ,也算是比較通用,圖個方便。未來會接其他數(shù)據(jù)源。RabbitMq的安裝在這里不展開了,實(shí)在是不想把篇幅拉得太大,大家可以自行谷歌下,比如“Docker安裝RabbitMq”類似的文章,幾分鐘就可以設(shè)置安裝好。
log-record.rabbitmq.host=localhost
log-record.rabbitmq.port=5672
log-record.rabbitmq.username=admin
log-record.rabbitmq.password=xxxxxxxx
log-record.rabbitmq.queue-name=logrecord
log-record.rabbitmq.routing-key=
log-record.rabbitmq.exchange-name=logrecord
第三步:在你自己的項(xiàng)目中,在需要記錄日志的方法上,添加注解。
@OperationLog(bizType?=?"bizType",?bizId?=?"#request.orderId",?pipeline?=?DataPipelineEnum.QUEUE)
public?Response?function(Request?request)?{
?//?方法執(zhí)行邏輯
} (必填)bizType:業(yè)務(wù)類型 (必填)bizId:唯一業(yè)務(wù)ID(支持SpEL表達(dá)式) (必填)pipeline:數(shù)據(jù)管道,目前只有QUEUE一個數(shù)據(jù)管道,后續(xù)可考慮接入更多數(shù)據(jù)源 (非必填)msg:需要傳遞的其他數(shù)據(jù)(支持SpEL表達(dá)式) (非必填)tag:自定義標(biāo)簽
代碼工作原理
由于采用的是SpringBoot Starter方式,所以只要你是用的是SpringBoot,會自動掃描到依賴包中的類,并自動通過Spring進(jìn)行配置和管理。
該注解通過在切面中解析SpEL參數(shù)(啥事SpEL?快去谷歌下,之后要講),將數(shù)據(jù)發(fā)往數(shù)據(jù)源。目前僅支持RabbitMq,發(fā)送的消息體如下:
方法處理正常發(fā)送消息體:
[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","2","3"],"testStr":"str"},?tag=operation)]
方法處理異常發(fā)送消息體:
[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","2","3"],"testStr":"str"},?tag=operation)]
LogDTO是定義的消息結(jié)構(gòu):
logId:生成的UUID
bizId:注解中傳遞的bizId
bizType:注解中傳遞的bizType
exception:若方法執(zhí)行失敗,寫入執(zhí)行的異常信息
operateDate:操作執(zhí)行的當(dāng)前時間
success:方式是否執(zhí)行成功
msg:注解中傳遞的tag
tag:注解中傳遞的tag
我還加上了重復(fù)注解的支持,可以在一個方法上同時加多個@OperationLog,下圖是最終使用效果,可以看到,有幾個@OperationLog,就能同時發(fā)送多條日志:

項(xiàng)目具體的實(shí)現(xiàn)原理和細(xì)節(jié),放在下一篇文章詳細(xì)講。(肯定會填坑)
應(yīng)用場景
以下羅列了一些實(shí)際的應(yīng)用場景,包括我業(yè)務(wù)中實(shí)際使用,并且已經(jīng)上線使用的場景。
一、特定操作記錄日志:如文章最上面一張CRM系統(tǒng)的圖描述的那樣,在用戶進(jìn)行了編輯操作后,拿到用戶操作的數(shù)據(jù),執(zhí)行日志寫入。
二、特定操作觸發(fā)通知:由于我的業(yè)務(wù)是接手了好幾個倉庫,并且這幾個倉庫的操作串成了一條完成鏈路,我需要在鏈路的某個節(jié)點(diǎn)觸發(fā)給用戶的提醒,如果寫硬編碼也可以實(shí)現(xiàn),但是遠(yuǎn)不如在方法上使用注解發(fā)送消息來得方便。例如下方在下單方法調(diào)用后發(fā)送消息。

三、特定操作更新數(shù)據(jù)表:我的業(yè)務(wù)中,幾個系統(tǒng)互相吞吐數(shù)據(jù),訂單的一部分?jǐn)?shù)據(jù)存留在外部系統(tǒng)里,我們最終目標(biāo)想要將其中一個系統(tǒng)替代掉,所以需要攔截他們的數(shù)據(jù),恰好幾個系統(tǒng)是使用LINK作為網(wǎng)關(guān)的,我們將數(shù)據(jù)請求攔截一層,并將攔截的方法使用該二方庫進(jìn)行全部參數(shù)的發(fā)送,將數(shù)據(jù)同步寫入我們自己的數(shù)據(jù)庫中,實(shí)現(xiàn)”雙寫“。

四、跨多應(yīng)用數(shù)據(jù)聚合操作:和”三“類似,在多個應(yīng)用中,如果需要做行為相同的業(yè)務(wù)邏輯,完全可以在各個系統(tǒng)中將數(shù)據(jù)發(fā)送到同一個消息隊列中,再進(jìn)行統(tǒng)一處理。
附錄:Demo
最后,肯定有小伙伴希望有一個完整的使用Demo,這就奉上!
https://github.com/qqxx6661/systemLog
總結(jié)
本文帶大家了解了操作日志在Java中的幾種實(shí)現(xiàn)方式,并且初步介紹了自己的實(shí)現(xiàn)代碼,在之后的文章里,我會把實(shí)現(xiàn)的細(xì)節(jié),包括如何部署到Maven倉庫等一一和大家嘮嘮,記得留下你的點(diǎn)贊和收藏~
我是目前在阿里搬磚的工程師蠻三刀醬。
持續(xù)的創(chuàng)作離不開你的點(diǎn)贊和轉(zhuǎn)發(fā)分享!
往期精彩文章:
老外為了在MacBook上玩原神,讓M1支持了所有iOS應(yīng)用
API網(wǎng)關(guān)才是大勢所趨?SpringCloud Gateway保姆級入門教程
誰會拒絕一臺Win11和MacOS無縫切換的MacBook呢?Parallels17極速體驗(yàn)
