我們開發(fā)的業(yè)務(wù)系統(tǒng)通常會提供給很多人使用,那在使用的過程中,日志系統(tǒng)變得非常重要。
日志系統(tǒng)記錄的用戶行為有以下的作用:
- 從系統(tǒng)用戶角度看:它展示了用戶自身的操作歷史和具體對象的變動歷史,便于用戶進行梳理
- 從系統(tǒng)管理員角度看:它可以記錄了所有用戶操作,便于我們定位異常行為
例如,在git的project操作中,我們就可以看到這樣的操作日志展示:
對于這樣的日志記錄,我們可以在相關(guān)記錄點添加對應(yīng)的日志寫入代碼或者通過切面實現(xiàn)。然而,這樣的日志展示是相對簡單的,只是記錄了操作行為的種類。而有時我們需要記錄每個操作行為對操作對象引發(fā)的具體變動,例如展示出這樣的結(jié)果:
這給日志記錄帶來了不小的挑戰(zhàn):
- 在一個系統(tǒng)中,可能涉及到多種對象(例如,學(xué)生、課程、老師),而每個對象的屬性是完全不一樣的
- 在一次操作中,可能改變了對象的一個或者多個屬性,這也使得我們極難逐一記錄
而今天,我們要介紹的是一套強大且易用的JAVA對象日志記錄系統(tǒng),支持對象屬性變動的記錄與查詢。借助于它,我們可以方便地實現(xiàn)對象屬性變動的記錄與查詢。
1 系統(tǒng)簡介
ObjectLogger是一套強大且易用的對象日志記錄系統(tǒng)。它能夠?qū)⑷我鈱ο蟮淖儎尤罩居涗浵聛恚⒅С植樵儭?梢詰?yīng)用在用戶操作日志記錄、對象屬性變更記錄等諸多場景中。
該系統(tǒng)具有以下特點:
- 一站整合:系統(tǒng)支持日志的記錄與查詢,開發(fā)者只需再開發(fā)前端界面即可使用。
- 完全獨立:與業(yè)務(wù)系統(tǒng)無耦合,可插拔使用,不影響主業(yè)務(wù)流程。
- 應(yīng)用共享:系統(tǒng)可以同時供多個業(yè)務(wù)系統(tǒng)使用,互不影響。
- 簡單易用:服務(wù)端直接jar包啟動;業(yè)務(wù)系統(tǒng)有官方Maven插件支持。
- 自動解析:能自動解析對象的屬性變化,并支持富文本的前后對比。
- 便于擴展:支持自定義對象變動說明、屬性變動說明。支持更多對象屬性類型的擴展。
整個項目包含四個部分:
- ObjectLoggerClient:能夠集成到業(yè)務(wù)系統(tǒng)進行日志分析、發(fā)送jar包。可以從Maven官方倉庫引入該jar包。該模塊位于client子包下。
- ObjectLoggerServer:一個web服務(wù),需要數(shù)據(jù)庫的支持。它能夠接收并保存ObjectLoggerClient發(fā)出的日志信息,支持日志的查詢操作。該模塊位于server子包下。
- react-object-logger:一個React前端組件,用于進行日志的前端展示。可以從npm官方倉庫引入該組件。該子項目位于react-object-logger。
- ObjectLoggerDemo:一個業(yè)務(wù)端集成ObjectLoggerClient的示例。該模塊位于demo子包下。
2 快速上手
2.1 創(chuàng)建數(shù)據(jù)庫
使用該項目的/server/database/init_data_table.sql文件初始化兩個數(shù)據(jù)表。
2.2 啟動Server
下載該項目下最新的Server服務(wù)jar包,地址為/server/target/ObjectLoggerServer-*.jar。
啟動下載的jar包。
java -jar ObjectLoggerServer-*.jar --spring.datasource.url=jdbc:{db}://{db_address}/{db_name} --spring.datasource.username={db_username} --spring.datasource.password={db_password}
上述命令中的用戶配置項說明如下:
- db:數(shù)據(jù)庫類型。如果使用MySQL數(shù)據(jù)庫則為mysql;如果使用SqlServer數(shù)據(jù)庫則為sqlserver。
- db_address:數(shù)據(jù)庫連接地址。如果數(shù)據(jù)庫在本機則為127.0.0.1。
- db_name:數(shù)據(jù)庫名,該數(shù)據(jù)庫中需包含上一步初始化的兩個數(shù)據(jù)表。
- db_username:數(shù)據(jù)庫登錄用戶名。
- db_password:數(shù)據(jù)庫登錄密碼。
啟動jar成功后可以看到下面的界面:
使用瀏覽器訪問下面的頁面可以看到系統(tǒng)歡迎頁面:
http://127.0.0.1:12301/ObjectLoggerServer/
訪問上述地址可以看到下面的歡迎界面:
至此,ObjectLoggerServer系統(tǒng)已經(jīng)搭建結(jié)束,可以接受業(yè)務(wù)系統(tǒng)的日志寫入和查詢操作。
3 業(yè)務(wù)系統(tǒng)接入
該部分講解如何配置業(yè)務(wù)系統(tǒng)來將業(yè)務(wù)系統(tǒng)中的對象變化通過ObjectLoggerClient分析,然后記錄到ObjectLoggerServer中。
這一部分的使用可以參照ObjectLoggerDemo項目,該項目給出了業(yè)務(wù)系統(tǒng)集成ObjectLoggerClient的詳細示例。ObjectLoggerDemo的制品包可以從/demo/target/ObjectLoggerDemo-*.jar獲得,無需其他配置直接運行java -jar ObjectLoggerDemo-*.jar便可以直接啟動該項目。
可以直接根據(jù)啟動頁面的提示訪問相關(guān)地址來體驗:
3.1 引入依賴包
在pom中增加下面的依賴:
<dependency>
<groupId>com.github.yeecode.objectlogger</groupId>
<artifactId>ObjectLoggerClient</artifactId>
<version>{最新版本}</version>
</dependency>
3.2 添加對ObjectLoggerClient中bean的自動注入
3.2.1 對于SpringBoot應(yīng)用
在SpringBoot的啟動類前添加@ComponentScan注解,并在basePackages中增加ObjectLoggerClient的包地址:com.github.yeecode.objectlogger,如:
@SpringBootApplication
@ComponentScan(basePackages={"{your_beans_root}","com.github.yeecode.objectlogger"})
public class MyBootAppApplication {
public static void main(String[] args) {
// 省略其他代碼
}
}
3.2.2 對于Spring應(yīng)用
在applicationContext.xml增加對ObjectLoggerClient包地址的掃描:
<context:component-scan base-package="com.github.yeecode.objectlogger"> </context:component-scan>
3.3 完成配置
在application.properties中增加:
yeecode.objectLogger.serverAddress=http://{ObjectLoggerServer_address}
yeecode.objectLogger.businessAppName={your_app_name}
yeecode.objectLogger.autoLogAttributes=true
- ObjectLoggerServer_address:屬性指向上一步的ObjectLoggerServer的部署地址,例如:127.0.0.1:12301
- your_app_name:指當(dāng)前業(yè)務(wù)系統(tǒng)的應(yīng)用名。以便于區(qū)分日志來源,實現(xiàn)同時支持多個業(yè)務(wù)系統(tǒng)
- yeecode.objectLogger.autoLogAttributes:是否對對象的所有屬性進行變更日志記錄
至此,業(yè)務(wù)系統(tǒng)的配置完成。已經(jīng)實現(xiàn)了和ObjectLoggerServer端的對接。
4 日志查詢
系統(tǒng)運行后,可以通過http://127.0.0.1:12301/ObjectLoggerServer/log/query查詢系統(tǒng)中記錄的日志,并通過傳入?yún)?shù)對日志進行過濾。
通過這里,我們可以查詢下一步中寫入的日志。
5 日志展示
ObjectLogger有前端React組件react-object-logger支持,用于進行日志信息的展示。體驗頁面:react-object-logger 示例頁面
展示效果如圖:
感興趣的用戶可以前往react-object-logger進行了解。
同時也歡迎各位開發(fā)者開發(fā)面向其它前端技術(shù)棧的組件。
6 日志寫入
業(yè)務(wù)系統(tǒng)在任何需要進行日志記錄的類中引入LogClient。例如:
@Autowired private LogClient logClient;
6.1 簡單使用
直接將對象的零個、一個、多個屬性變化放入List<BaseAttributeModel>后調(diào)用logAttributes方法發(fā)出即可。List<BaseAttributeModel>置為null則表示此次對象無需要記錄的屬性變動。例如,業(yè)務(wù)應(yīng)用中調(diào)用:
logClient.logAttributes( "CleanRoomTask", 5, "Tom", "add", "Add New Task", "Create a cleanRoomTask", "taskName is :Demo Task", null);
在ObjectLoggerServer中使用如下查詢條件:
http://127.0.0.1:12301/ObjectLoggerServer/log/query?appName=ObjectLoggerDemo&objectName=CleanRoomTask&objectId=5
查詢到日志:
{
"respMsg": "SUCCESS",
"respData": [
{
"id": 1,
"appName": "ObjectLoggerDemo",
"objectName": "CleanRoomTask",
"objectId": 5,
"operator": "Jone",
"operationName": "start",
"operationAlias": "Start a Task",
"extraWords": "Begin to clean room...",
"comment": "Come on and start cleaning up.",
"operationTime": "2019-07-04T06:53:40.000+0000",
"attributeModelList": [
{
"attributeType": "NORMAL",
"attributeName": "status",
"attributeAlias": "Status",
"oldValue": "TODO",
"newValue": "DOING",
"diffValue": null,
"id": 1,
"operationId": 1
}
]
}
],
"respCode": "1000"
}
6.2 對象變動自動記錄
該功能可以自動完成新老對象的對比,并根據(jù)對比結(jié)果,將多個屬性變動一起寫入日志系統(tǒng)中。使用時,要確保傳入的新老對象屬于同一個類。
例如,業(yè)務(wù)系統(tǒng)這樣調(diào)用:
CleanRoomTask oldTask = new CleanRoomTask();
oldTask.setId(5);
oldTask.setTaskName("Demo Task");
oldTask.setStatus("TODO");
oldTask.setDescription("Do something...");
CleanRoomTask newTask = new CleanRoomTask();
newTask.setId(5);
newTask.setTaskName("Demo Task");
newTask.setStatus("DOING");
newTask.setDescription("The main job is to clean the floor.");
newTask.setAddress("Sunny Street");
newTask.setRoomNumber(702);
logClient.logObject(
cleanRoomTask.getId().toString(),
"Tom",
"update",
"Update a Task",
null,
null,
oldTask,
newTask);
則我們可以使用下面查詢條件:
http://127.0.0.1:12301/ObjectLoggerServer/log/query?appName=ObjectLoggerDemo&objectName=CleanRoomTask&objectId=5
查詢到如下結(jié)果:
{
"respMsg": "SUCCESS",
"respData": [
{
"id": 4,
"appName": "ObjectLoggerDemo",
"objectName": "CleanRoomTask",
"objectId": 5,
"operator": "Tom",
"operationName": "update",
"operationAlias": "Update a Task",
"extraWords": null,
"comment": null,
"operationTime": "2019-07-04T07:22:59.000+0000",
"attributeModelList": [
{
"attributeType": "NORMAL",
"attributeName": "roomNumber",
"attributeAlias": "roomNumber",
"oldValue": "",
"newValue": "702",
"diffValue": null,
"id": 5,
"operationId": 4
},
{
"attributeType": "NORMAL",
"attributeName": "address",
"attributeAlias": "address",
"oldValue": "",
"newValue": "Sunny Street",
"diffValue": null,
"id": 6,
"operationId": 4
},
{
"attributeType": "NORMAL",
"attributeName": "status",
"attributeAlias": "Status",
"oldValue": "TODO",
"newValue": "DOING",
"diffValue": null,
"id": 7,
"operationId": 4
},
{
"attributeType": "TEXT",
"attributeName": "description",
"attributeAlias": "Description",
"oldValue": "Do something...",
"newValue": "The main job is to clean the floor.",
"diffValue": "Line 1<br/> -: <del> Do something... </del> <br/> +: <u> The main job is to clean the floor. </u> <br/>",
"id": 8,
"operationId": 4
}
]
}
],
"respCode": "1000"
}
7 對象屬性過濾
有些對象的屬性的變動不需要進行日志記錄,例如updateTime、hashCode等。ObjectLoggerClient支持對對象的屬性進行過濾,只追蹤我們感興趣的屬性。
并且,對于每個屬性我們可以更改其記錄到ObjectLoggerClient系統(tǒng)中的具體方式,例如修改命名等。
要想啟用這個功能,首先將配置中的yeecode.objectLogger.autoLogAttributes改為false。
yeecode.objectLogger.autoLogAttributes=true
然后在需要進行變化日志記錄的屬性上增加@LogTag注解。凡是沒有增加該注解的屬性在日志記錄時會被自動跳過。
例如,注解配置如下則id字段的變動將被忽略。
private Integer id; @LogTag private String taskName; @LogTag(alias = "UserId", extendedType = "userIdType") private int userId; @LogTag(alias = "Status") private String status; @LogTag(alias = "Description", builtinType = BuiltinTypeHandler.TEXT) private String description;
該注解屬性介紹如下:
- alias:屬性別名。默認情況下會將屬性名寫入。
- builtinType:ObjectLoggerClient的內(nèi)置類型,為BuiltinTypeHandler的值。默認為BuiltinTypeHandler.NORMAL。
- BuiltinTypeHandler.NORMAL:記錄屬性的新值和舊值,對比值為null
- BuiltinTypeHandler.RICHTEXT: 用戶富文本對比。記錄屬性值的新值和舊值,并將新舊值轉(zhuǎn)化為純文本后逐行對比差異,對比值中記錄差異
- extendedType:擴展屬性類型。使用ObjcetLogger時,用戶可以擴展某些字段的處理方式,此時,alias等信息均可以被用戶自主覆蓋。
8 屬性處理擴展
很多情況下,用戶希望能夠自主決定某些對象屬性的處理方式。例如,對于例子中Task對象的userId屬性,用戶可能想將其轉(zhuǎn)化為姓名后存入日志系統(tǒng),從而使得日志系統(tǒng)與userId完全解耦。
ObjectLoggerClient完全支持這種情況,可以讓用戶自主決定某些屬性的日志記錄方式。要想實現(xiàn)這種功能,首先在需要進行擴展處理的屬性上為@LogTag的extendedType屬性賦予一個字符串值。例如:
@LogTag(alias = "UserId", extendedType = "userIdType") private int userId;
然后在業(yè)務(wù)系統(tǒng)中聲明一個Bean繼承BaseExtendedTypeHandler,作為自由擴展的鉤子。代碼如下:
@Service
public class ExtendedTypeHandler implements BaseExtendedTypeHandler {
@Override
public BaseAttributeModel handleAttributeChange(String extendedType, String attributeName, String attributeAlias, Object oldValue, Object newValue) {
// TODO
}
}
接下來,當(dāng)ObjectLoggerClient處理到該屬性時,會將該屬性的相關(guān)信息傳入到擴展Bean的handleAttributeChange方法中,然后用戶可以自行處理。傳入的四個參數(shù)解釋如下:
- extendedType:擴展類型值,即@LogTag注解的extendedType值。本示例中為userIdType。
- attributeName:屬性名。本示例中為userId。
- attributeAlias:屬性別名,@LogTag注解的alias值。本示例中為UserId。
- oldValue:該屬性的舊值。
- newValue:該屬性的新值。
例如我們可以采用如下的方式處理userIdType屬性:
@Service
public class ExtendedTypeHandler implements BaseExtendedTypeHandler {
@Override
public BaseAttributeModel handleAttributeChange(String extendedType, String attributeName, String attributeAlias, Object oldValue, Object newValue) {
BaseAttributeModel baseAttributeModel = new BaseAttributeModel();
if (extendedType.equals("userIdType")) {
baseAttributeModel.setOldValue("USER_" + oldValue);
baseAttributeModel.setNewValue("USER_" + newValue);
baseAttributeModel.setDiffValue(oldValue + "->" + newValue);
}
return baseAttributeModel;
}
}
9 總結(jié)
怎么樣,是不是有了這一套系統(tǒng)之后,再負責(zé)的日志系統(tǒng)都變得簡單起來。快收藏起來當(dāng)作自己的秘技吧!
易哥,高級軟件架構(gòu)師、網(wǎng)絡(luò)工程師、數(shù)據(jù)庫工程師、注冊電氣工程師。現(xiàn)從事軟件架構(gòu)架構(gòu)設(shè)計工作。
分享架構(gòu)師知識! 歡迎關(guān)注我們,不錯過每期的原創(chuàng)干貨!






