架構(gòu)師必看!操作日志系統(tǒng)搭建秘技
在Java開發(fā)中,我們經(jīng)常會(huì)遇到一個(gè)棘手的問題:記錄用戶的操作行為。
某些操作是相對(duì)簡(jiǎn)單的,我們可以逐條記錄。但是某些操作行為卻很難記錄,例如編輯操作。在某一次操作中,用戶可能編輯了對(duì)象A的幾個(gè)屬性,而下一次操作中用戶可能編輯了對(duì)象B的幾個(gè)屬性。如果我們針對(duì)對(duì)象A、對(duì)象B的屬性變化分別進(jìn)行記錄,則整個(gè)操作十分復(fù)雜。而且,會(huì)與業(yè)務(wù)操作高度耦合。
而今天我們介紹的是一個(gè)叫ObjectLogger的系統(tǒng),它是一個(gè)強(qiáng)大且易用的Java對(duì)象日志記錄系統(tǒng),能夠分析任何對(duì)象的屬性變化,實(shí)現(xiàn)對(duì)象變化的記錄與查詢。
因此,它可以應(yīng)用在用戶操作日志記錄、對(duì)象屬性變更記錄等諸多場(chǎng)景中。簡(jiǎn)單易用,實(shí)為利器。
基于它,我們可以很方便地實(shí)現(xiàn)下面的效果。

該系統(tǒng)為github開源項(xiàng)目,地址為:https://github.com/yeecode/ObjectLogger
下面我們簡(jiǎn)單介紹下該系統(tǒng)。基于它,我們可以非常方便地搭建一套日志記錄系統(tǒng)。
1 系統(tǒng)特點(diǎn)
該系統(tǒng)具有以下特點(diǎn):
- 一站整合:系統(tǒng)支持日志的記錄與查詢,開發(fā)者只需再開發(fā)前端界面即可使用。
- 完全獨(dú)立:與業(yè)務(wù)系統(tǒng)無耦合,可插拔使用,不影響主業(yè)務(wù)流程。
- 應(yīng)用共享:系統(tǒng)可以同時(shí)供多個(gè)業(yè)務(wù)系統(tǒng)使用,互不影響。
- 簡(jiǎn)單易用:服務(wù)端直接jar包啟動(dòng);業(yè)務(wù)系統(tǒng)有官方Maven插件支持。
- 自動(dòng)解析:能自動(dòng)解析對(duì)象的屬性變化,并支持富文本的前后對(duì)比。
- 便于擴(kuò)展:支持自定義對(duì)象變動(dòng)說明、屬性變動(dòng)說明。支持更多對(duì)象屬性類型的擴(kuò)展。
2 快速上手
2.1 創(chuàng)建數(shù)據(jù)庫(kù)
使用該項(xiàng)目的/server/database/init_data_table.sql文件初始化兩個(gè)數(shù)據(jù)表。
2.2 啟動(dòng)Server
下載該項(xiàng)目下***的Server服務(wù)jar包,地址為/server/target/ObjectLogger-*.jar。
啟動(dòng)下載的jar包。
java -jar ObjectLogger-*.jar --spring.datasource.driver-class-name={db_driver} --spring.datasource.url=jdbc:{db}://{db_address}/{db_name} --spring.datasource.username={db_username} --spring.datasource.password={db_password}
上述命令中的用戶配置項(xiàng)說明如下:
- db_driver:數(shù)據(jù)庫(kù)驅(qū)動(dòng)。如果使用MySql數(shù)據(jù)庫(kù)則為com.mysql.jdbc.Driver;如果使用SqlServer數(shù)據(jù)庫(kù)則為com.microsoft.sqlserver.jdbc.SQLServerDriver。
- db:數(shù)據(jù)庫(kù)類型。如果使用MySql數(shù)據(jù)庫(kù)則為mysql;如果使用SqlServer數(shù)據(jù)庫(kù)則為sqlserver。
- db_address:數(shù)據(jù)庫(kù)連接地址。如果數(shù)據(jù)庫(kù)在本機(jī)則為127.0.0.1。
- db_name:數(shù)據(jù)庫(kù)名,該數(shù)據(jù)庫(kù)中需包含上一步初始化的兩個(gè)數(shù)據(jù)表。
- db_username:數(shù)據(jù)庫(kù)登錄用戶名。
- db_password:數(shù)據(jù)庫(kù)登錄密碼。
啟動(dòng)jar包后,系統(tǒng)默認(rèn)的服務(wù)地址為:
http://127.0.0.1:8080/ObjectLogger/
訪問上述地址可以看到下面的歡迎界面:

