log-record正式版本發(fā)布:自定義函數(shù)、手動傳遞上下文 、本地監(jiān)聽支持
前言
之前寫了兩篇文章,來介紹我的log-record開源項目(優(yōu)雅記錄操作日志)是如何誕生的。
前文:
萌新寫開源01 | 如何使用注解優(yōu)雅的記錄操作日志
這兩周我又迭代了幾個版本,正式把自定義函數(shù),手動傳遞上下文,本地監(jiān)聽等基礎功能做了支持,并且重新給項目編了個易于人類閱讀的文檔。
這算是正兒八經(jīng)的一次介紹了,現(xiàn)在該庫已經(jīng)正式可以在Maven倉庫拉取了。
大家有興趣的話,建議多多使用體驗一下,給我一些反饋或者Pr,我會加油好好迭代項目。
倉庫地址:
https://github.com/qqxx6661/logRecord
項目背景
大家一定見過下圖的操作日志:

在代碼層面,如何優(yōu)雅的實現(xiàn)上面的日志記錄呢?
你可能會一下子想到最簡單的方式,通過封裝一個操作日志記錄類(例如LogUtil)。使用一個用戶修改配送地址的操作舉例:
String?template?=?"用戶%s修改了訂單的配送地址:從“%s”修改到“%s”"
LogUtil.log(orderNo,?String.format(tempalte,?"小明",?"金燦燦小區(qū)",?"銀盞盞小區(qū)"),??"小明")
這種方式會導致業(yè)務邏輯被記錄日志的代碼侵入,對于代碼的可讀性和可維護性來說是一個災難。
這個方式顯然不優(yōu)雅,讓我們試試使用注解:
@OperationLog(bizType?=?"addressChange",?bizId?=?"20211102001",?msg?=?"用戶?小明?修改了訂單的配送地址:從?金燦燦小區(qū)?修改到?銀盞盞小區(qū)")
public?Response?function(Request?request)?{
??//?業(yè)務執(zhí)行邏輯
}
可以看到,這樣日志的記錄被放到了注解,對業(yè)務代碼沒有了侵入。但是新的問題來了,我們該如何把訂單ID,用戶信息,數(shù)據(jù)庫里的舊地址,函數(shù)入?yún)⒌男碌刂穫鬟f給注解呢?
Spring的SpEL表達式(Spring Expression Language)可以幫助我們,通過引入SpEL表達式,我們可以獲取函數(shù)的入?yún)?。這樣我們就可以對上面的注解進行修改:
訂單ID:#request.orderId 新地址"銀盞盞小區(qū)":#request.newAddress
@OperationLog(bizType?=?"addressChange",?bizId?=?"#request.orderId",?msg?=?"'用戶?小明?修改了訂單的配送地址:從?金燦燦小區(qū)?修改到'?+?#request.newAddress")
public?Response?function(Request?request)?{
??//?業(yè)務執(zhí)行邏輯
}
這樣依賴,訂單ID和地址的新值就可以通過解析入?yún)討B(tài)獲取了,但是事情還沒有結束,我們的用戶信息,以及老的配送地址,是需要業(yè)務代碼去獲取的,入?yún)⒗锊⒉粫@些數(shù)據(jù)。
解決方案也不是沒有,我們創(chuàng)建一個日志上下文LogRecordContext,讓用戶手動傳遞代碼中計算出來的值,再交給SpEL解析。
@OperationLog(bizType?=?"addressChange",?bizId?=?"#request.orderId",?msg?=?"'用戶'?+?#userName +?'修改了訂單的配送地址:從'?+?#oldAddress +?'修改到'?+?#request.newAddress")
public?Response?function(Request?request)?{
??//?業(yè)務執(zhí)行邏輯
??...
??//?手動傳遞日志上下文:用戶信息?地址舊值
??LogRecordContext.putVariables("userName",?queryUserName(request.getUserId()));
??LogRecordContext.putVariables("oldAddress",?queryOldAddress(request.getOrderId()));
}
什么?你說這不就又侵入了業(yè)務邏輯了么?
確實是的,雖然能用,但是對于有“強迫癥”的同學,這樣的實現(xiàn)還是不夠優(yōu)雅,接下來我們用自定義函數(shù),解決這個問題。
SpEL支持在表達式中傳入用戶自定義函數(shù),我們將queryUserName和queryOldAddress這兩個函數(shù)傳遞給SpEL,SpEL在解析表達式時,會自動執(zhí)行對應函數(shù)。
最終,我們的注解變成了這樣,并且最終記錄了日志:
@OperationLog(bizType?=?"addressChange",?bizId?=?"#request.orderId",?msg?=?"'用戶'?+?#queryUserName(#request.userId)?+?'修改了訂單的配送地址:從'?+?#oldAddress +?'修改到'?+?#queryOldAddress(#request.orderId)")
public?Response?function(Request?request)?{
??//?業(yè)務執(zhí)行邏輯
}
用戶 小明 修改了訂單的配送地址:從 金燦燦小區(qū) 修改到 銀盞盞小區(qū)
項目介紹
本倉庫幫助你通過注解優(yōu)雅地聚合項目中的操作日志,對業(yè)務代碼無侵入。
此外,你可以方便地將所有日志推送到下列數(shù)據(jù)管道:
本地監(jiān)聽處理 發(fā)送至RabbitMQ 發(fā)送至RocketMQ
日志內(nèi)包含:
logId:生成的UUID
bizId:業(yè)務唯一ID
bizType:業(yè)務類型
exception:函數(shù)執(zhí)行失敗時寫入異常信息
operateDate:操作執(zhí)行時間
success:函數(shù)是否執(zhí)行成功
msg:注解中傳遞的msg(支持JSON)
tag:用戶自定義標簽
returnStr:?方法執(zhí)行成功后的返回值(JSON化)
本項目特點:
方便接入:使用Spring Boot Starter實現(xiàn),用戶直接在pom.xml引入依賴,快速接入 SpEL解析:直接寫表達式解析入?yún)?/section> 自定義上下文:支持手動傳遞鍵值對,通過SpEL進行解析 自定義函數(shù):支持注冊自定義函數(shù),通過SpEL進行解析
使用方法
接入方式
只需要簡單的三步:
第一步: SpringBoot項目中引入依賴(最新版本號請查閱Maven公共倉庫)
????cn.monitor4all
????log-record-starter
????1.0.4
第二步: 添加數(shù)據(jù)源配置
支持推送日志數(shù)據(jù)至:
本地應用監(jiān)聽 RabbitMQ RocketMQ
1. 本地應用監(jiān)聽
若只需要在同一應用內(nèi)處理日志信息,只需要繼承抽象類CustomLogListener,便可對日志進行處理。
@Slf4j
@Component
public?class?TestCustomLogListener?extends?CustomLogListener?{
????@Override
????public?void?createLog(LogDTO?logDTO)?throws?Exception?{
????????log.info("TestCustomLogListener?本地接收到日志?[{}]",?logDTO);
????}
}
2. RabbitMQ
配置好RabbitMQ的發(fā)送者
log-record.data-pipeline=rabbitMq
log-record.rabbit-mq-properties.host=localhost
log-record.rabbit-mq-properties.port=5672
log-record.rabbit-mq-properties.username=admin
log-record.rabbit-mq-properties.password=xxxxxx
log-record.rabbit-mq-properties.queue-name=logRecord
log-record.rabbit-mq-properties.routing-key=
log-record.rabbit-mq-properties.exchange-name=logRecord
3. RocketMQ
配置好RocketMQ的發(fā)送者
log-record.data-pipeline=rocketMq
log-record.rocket-mq-properties.topic=logRecord
log-record.rocket-mq-properties.tag=
log-record.rocket-mq-properties.group-name=logRecord
log-record.rocket-mq-properties.namesrv-addr=localhost:9876
第三步: 在你自己的項目中,在需要記錄日志的方法上,添加@OperationLog注解。
@OperationLog(bizType?=?"orderCreate",?bizId?=?"#request.orderId",?msg?=?"#request")
public?Response ?function(Request?request)?{
??//?業(yè)務執(zhí)行邏輯
}
(必填)bizType:業(yè)務類型 (必填)bizId:唯一業(yè)務ID(支持SpEL表達式) (非必填)msg:需要傳遞的其他數(shù)據(jù)(支持SpEL表達式) (非必填)tag:自定義標簽
自定義傳遞上下文
直接引入類LogRecordContext,放入鍵值對。
@OperationLog(bizType?=?"addressChange",?bizId?=?"#request.orderId",?msg?=?"'用戶'?+?#userName +?'修改了訂單的配送地址:從'?+?#oldAddress +?'修改到'?+?#request.newAddress")
public?Response?function(Request?request)?{
??//?業(yè)務執(zhí)行邏輯
??...
??//?手動傳遞日志上下文:用戶信息?地址舊值
??LogRecordContext.putVariables("userName",?queryUserName(request.getUserId()));
??LogRecordContext.putVariables("oldAddress",?queryOldAddress(request.getOrderId()));
}
自定義函數(shù)
將@LogRecordFunc注解申明在需要注冊到SpEL的自定義函數(shù)上。
注意,需要在類上也聲明@LogRecordFunc,否則無法找到該函數(shù)。
@LogRecordFunc
public?class?MyFuction?{
????private?static?TestService?testService;
????@LogRecordFunc
????public?static?String?testFunc(String?str)?{
????????if?(testService?==?null)?{
????????????testService?=?SpringContextUtils.getBean(TestService.class);
????????}
????????return?testService.testServiceFunc2(str);
????}
}
@OperationLog(bizType?=?"addressChange",?bizId?=?"#request.orderId",?msg?=?"'用戶'?+?#queryUserName(#request.userId)?+?'修改了訂單的配送地址:從'?+?#oldAddress +?'修改到'?+?#queryOldAddress(#request.orderId)")
public?Response?function(Request?request)?{
??//?業(yè)務執(zhí)行邏輯
}
重復注解
@OperationLog(bizId?=?"#testClass.testId",?bizType?=?"testType1",?msg?=?"#testFunc(#testClass.testId)")
@OperationLog(bizId?=?"#testClass.testId",?bizType?=?"testType2",?msg?=?"#testFunc(#testClass.testId)")
@OperationLog(bizId?=?"#testClass.testId",?bizType?=?"testType3",?msg?=?"'用戶將舊值'?+?#old?+?'更改為新值'?+?#testClass.testStr")
我們還加上了重復注解的支持,可以在一個方法上同時加多個@OperationLog,下圖是最終使用效果:

實現(xiàn)原理
由于采用的是SpringBoot Starter方式,會自動掃描到依賴包中的類,并自動通過Spring進行配置和管理。
該注解通過在切面中解析SpEL參數(shù),將數(shù)據(jù)發(fā)往數(shù)據(jù)源。發(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)]
應用場景
以下羅列了一些實際的應用場景,包括我業(yè)務中實際使用,并且已經(jīng)上線使用的場景。
一、操作日志:如最上面一張CRM系統(tǒng)的圖描述的那樣,在用戶進行了編輯操作后,拿到用戶操作的數(shù)據(jù),執(zhí)行日志寫入。
二、通知觸發(fā):由于我的業(yè)務是接手了好幾個倉庫,并且這幾個倉庫的操作串成了一條完成鏈路,我需要在鏈路的某個節(jié)點觸發(fā)給用戶的提醒,如果寫硬編碼也可以實現(xiàn),但是遠不如在方法上使用注解發(fā)送消息來得方便。例如下方在下單方法調用后發(fā)送消息。

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

四、跨應用數(shù)據(jù)聚合:和”三“類似,在多個應用中,如果需要做行為相同的業(yè)務邏輯,完全可以在各個系統(tǒng)中將數(shù)據(jù)發(fā)送到同一個消息隊列中,再進行統(tǒng)一處理。
附錄:Demo
最后,肯定有小伙伴希望有一個完整的客戶端Demo,這就奉上!
完整Demo項目:
https://github.com/qqxx6661/systemLog
- END -我是目前在阿里搬磚的工程師蠻三刀醬。
持續(xù)的創(chuàng)作離不開你的點贊和轉發(fā)分享!
