如何使用註解優雅的記錄操作日誌

寫在開頭
本文討論如何優雅的記錄操作日誌,並且 實現了一個SpringBoot Starter(取名log-record-starter),方便的使用註解記錄操作日誌,並將日誌資料推送到指定資料管道(訊息佇列等)
本文靈感來源於美團技術團隊的文章:如何優雅地記錄操作日誌?。文中使用的部分定義描述和示例來源於美團原文,請知悉。
本文作為《萌新寫開源》的開篇,先把專案成品介紹給大家,之後的文章會詳細介紹,如何一步步將個人專案做成一個大家都能參與的開源專案(如何寫SpringBoot Starter,如何上傳到Maven倉庫,如何設計和使用註解和切面等),麻煩大家多多點贊支援,這是我更新的動力。請大家放心,公眾號還會持續更新,我沒有忘掉密碼。:)——蠻三刀醬
本文目錄:
-
什麼是操作日誌?
-
Java中常見的操作日誌實現方式
-
實戰:通過註解實現操作日誌的記錄
什麼是操作日誌?
定義: 操作日誌 主要是指對某個物件進行新增操作或者修改操作後記錄下這個新增或者修改,操作日誌要求可讀性比較強,因為它主要是給使用者看的,比如訂單的物流資訊,使用者需要知道在什麼時間發生了什麼事情。再比如,客服對工單的處理記錄資訊。
以我們系統內部使用的一個CRM系統舉例,裡面每個聯絡人的資料都會有操作歷史:

這些資料就是作業系統日誌,這些資料通常會以結構化資料的形式儲存在資料庫中,對於開發來說,這種日誌的程式碼邏輯通常是非常規律,比如讀取變化前和變化後的資料,獲取當前操作人和操作時間等等。
常見的操作日誌實現方式
在小型專案中,這種日誌記錄的操作通常會以提供一個介面或整個日誌記錄Service來實現。那麼放到多人共同開發的專案中,除了封裝一個方法,還有什麼更好的辦法來統一實現操作日誌的記錄?下面就要討論下在Java中,常見的操作日誌實現方式。
當你需要給一個大型系統從頭到尾加上操作日誌,那麼除了上述的手動處理方式,也有很多種整體設計方案:
1. 使用Canal監聽資料庫記錄操作日誌
Canal應運而生,它通過偽裝成資料庫的從庫,讀取主庫發來的binlog,用來實現 資料庫增量訂閱和消費業務需求 。可以看我的這篇文章:
這個方式有點是和業務邏輯完全分離,缺點也很大,需要使用到MySQL的Binlog,向DBA申請就有點困難。如果涉及到修改第三方介面,那麼就無法監聽別人的資料庫了。所以呼叫RPC介面時,就需要額外的在業務程式碼中增加記錄程式碼,破壞了“和業務邏輯完全分離”這個基本原則,侷限性大。
2. 通過日誌檔案的方式記錄
log.info("訂單已經建立,訂單編號:{}", orderNo)
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", bizId = "#request.orderId", pipeline = DataPipelineEnum.QUEUE)
public Response<BaseResult> function(Request request) {
// 方法執行邏輯
}
我們可以在註解的操作日誌上記錄固定文案,這樣業務邏輯和業務程式碼可以做到解耦,讓我們的業務程式碼變得純淨起來。
美團的原文給出了註解記錄日誌的詳細架構設計方案,並且貼出了部分原始碼。但是文中並沒有完整的開源專案,由於自己也很感興趣,並且公司的業務正好也有類似需求,所以我花了點時間,實現了一版最簡易的版本,支援將操作日誌傳遞到訊息佇列中。
實戰:通過註解實現操作日誌的記錄
大樓不是一天建成的,美團部落格中描述的方案應該在公司內部已經非常成熟了,我也沒有那麼多精力一口氣吃成一個胖子,我們從最基礎的版本寫起。
我給自己的這個專案,或者說依賴包起名為log-record-starter,一方面遵循springboot-starter命名規範,一方面也表明專案的用處,記錄日誌。
開啟專案之前,先問問自己
Q:你這個依賴包,又是一個冗餘的造輪子吧?市面上這種東西是不是已經夠多了?
A:本著有現成輪子絕不造輪子的原則,我在Github和其他網站進行了一系列的相關搜尋,Github有幾個類似的實現專案,不過都以個人實現為主,沒有一個具有一定影響力的成熟專案。 基於我在自己的業務專案中擁有實際的場景需求,並且目前還沒有滿足我需求的現成可接入依賴,我才開始這個依賴包的程式碼編寫。
Q:我用了你這個依賴包,是不是很複雜?之後你不維護了的話,是不是坑我們這些吃螃蟹的?
A:依賴包的維護問題一直是一個大問題,本著最小依賴,儘量可擴充套件的原則。 本庫特點如下:
-
使用SpringBoot Starter,接入只需要簡單引入一個依賴。
-
通過Spring Spel表示式拿到引數,對你的業務邏輯沒有侵入性。
-
預設使用RabbitMq傳遞日誌訊息,日誌操作解耦。
-
之後會引入其他資料來源,例如Kafka等(畢竟還要給三歪的專案用,我沒有被三歪綁架,嗯,絕對沒有)。
好了,這就是我想說在前面的話。下面就是該專案的使用介紹和應用場景介紹。
Log-record-starter 一句話介紹
本專案支援使用者使用註解的方式從方法中獲取操作日誌,並推送到指定資料來源
只需要簡單的加上一個@OperationLog便可以將方法的引數,返回結果甚至是異常堆疊通過訊息佇列傳送出去,統一處理。
@OperationLog(bizType = "bizType", bizId = "#request.orderId", pipeline = DataPipelineEnum.QUEUE)
public Response<BaseResult> function(Request request) {
// 方法執行邏輯
}
使用方法
只需要簡單的三步:
第一步:SpringBoot專案中引入依賴
<dependency>
<groupId>cn.monitor4all</groupId>
<artifactId>log-record-starter</artifactId>
<version>1.0.0</version>
</dependency>
這裡先打斷一下,由於Maven公共倉庫,是全球唯一託管的,個人開發的專案要提交上去,需要複雜的稽核流程,我搞了一會沒搞定,就先將包傳到了Github Package上(實際就是Github的私有Maven庫),所以大家引入依賴後,是不會直接拉到包的,需要配置下你的Maven settings.xml檔案。( 之後我肯定想辦法發到公共倉庫,嗚嗚嗚~ )
配置很簡單,兩步,一步是去Github登入,到自己的Settings中,申請一個token,拿到一串字串。