至此,ObjectLogger系統(tǒng)已經(jīng)搭建結(jié)束,可以接受業(yè)務(wù)系統(tǒng)的日志寫入和查詢操作。
3 業(yè)務(wù)系統(tǒng)接入
該部分講解如何配置業(yè)務(wù)系統(tǒng)來將業(yè)務(wù)系統(tǒng)中的對(duì)象變化記錄到ObjectLogger中。
3.1 引入依賴包
在pom中增加下面的依賴:
- <dependency>
- <groupId>com.github.yeecode.objectLogger</groupId>
- <artifactId>ObjectLoggerClient</artifactId>
- <version>{***版本}</version>
- </dependency>
3.2 添加對(duì)ObjectLoggerClient中bean的自動(dòng)注入
3.2.1 對(duì)于SpringBoot應(yīng)用
在SpringBoot的啟動(dòng)類前添加@ComponentScan注解,并在basePackages中增加ObjectLoggerClient的包地址:com.github.yeecode.objectLoggerClient,如:
- @SpringBootApplication
- @ComponentScan(basePackages={"{your_beans_root}","com.github.yeecode.objectLogger"})
- public class MyBootAppApplication {
- public static void main(String[] args) {
- // 省略其他代碼
- }
- }
3.2.2 對(duì)于Spring應(yīng)用
在applicationContext.xml增加對(duì)ObjectLoggerClient包地址的掃描:
- <context:component-scan base-package="com.github.yeecode.objectLoggerClient">
- </context:component-scan>
3.3 完成配置
在application.properties中增加:
- object.logger.add.log.api=http://{ObjectLogger_address}/ObjectLogger/log/add
- object.logger.appName={your_app_name}
- object.logger.autoLog=true
- ObjectLogger_address:屬性指向上一步的ObjectLogger的部署地址,例如:127.0.0.1:8080
- your_app_name:指當(dāng)前業(yè)務(wù)系統(tǒng)的應(yīng)用名。以便于區(qū)分日志來源,實(shí)現(xiàn)同時(shí)支持多個(gè)業(yè)務(wù)系統(tǒng)
- object.logger.autoLog:是否對(duì)對(duì)象的所有屬性進(jìn)行變更日志記錄
至此,業(yè)務(wù)系統(tǒng)的配置完成。已經(jīng)實(shí)現(xiàn)了和ObjectLogger的Server端的對(duì)接。
4 日志查詢
系統(tǒng)運(yùn)行后,可以通過/ObjectLogger/log/query查詢系統(tǒng)中記錄的日志,并通過傳入?yún)?shù)對(duì)日志進(jìn)行過濾。

