我們開發的業務系統通常會提供給很多人使用,那在使用的過程中,日志系統變得非常重要。
日志系統記錄的用戶行為有以下的作用:
- 從系統用戶角度看:它展示了用戶自身的操作歷史和具體對象的變動歷史,便于用戶進行梳理
- 從系統管理員角度看:它可以記錄了所有用戶操作,便于我們定位異常行為
例如,在git的project操作中,我們就可以看到這樣的操作日志展示:

對于這樣的日志記錄,我們可以在相關記錄點添加對應的日志寫入代碼或者通過切面實現。然而,這樣的日志展示是相對簡單的,只是記錄了操作行為的種類。而有時我們需要記錄每個操作行為對操作對象引發的具體變動,例如展示出這樣的結果:

這給日志記錄帶來了不小的挑戰:
- 在一個系統中,可能涉及到多種對象(例如,學生、課程、老師),而每個對象的屬性是完全不一樣的
- 在一次操作中,可能改變了對象的一個或者多個屬性,這也使得我們極難逐一記錄
而今天,我們要介紹的是一套強大且易用的JAVA對象日志記錄系統,支持對象屬性變動的記錄與查詢。借助于它,我們可以方便地實現對象屬性變動的記錄與查詢。
1 系統簡介
ObjectLogger是一套強大且易用的對象日志記錄系統。它能夠將任意對象的變動日志記錄下來,并支持查詢??梢詰迷谟脩舨僮魅罩居涗?、對象屬性變更記錄等諸多場景中。
該系統具有以下特點:
- 一站整合:系統支持日志的記錄與查詢,開發者只需再開發前端界面即可使用。
- 完全獨立:與業務系統無耦合,可插拔使用,不影響主業務流程。
- 應用共享:系統可以同時供多個業務系統使用,互不影響。
- 簡單易用:服務端直接jar包啟動;業務系統有官方Maven插件支持。
- 自動解析:能自動解析對象的屬性變化,并支持富文本的前后對比。
- 便于擴展:支持自定義對象變動說明、屬性變動說明。支持更多對象屬性類型的擴展。
整個項目包含四個部分:
- ObjectLoggerClient:能夠集成到業務系統進行日志分析、發送jar包??梢詮腗aven官方倉庫引入該jar包。該模塊位于client子包下。
- ObjectLoggerServer:一個web服務,需要數據庫的支持。它能夠接收并保存ObjectLoggerClient發出的日志信息,支持日志的查詢操作。該模塊位于server子包下。
- react-object-logger:一個React前端組件,用于進行日志的前端展示。可以從npm官方倉庫引入該組件。該子項目位于react-object-logger。
- ObjectLoggerDemo:一個業務端集成ObjectLoggerClient的示例。該模塊位于demo子包下。
2 快速上手
2.1 創建數據庫
使用該項目的/server/database/init_data_table.sql文件初始化兩個數據表。
2.2 啟動Server
下載該項目下最新的Server服務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:數據庫類型。如果使用MySQL數據庫則為mysql;如果使用SqlServer數據庫則為sqlserver。
- db_address:數據庫連接地址。如果數據庫在本機則為127.0.0.1。
- db_name:數據庫名,該數據庫中需包含上一步初始化的兩個數據表。
- db_username:數據庫登錄用戶名。
- db_password:數據庫登錄密碼。
啟動jar成功后可以看到下面的界面:

使用瀏覽器訪問下面的頁面可以看到系統歡迎頁面:
http://127.0.0.1:12301/ObjectLoggerServer/
訪問上述地址可以看到下面的歡迎界面:

至此,ObjectLoggerServer系統已經搭建結束,可以接受業務系統的日志寫入和查詢操作。
3 業務系統接入
該部分講解如何配置業務系統來將業務系統中的對象變化通過ObjectLoggerClient分析,然后記錄到ObjectLoggerServer中。
這一部分的使用可以參照ObjectLoggerDemo項目,該項目給出了業務系統集成ObjectLoggerClient的詳細示例。ObjectLoggerDemo的制品包可以從/demo/target/ObjectLoggerDemo-*.jar獲得,無需其他配置直接運行java -jar ObjectLoggerDemo-*.jar便可以直接啟動該項目。
可以直接根據啟動頁面的提示訪問相關地址來體驗:

3.1 引入依賴包
在pom中增加下面的依賴:
<dependency> <groupId>com.github.yeecode.objectlogger</groupId> <artifactId>ObjectLoggerClient</artifactId> <version>{最新版本}</version> </dependency>
3.2 添加對ObjectLoggerClient中bean的自動注入
3.2.1 對于SpringBoot應用
在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應用
在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:指當前業務系統的應用名。以便于區分日志來源,實現同時支持多個業務系統
- yeecode.objectLogger.autoLogAttributes:是否對對象的所有屬性進行變更日志記錄
至此,業務系統的配置完成。已經實現了和ObjectLoggerServer端的對接。
4 日志查詢
系統運行后,可以通過http://127.0.0.1:12301/ObjectLoggerServer/log/query查詢系統中記錄的日志,并通過傳入參數對日志進行過濾。

通過這里,我們可以查詢下一步中寫入的日志。
5 日志展示
ObjectLogger有前端React組件react-object-logger支持,用于進行日志信息的展示。體驗頁面:react-object-logger 示例頁面
展示效果如圖:

感興趣的用戶可以前往react-object-logger進行了解。
同時也歡迎各位開發者開發面向其它前端技術棧的組件。
6 日志寫入
業務系統在任何需要進行日志記錄的類中引入LogClient。例如:
@Autowired private LogClient logClient;
6.1 簡單使用
直接將對象的零個、一個、多個屬性變化放入List<BaseAttributeModel>后調用logAttributes方法發出即可。List<BaseAttributeModel>置為null則表示此次對象無需要記錄的屬性變動。例如,業務應用中調用:
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 對象變動自動記錄
該功能可以自動完成新老對象的對比,并根據對比結果,將多個屬性變動一起寫入日志系統中。使用時,要確保傳入的新老對象屬于同一個類。
例如,業務系統這樣調用:
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
查詢到如下結果:
{ "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系統中的具體方式,例如修改命名等。
要想啟用這個功能,首先將配置中的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的內置類型,為BuiltinTypeHandler的值。默認為BuiltinTypeHandler.NORMAL。
- BuiltinTypeHandler.NORMAL:記錄屬性的新值和舊值,對比值為null
- BuiltinTypeHandler.RICHTEXT: 用戶富文本對比。記錄屬性值的新值和舊值,并將新舊值轉化為純文本后逐行對比差異,對比值中記錄差異
- extendedType:擴展屬性類型。使用ObjcetLogger時,用戶可以擴展某些字段的處理方式,此時,alias等信息均可以被用戶自主覆蓋。
8 屬性處理擴展
很多情況下,用戶希望能夠自主決定某些對象屬性的處理方式。例如,對于例子中Task對象的userId屬性,用戶可能想將其轉化為姓名后存入日志系統,從而使得日志系統與userId完全解耦。
ObjectLoggerClient完全支持這種情況,可以讓用戶自主決定某些屬性的日志記錄方式。要想實現這種功能,首先在需要進行擴展處理的屬性上為@LogTag的extendedType屬性賦予一個字符串值。例如:
@LogTag(alias = "UserId", extendedType = "userIdType") private int userId;
然后在業務系統中聲明一個Bean繼承BaseExtendedTypeHandler,作為自由擴展的鉤子。代碼如下:
@Service public class ExtendedTypeHandler implements BaseExtendedTypeHandler { @Override public BaseAttributeModel handleAttributeChange(String extendedType, String attributeName, String attributeAlias, Object oldValue, Object newValue) { // TODO } }
接下來,當ObjectLoggerClient處理到該屬性時,會將該屬性的相關信息傳入到擴展Bean的handleAttributeChange方法中,然后用戶可以自行處理。傳入的四個參數解釋如下:
- 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 總結
怎么樣,是不是有了這一套系統之后,再負責的日志系統都變得簡單起來。快收藏起來當作自己的秘技吧!
易哥,高級軟件架構師、網絡工程師、數據庫工程師、注冊電氣工程師。現從事軟件架構架構設計工作。
分享架構師知識! 歡迎關注我們,不錯過每期的原創干貨!