第二步,找到你的settings.xml檔案,新增上:
activeProfiles>
<activeProfile>github</activeProfile>
</activeProfiles>
<profiles>
<profile>
<id>github</id>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2</url>
</repository>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/OWNER/REPOSITORY</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<servers>
<server>
<id>github</id>
<username>這裡填寫你的Github使用者名稱</username>
<password>這裡填寫你剛才申請的token</password>
</server>
</servers>
還搞不定的同學,這裡是Github官方中文教程:
https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry
重啟下你的IDEA,能看到下面這個,應該你的settings.xml生效了。

目前我的版本號是1.0.0,之後會更新,未來最新版本號在我倉庫查詢:
https://github.com/qqxx6661/logRecord
第二步:在Spring配置檔案中新增RabbitMq資料來源配置
在自己公司裡由於阿里封裝了自己的MQ叫做MetaQ,並沒有對外開源,所以這裡先接入了RabbitMQ,也算是比較通用,圖個方便。未來會接其他資料來源。RabbitMq的安裝在這裡不展開了,實在是不想把篇幅拉得太大,大家可以自行谷歌下,比如“Docker安裝RabbitMq”類似的文章,幾分鐘就可以設定安裝好。
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
第三步:在你自己的專案中,在需要記錄日誌的方法上,添加註解。
@OperationLog(bizType = "bizType", bizId = "#request.orderId", pipeline = DataPipelineEnum.QUEUE)
public Response<BaseResult> function(Request request) {
// 方法執行邏輯
}
-
(必填)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","2","3"],"testStr":"str"}, tag=operation)]
方法處理異常傳送訊息體:
[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是定義的訊息結構:
logId:生成的UUID
bizId:註解中傳遞的bizId
bizType:註解中傳遞的bizType
exception:若方法執行失敗,寫入執行的異常資訊
operateDate:操作執行的當前時間
success:方式是否執行成功
msg:註解中傳遞的tag
tag:註解中傳遞的tag
我還加上了重複註解的支援,可以在一個方法上同時加多個@OperationLog,下圖是最終使用效果,可以看到,有幾個@OperationLog,就能同時傳送多條日誌:

專案具體的實現原理和細節,放在下一篇文章詳細講。(肯定會填坑)
應用場景
以下羅列了一些實際的應用場景,包括我業務中實際使用,並且已經上線使用的場景。
一、特定操作記錄日誌:如文章最上面一張CRM系統的圖描述的那樣,在使用者進行了編輯操作後,拿到使用者操作的資料,執行日誌寫入。
二、特定操作觸發通知:由於我的業務是接手了好幾個倉庫,並且這幾個倉庫的操作串成了一條完成鏈路,我需要在鏈路的某個節點觸發給使用者的提醒,如果寫硬編碼也可以實現,但是遠不如在方法上使用註解傳送訊息來得方便。例如下方在下單方法呼叫後傳送訊息。
三、特定操作更新資料表:我的業務中,幾個系統互相吞吐資料,訂單的一部分資料存留在外部系統裡,我們最終目標想要將其中一個系統替代掉,所以需要攔截他們的資料,恰好幾個系統是使用LINK作為閘道器的,我們將資料請求攔截一層,並將攔截的方法使用該二方庫進行全部引數的傳送,將資料同步寫入我們自己的資料庫中,實現”雙寫“。
四、跨多應用資料聚合操作:和”三“類似,在多個應用中,如果需要做行為相同的業務邏輯,完全可以在各個系統中將資料傳送到同一個訊息佇列中,再進行統一處理。
- 如何使用註解優雅的記錄操作日誌
- 萬字長文 | 深入理解 OpenFeign 的架構原理
- InnoDB兩萬字詳解
- 領域驅動設計:從理論到實踐,一文帶你掌握DDD!
- 分散式一致性協議 Gossip 和 Redis 叢集原理解析
- 宇宙條一面:十道經典面試題解析
- Redis 使用 List 實現訊息佇列的利與弊
- 實戰Canal,完成資料同步(附程式碼)
- Redis的記憶體模型一篇帶走
- 面試被問Redis的持久化,和麵試官大戰幾個小時
- 併發程式設計-ReentrantReadWriteLock讀寫鎖詳解
- 年末跳槽面試之網路19問
- 今天聊一聊美團執行緒池 “動態更新”實現
- 面試官扎心一問:如何使用Redis實現電商系統的庫存扣減?
- 【面朝大廠】萬字 圖解 Redis,面試不用愁了!
- Java8 中的真的 Optional 很強大,你用對了嗎?
- 阿里面:淘寶七天自動確認收貨,可以怎麼實現?(附程式碼)
- Go時代來了
- 深度好文 | 計算機是怎麼識別你的程式碼的?
- 超硬核,Nacos實現原理詳細講解