通過這里,我們可以查詢下一步中寫入的日志。
5 日志寫入
業(yè)務(wù)系統(tǒng)在任何需要進(jìn)行日志記錄的類中引入LogClient。例如:
- @Autowired
- private LogClient logClient;
5.1 簡(jiǎn)單使用
直接將對(duì)象的零個(gè)、一個(gè)、多個(gè)屬性變化放入actionItemModelList中發(fā)出即可。actionItemModelList置為null則表示此次對(duì)象無需要記錄的屬性變動(dòng)。例如,業(yè)務(wù)應(yīng)用中調(diào)用:
- logClient.sendLogForItems("TaskModel",5,"actor name","addTask","add Task","via web page","some comments",null);
在ObjectLogger中使用如下查詢條件:
- http://{your_ObjectLogger_address}/ObjectLogger/log/query?appName=myBootApp&objectName=TaskModel&objectId=5
查詢到日志:
- {
- "respMsg": "成功",
- "respData": [
- {
- "id": 16,
- "appName": "myBootApp",
- "objectName": "TaskModel",
- "objectId": 5,
- "actor": "actor name",
- "action": "addTask",
- "actionName": "add Task",
- "extraWords": "via web page",
- "comment": "some comments",
- "actionTime": "2019-04-10T10:56:15.000+0000",
- "actionItemModelList": []
- }
- ],
- "respCode": "1000"
- }
5.2 對(duì)象變動(dòng)自動(dòng)記錄
該功能可以自動(dòng)完成新老對(duì)象的對(duì)比,并根據(jù)對(duì)比結(jié)果,將多個(gè)屬性變動(dòng)一起寫入日志系統(tǒng)中。使用時(shí),要確保傳入的新老對(duì)象屬于同一個(gè)類。
例如,業(yè)務(wù)系統(tǒng)這樣調(diào)用:
- TaskModel oldTaskModel = new TaskModel();
- oldTaskModel.setId(9);
- oldTaskModel.setTaskName("oldName");
- oldTaskModel.setUserId(3);
- oldTaskModel.setDescription(" <p>the first line</p>
- " +
- " <p>the second line</p>
- " +
- " <p>the 3th line</p>");
- TaskModel newTaskModel = new TaskModel();
- newTaskModel.setId(9);
- newTaskModel.setTaskName("newName");
- newTaskModel.setUserId(5);
- newTaskModel.setDescription(" <p>the first line</p>
- " +
- " <p>the second line</p>
- " +
- " <p>the last line</p>");
- logClient.sendLogForObject(9,"actor name","editTask","edit Task","via app",
- "some comments",oldTaskModel,newTaskModel);
則我們可以使用下面查詢條件:
- http://{your_ObjectLogger_address}/ObjectLogger/log/query?appName=myBootApp&objectName=TaskModel&objectId=9
查詢到如下結(jié)果:
- {
- "respMsg": "成功",
- "respData": [
- {
- "id": 15,
- "appName": "myBootApp",
- "objectName": "TaskModel",
- "objectId": 9,
- "actor": "actor name",
- "action": "editTask",
- "actionName": "edit Task",
- "extraWords": "via app",
- "comment": "some comments",
- "actionTime": "2019-04-10T10:56:17.000+0000",
- "actionItemModelList": [
- {
- "id": 18,
- "actionId": 15,
- "attributeType": "NORMAL",
- "attribute": "taskName",
- "attributeName": "TASK",
- "oldValue": "oldName",
- "newValue": "newName",
- "diffValue": null
- },
- {
- "id": 19,
- "actionId": 15,
- "attributeType": "USERID",
- "attribute": "userId",
- "attributeName": "USER",
- "oldValue": "USER:3",
- "newValue": "USER:5",
- "diffValue": "diffValue"
- },
- {
- "id": 20,
- "actionId": 15,
- "attributeType": "TEXT",
- "attribute": "description",
- "attributeName": "DESCRIPTION",
- "oldValue": ""\t<p>the first line</p>\n\t<p>the second line</p>\n\t<p>the 3th line</p>"",
- "newValue": ""\t<p>the first line</p>\n\t<p>the second line</p>\n\t<p>the last line</p>"",
- "diffValue": "第6行變化:<br/> -:<del> the 3th line </del> <br/> +:<u> the last line </u> <br/>"
- }
- ]
- }
- ],
- "respCode": "1000"
- }
6 對(duì)象屬性過濾
有些對(duì)象的屬性的變動(dòng)不需要進(jìn)行日志記錄,例如updateTime、hashCode等。ObjectLogger支持對(duì)對(duì)象的屬性進(jìn)行過濾,只追蹤我們感興趣的屬性。
并且,對(duì)于每個(gè)屬性我們可以更改其記錄到ObjectLogger系統(tǒng)中的具體方式,例如修改命名等。
要想啟用這個(gè)功能,首先將配置中的object.logger.autoLog改為false。
- object.logger.autoLog=false
然后在需要進(jìn)行變化日志記錄的屬性上增加@LogTag注解。凡是沒有增加該注解的屬性在日志記錄時(shí)會(huì)被自動(dòng)跳過。
例如,注解配置如下則id字段的變動(dòng)將被忽略。
- private Integer id;
- @LogTag(name = "TaskName")
- private String taskName;
- @LogTag(name = "UserId", extendedType = "userIdType")
- private int userId;
- @LogTag(name = "Description", builtinType = BuiltinTypeHandler.TEXT)
- private String description;
該注解屬性介紹如下:
- name:必填,對(duì)應(yīng)寫入日志后的attributeName值。
- builtinType:ObjectLogger的內(nèi)置類型,為BuiltinTypeHandler的值。默認(rèn)為BuiltinTypeHandler.NORMAL。
- BuiltinTypeHandler.NORMAL:記錄屬性的新值和舊值,對(duì)比值為null
- BuiltinTypeHandler.TEXT: 用戶富文本對(duì)比。記錄屬性值的新值和舊值,并將新舊值轉(zhuǎn)化為純文本后逐行對(duì)比差異,對(duì)比值中記錄差異
- extendedType:擴(kuò)展屬性類型。使用ObjcetLogger時(shí),用戶可以擴(kuò)展某些字段的處理方式。
7 屬性處理擴(kuò)展
很多情況下,用戶希望能夠自主決定某些對(duì)象屬性的處理方式。例如,對(duì)于例子中Task對(duì)象的userId屬性,用戶可能想將其轉(zhuǎn)化為姓名后存入日志系統(tǒng),從而使得日志系統(tǒng)與userId完全解耦。
ObjectLogger完全支持這種情況,可以讓用戶自主決定某些屬性的日志記錄方式。要想實(shí)現(xiàn)這種功能,首先在需要進(jìn)行擴(kuò)展處理的屬性上為@LogTag的extendedType屬性賦予一個(gè)字符串值。例如:
- @LogTag(name = "UserId", extendedType = "userIdType")
- private int userId;
然后在業(yè)務(wù)系統(tǒng)中聲明一個(gè)Bean繼承BaseExtendedTypeHandler,作為自由擴(kuò)展的鉤子。代碼如下:
- @Service
- public class ExtendedTypeHandler implements BaseExtendedTypeHandler {
- @Override
- public BaseActionItemModel handleAttributeChange(String attributeName, String logTagName, Object oldValue, Object newValue) {
- return null;
- }
- }
接下來,當(dāng)ObjectLogger處理到該屬性時(shí),會(huì)將該屬性的相關(guān)信息傳入到擴(kuò)展Bean的handleAttributeChange方法中,然后用戶可以自行處理。傳入的四個(gè)參數(shù)解釋如下:
- extendedType:擴(kuò)展類型值,即@LogTag注解的extendedType值。本示例中為userIdType。
- attributeName:屬性名。本示例中為userId。
- logTagName:@LogTag注解的name值,可能為null。本示例中為UserId。
- oldValue:該屬性的舊值。
- newValue:該屬性的新值。
例如我們可以采用如下的方式處理userIdType屬性:
- public BaseActionItemModel handleAttributeChange(String extendedType, String attributeName, String logTagName, Object oldValue, Object newValue) {
- BaseActionItemModel baseActionItemModel = new BaseActionItemModel();
- if (extendedType.equals("userIdType")) {
- baseActionItemModel.setOldValue("USER_" + oldValue);
- baseActionItemModel.setNewValue("USER_" + newValue);
- baseActionItemModel.setDiffValue(oldValue + "->" + newValue);
- }
- return baseActionItemModel;
- }
8 總結(jié)
怎么樣,是不是ObjectLogger https://github.com/yeecode/ObjectLogger 的存在極大地方便了我們的日志記錄操作。