Oplog4j - 基於Spring的通用操做日誌生成工具

做爲一個JAVA程序員, 都應該瞭解操做日誌的需求吧? 這裏介紹一個快速實現相似操做日誌, 審覈日誌等審查功能的開源工具oplog4j.前端

實例

先來看一個OpLog4j生成的操做日誌實例. 爲了便於查看, 這裏對內容作了格式化, 還添加了一些註釋說明. 開發者能夠根據這個實例的內容判斷OpLog4j是否能知足產品的需求.java

//如下內容是基於一個單元測試中, com.github.djbing85.model.DefaultOpLog的真實輸出
summary: update user info	//操做項名稱, 這裏是更新用戶信息

operator: system admin		//這裏記錄了操做人, 若是你的操做人是一個ID, 也能夠在想要的地方進行轉換

//變動前的UserBO, JSON格式
pre: {"auditStatus":0,"balance":815146771322186439.1516528490624142744280788974720053374767303466796875,"classifyMsg":"top secret: 06dd9897-8600-49bf-a61a-ef99dae25136","createTime":1599493177359,"ignoreField":"invisible.de272fdd-f483-4a5c-b841-3b6ab4932941","phone":"911","pswd":"The quick brown fox jumps over the lazy dog","status":0,"type":"e","userId":1,"userName":"jasper.d.0e6721ab-f88a-4144-908a-e5667792fe7a"}


//變動後的UserBO, JSON格式
post: {"auditStatus":1,"balance":5825067578147956587.33458620514737635875945898078498430550098419189453125,"classifyMsg":"top secret: 677b5c9d-b7f5-4ef3-9f67-d8b37391cbad","createTime":1599493177359,"ignoreField":"invisible.9a1c12c3-6ea9-4249-9176-9f13ddd464e9","phone":"911","pswd":"The quick brown fox jumps over the lazy dog","status":1,"type":"p","userId":1,"userName":"jasper.d.1454e20f-3f34-46d5-8c4e-cb20442348f8"}


//變動詳情, 變動詳情的內容是在com.github.djbing85.aop.DefaultOpLogAOPInterceptor.getModelDiff(Class<BO>, Object, Object)中生成的, 能夠經過繼承類的方式定製本身想要的變動詳情樣式.
diff: 
	//<變動項目名稱> <變動前的值> --> <變動後的值>
	User Name: jasper.d.6b25870e-bf31-448d-a3e2-8c19a87b915e --> jasper.d.7ea08e0e-0409-4da1-b283-e04de87a93d8
	//狀態的原值是0/1, 這裏轉換成了Enable/Disable, 詳細的實現請參考後續的文檔
	Status: Enable --> Disable
	//注意這裏的0, 是沒有正確進行fieldMapping的狀況下, 取的原值展現
	Audit Status: 0 --> PENDING
	//對比原值-7500572336794443600.2980875307817212327421430018148384988307952880859375, 可見數字被格式化了
	Balance: -7,500,572,336,794,443,600.3 --> 8,830,565,855,212,825,657.58

//操做的時間
opTime: Tue Sep 08 11:05:01 CST 2020

//類型
opType: UPDATE

//BO類
modelClass: class com.github.djbing85.test.xml.model.UserBO

上面這樣詳細的操做變動詳情, 應該就能夠知足大部分對於操做日誌的需求了.mysql

這裏咱們總結一下操做日誌的幾個要素:git

操做日誌的要素

要素 說明
summary 方法簡要描述
operator 操做人
pre 變動前BO值
post 變動後BO值
diff 差別
opTime 操做時間
opType 操做類型, 新增/編輯/刪除
modelClass 操做日誌bean的類型

實現流程

引入依賴

Maven Dependency

參考 Maven Center Repo, 以下引入MAVEN依賴程序員

<dependency>
  <groupId>com.github.djbing85</groupId>
  <artifactId>oplog4j</artifactId>
  <version>0.0.1-RC01</version>
</dependency>

Gradle Groovy

implementation 'com.github.djbing85:oplog4j:0.0.1-RC01'

Gradle Kotlin

implementation("com.github.djbing85:oplog4j:0.0.1-RC01")

配置總覽

以後開發者所須要作的, 僅僅是經過一些簡單配置, 就能夠快速實現操做日誌的功能github

配置項目 說明
Bean註釋 標記BO Model bean的屬性名稱, 轉換規則等等 OpLogModel, OpLogField
Service/Dao的切入方法 配置產生日誌的方法, 該方法一般變動了對應BO的數據 OpLogJoinPoint, OpLogParam, OpLogID
Spring配置 支持xml方式springboot方式配置; 若是是xml, 僅須要最少8行配置代碼 參考Spring配置配置
Handler類 方法執行完畢後, 日誌若是須要寫入DB, 則須要在這裏實現 實現對應BO Model bean的IOpLogHandler接口

下面咱們一步步地介紹如何實現spring

Bean註釋

對於須要生成操做日誌的BO類, OpLog4j要求必需有且僅有一個ID作爲主鍵, 目前暫時不支持複合主鍵的類;sql

特別地, 對於特定的組合bean, 也能夠經過巧妙的配置達到生成操做日誌的功能, 但這類組合bean也須要知足這樣的要求: 可以由一個ID爲主鍵從存儲介質中加載.json

Bean 註釋的類是OpLogModel與OpLogField, 分別做用於類與屬性上.springboot

OpLogModel

這個annotation 的做用域是ElementType.TYPE

屬性 說明
daoBeanId Spring中定義的DAO bean ID
method 如com.xxx.dao.UserBODao.getById(Long id), 則method應該取值: "getById"

經過上面兩個配置, 咱們就能夠從Spring中獲取獲得一個BO(Business Object)對應DAO的getById方法. 這個方法在生成操做日誌時獲取"變動前對象"和"變動後對象"起到了重要的做用.

OpLogField

這個annotation 的做用域是ElementType.FIELD

咱們直接給出一個例子:

import java.math.BigDecimal;
import java.util.Date;

import com.github.djbing85.annotation.OpLogField;
import com.github.djbing85.annotation.OpLogModel;

//BO類註釋, 注意代碼中必定要有對應的com.xxx.dao.UserBODao.getById(Long id)方法
@OpLogModel(daoBeanId = "userBODao", method = "getById")
public class UserBO {
    
    //若是有自定義的構造方法, 那麼請必定補充一個默認構造方法, 不然會報異常致使沒法生成操做日誌
    public UserBO() {}

    //id = 0表示這是主鍵的第0個參數, 目前只支持一個屬性作主鍵, 複合主鍵功能尚不支持; 
    //fieldName是屬性的名稱, 若是有國際化的需求, 須要注意這裏
    @OpLogField(id = 0, fieldName = "User ID")
    private Long userId;
    
    //fieldName爲""或者null時, 會直接使用fieldName = "userName"
    @OpLogField(fieldName = "User Name")
    private String userName;
    
    //DB中type的取值是p/e, 分別表示Personal/Enterprise, fieldMapping則是一個JSON, 在生成對比的變化內容時, 會把p/e轉換成可讀性更好的Personal/Enterprise.
    @OpLogField(fieldName = "User Type", fieldMapping = "{\"p\":\"Personal\", \"e\":\"Enterprise\"}")
    private String type;
    
    //isSensitive表示這是一個敏感字段, 須要把內容轉換成**, 即審查人員也是不能看到具體內容的; @See OpLogSensitiveTypeEnum 查看更多細節
    @OpLogField(fieldName = "Password", isSensitive = true)
    private String pswd;
    
    //日期格式化支持Date/Calendar/LocalDate/LocalTime/LocalDateTime, 注意格式化失敗時會直接輸出createTime.toString()
    @OpLogField(fieldName = "Create Time", dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    
    //這裏的fieldMapping對Integer進行了轉換 
    @OpLogField(fieldName = "Status", fieldMapping = "{0:\"Disable\", 1:\"Enable\"}")
    private Integer status;
    
    @OpLogField(fieldName = "Audit Status", fieldMapping = "{1:\"PENDING\", 2:\"PASS\", 3:\"REJECT\"}")
    private Integer auditStatus;
    
    //金額的格式化 
    @OpLogField(fieldName = "Balance", decimalFormat = "#,###.##")
    private BigDecimal balance;
    
    //標註了ignore的field不會進行內容比對. 
    @OpLogField(ignore = true)
    private String ignoreField;
    
    //getter and setter ...
    
    //咱們建議非性能關鍵的BO都寫一個toString()方法, 方便在調試代碼時查看BO的值
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("UserBO [userId=");
        builder.append(userId);
        ...
        builder.append("]");
        return builder.toString();
    }
}

對應着上面的例子, 下面是更詳細的配置說明

屬性 說明 例值
id 默認-1:非主鍵. 主鍵上標註: [id = 0], 暫時不支持複合主鍵 id = 0
fieldName 屬性的名稱 User Name
fieldMapping json字符串, 會把屬性值作key, 轉換成對應的value輸出; 注意從map中取出的value爲null時, 不會進行轉換. {"0": "disabled", "1": "enabled"}
ignore 默認false, 爲true的field在對比時會被忽略 false
isSensitive 默認false, 爲true時會按maskPattern的策略對內容進行隱藏, 默認maskPattern = OpLogSensitiveTypeEnum.MASK_MIDDLE, 如內容爲12345678, 則處理後的內容爲12**78 false
maskPattern 目前支持隱藏策略爲: 隱藏前綴/後綴/中間/兩邊/所有, 默認隱藏所有. 詳情見附表1 OpLogSensitiveTypeEnum.MASK_MIDDLE
dateFormat 支持基本的日期類, 詳情見附表2. 取值要與field匹配, 不然會致使日期格式化異常 yyyy-MM-dd HH:mm:ss
decimalFormat 數字的格式化, 支持Double/Float/Long/Integer/BigDecimal, 爲""或null時不會對數字進行格式化 #,###.##

附表1

dateFormat支持的日期類型
java.util.Date
java.util.Calendar
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime

注意dateFormat取值要與field匹配, 避免出現日期格式化失敗的狀況

附表2

maskPattern支持的隱藏策略 策略
OpLogSensitiveTypeEnum.MASK_PREFIX 隱藏前綴
OpLogSensitiveTypeEnum.MASK_SUBFIX 隱藏後綴
OpLogSensitiveTypeEnum.MASK_MIDDLE 隱藏中間
OpLogSensitiveTypeEnum.MASK_2SIDES 隱藏兩邊
OpLogSensitiveTypeEnum.MASK_ALL 默認隱藏所有

切入方法

OpLogJoinPoint

這個annotation 的做用域是ElementType.TYPE

當前版本尚不支持重複註釋同一方法

屬性 說明 例值
summary 方法簡要說明 如: 新增用戶
operator 操做人, 使用EL表達式讀取參數列表中的某個參數做爲操做人, 如方法是void updatePassword(UserBO user), 當配置了operator = "user.userName"時, 操做人就會取參數中user.getUserName(), 賦值到最後生成的操做日誌類DefaultOpLog.operator中 "user.userName"
modelClass 指定與方法關聯的BO類, 必須與OpLogID聯合使用. 示例 UserBO.class
useReturn 若是方法編輯並返回了操做後的BO實例, 那麼應該set userReturn = true, 能夠減小一次DB的讀操做 默認false

OpLogParam

這個annotation 的做用域是ElementType.PARAMETER

屬性 說明 例值
isLoaded 爲true時表示方法的pre-BO直接使用被標註的對象, 爲false時pre-BO則須要按對象中配置的DAO bean與主鍵從DB中加載 默認false

OpLogID

這個annotation 的做用域是ElementType.PARAMETER

屬性 說明 例值
order 預留字段. 當前版本不支持複合主鍵, 請不要配置該屬性 default 0

示例

//summary描述了方法的操做摘要, 
	//operator指定了操做人, 這裏會直接取參數中operator的值
	//OpLogParam(isLoaded = true) 表示pre-BO能夠直接使用被標註的參數UserBO bo, 從而減小一次DB讀操做
    @OpLogJoinPoint(summary = "update user info", operator = "operator")
    public UserBO updateIsLoaded(@OpLogParam(isLoaded = true)UserBO bo, String operator) {
        Assert.notNull(bo);
        Assert.notNull(bo.getUserId());
        db.put(bo.getUserId(), bo);
        return bo;
    }

	//modelClass與@OpLogID搭配使用, 在方法執行先後, 會自動讀取updateNameById先後的對象, 並生成操做日誌
	//注意UserBO中須要有一個id字段(名稱能夠不同), 類型要與這裏的Long id匹配.
    @OpLogJoinPoint(summary = "update user name", modelClass = UserBO.class)
    public void updateNameById(@OpLogID Long id, String name) {
        Assert.notNull(id);
        UserBO bo = db.get(id);
        Assert.notNull(bo);
        bo.setUserName(name);
        db.put(id, bo);
    }

	//useReturn=true表示能夠直接使用方法返回的對象做爲post-BO, 不須要重複讀取DB. 
	//注意這裏可能有DB默認更新的字段沒法反饋到操做日誌中, 好比mysql中定義了ON UPDATE的字段, 使用時應該注意
    @OpLogJoinPoint(summary = "update user balance", useReturn=true)
    public UserBO updateBalanceById(@OpLogID Long id, BigDecimal balance) {
        Assert.notNull(id);
        UserBO bo = db.get(id);
        Assert.notNull(bo);
        bo.setBalance(balance);
        db.put(id, bo);
        return bo;
    }

modelClass選擇機制

生產中不少代碼可能在一個方法中對多個BO進行了修改, OpLog4j還不支持在一個方法中配置多個OpLogJoinPoint來生成多個BO的操做日誌.

一個方法只能對一個BO產生操做日誌, 運行時按如下順序選擇modelClass:

1. OpLogJoinPoint.modelClass配置的類, 一般與OpLogID搭配使用
	2. OpLogJoinPoint.useReturnValue = true時嘗試使用方法返回對象的class
	3. OpLogParam註釋參數的class
	4. 參數中第一個類型是被OpLogModel註釋類的class

Spring配置

支持xml與springboot方式的配置

注意源代碼中xml與springboot的測試代碼使用了不一樣的BO

xml方式

<!-- aop config -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
    <!-- 輸出操做日誌的handler, 須要實現IOpLogHandler接口 -->
    <bean id="userOpLogHandler" class="com.github.djbing85.test.xml.aop.handler.UserOpLogHandler"/>
    <!-- handler列表, 能夠有多個 -->
    <util:list id="opLogHandlers">
       <ref bean="userOpLogHandler" />
    </util:list>
    <!-- 默認操做日誌處理類. 有須要修改操做日誌的diff輸出時, 能夠繼承AbstractOpLogAOPInterceptor, 實現本身想要的輸出格式 -->
    <bean id="defaultOpAOPInterceptor" class="com.github.djbing85.aop.DefaultOpLogAOPInterceptor">
        <property name="handlers" ref="opLogHandlers" />
    </bean>
    <!-- JSON格式的操做日誌處理類. -->
    <!-- <bean id="jsonDiffOpAOPInterceptor" class="com.github.djbing85.aop.JsonDiffOpLogAOPInterceptor">
        <property name="handlers" ref="opLogHandlers" />
    </bean> -->

springboot方式

//定義handler, 注意須要實現IOpLogHandler接口
    @Bean
    public IOpLogHandler commodityOpLogHandler() {
        CommodityOpLogHandler h = new CommodityOpLogHandler();
        return h;
    }
	//定義handler
    @Bean
    public IOpLogHandler couponOpLogHandler() {
        CouponOpLogHandler h = new CouponOpLogHandler();
        return h;
    }
	//定義handler
    @Bean
    public IOpLogHandler orderChangeOpLogHandler() {
        OrderChangeOpLogHandler h = new OrderChangeOpLogHandler();
        return h;
    }
	//定義handler
    @Bean
    public IOpLogHandler orderOpLogHandler() {
        OrderOpLogHandler h = new OrderOpLogHandler();
        return h;
    }

	//定義handler list
    @Bean
    public List<IOpLogHandler> opLogHandlerList() {
        List<IOpLogHandler> list = new ArrayList<>();
        list.add(commodityOpLogHandler());
        list.add(couponOpLogHandler());
        list.add(orderChangeOpLogHandler());
        list.add(orderOpLogHandler());
        return list;
    }
    
	// 定義DefaultOpLogAOPInterceptor, 注入handler list
    @Bean
    public DefaultOpLogAOPInterceptor defaultOpAOPInterceptor() {
        DefaultOpLogAOPInterceptor defaultOpAOPInterceptor = new DefaultOpLogAOPInterceptor();
        defaultOpAOPInterceptor.setHandlers(opLogHandlerList());
        return defaultOpAOPInterceptor;
    }

	//json格式的操做日誌攔截器
    //@Bean
    //public JsonDiffOpLogAOPInterceptor jsonDiffOpAOPInterceptor() {
    //    JsonDiffOpLogAOPInterceptor jsonDiffOpAOPInterceptor = new JsonDiffOpLogAOPInterceptor();
    //    jsonDiffOpAOPInterceptor.setHandlers(opLogHandlerList());
    //    return jsonDiffOpAOPInterceptor;
    //}

IOpLogHandler接口

IOpLogHandler是輸出操做日誌的地方, 一般能夠在這裏對操做日誌作進一步的加工, 而後保存到DB中

import com.github.djbing85.aop.handler.IOpLogHandler;
import com.github.djbing85.model.DefaultOpLog;
import com.github.djbing85.test.springboot.model.UserOrder;

public class OrderOpLogHandler implements IOpLogHandler<UserOrder> {

    // 指定操做日誌的class
    @Override
    public Class<UserOrder> getModelClass() {
        return UserOrder.class;
    }

    @Override
    public void handleDiff(DefaultOpLog<UserOrder> log) {
        System.out.println("summary: " + log.getSummary());
        System.out.println("operator: " + log.getOperator());
        System.out.println("pre: " + log.getPre());
        System.out.println("post: " + log.getPost());
        System.out.println("diff: " + log.getDiff());
        System.out.println("opTime: " + log.getOpTime());
        System.out.println("opType: " + log.getOpType());
        System.out.println("modelClass: " + log.getModelClass());
    }
}

DefaultOpLogAOPInterceptor

這裏給出一例源碼中IOpLogHandler.handleDiff的輸出以下:

summary: Order Change

operator: null

pre: {"commodity":{"id":2,"img":"http://www.abc.org/path2/to/img.jpg","name":"hydrogen peroxide solution","priceBigDecimal":200,"priceDouble":200.0,"priceFloat":200.0,"priceInt":200,"priceLong":200},"coupon":{"discountDesc":"20.00%","discountInt":2000,"id":2,"name":"50% OFF","priceDouble":200.0},"order":{"commodityId":2,"couponId":2,"createdTime":1599492382991,"date":"2020-09-07","dateTime":"2020-09-07T23:26:22.991","orderId":2,"time":"23:26:22.991","totalPrice":200,"userId":2},"orderId":2}

post: {"commodity":{"id":2,"img":"http://www.abc.org/path2/to/img.jpg","name":"hydrogen peroxide solution","priceBigDecimal":543,"priceDouble":543.0,"priceFloat":543.0,"priceInt":543,"priceLong":543},"coupon":{"discountDesc":"6.67%","discountInt":667,"id":2,"name":"50% OFF","priceDouble":200.0},"order":{"commodityId":2,"couponId":2,"createdTime":1599492382991,"date":"2020-09-07","dateTime":"2020-09-07T23:26:22.991","orderId":2,"time":"23:26:22.991","totalPrice":1111,"userId":2},"orderId":2}

diff:     Order: 
        Total Price: 200 --> 1,111
    Commodity: 
        Price in Float: $200 --> $543
        Price in Double: $200 --> $543
        Price in BigDecimal: $200 --> $543
        Price in Long: $200 --> $543
        Price in Integer: $200 --> $543
    Coupon: 
        Discount: 20.00% --> 6.67%

opTime: Tue Sep 08 11:05:01 CST 2020

opType: UPDATE

modelClass: class com.github.djbing85.test.springboot.model.OrderChange

JsonDiffOpLogAOPInterceptor

這裏與DefaultOpLogAOPInterceptor不一樣的地方在於diff

summary: Order Change

operator: null

pre: {"commodity":{"id":2,"img":"http://www.abc.org/path2/to/img.jpg","name":"hydrogen peroxide solution","priceBigDecimal":200,"priceDouble":200.0,"priceFloat":200.0,"priceInt":200,"priceLong":200},"coupon":{"discountDesc":"20.00%","discountInt":2000,"id":2,"name":"50% OFF","priceDouble":200.0},"order":{"commodityId":2,"couponId":2,"createdTime":1599621329343,"date":"2020-09-09","dateTime":"2020-09-09T11:15:29.343","orderId":3,"time":"11:15:29.343","totalPrice":500,"userId":2},"orderId":2}

post: {"commodity":{"id":2,"img":"http://www.abc.org/path2/to/img.jpg","name":"hydrogen peroxide solution","priceBigDecimal":543,"priceDouble":543.0,"priceFloat":543.0,"priceInt":543,"priceLong":543},"coupon":{"discountDesc":"6.67%","discountInt":667,"id":2,"name":"50% OFF","priceDouble":200.0},"order":{"commodityId":2,"couponId":2,"createdTime":1599621329343,"date":"2020-09-09","dateTime":"2020-09-09T11:15:29.343","orderId":3,"time":"11:15:29.343","totalPrice":1111,"userId":2},"orderId":2}

diff: [{"fieldName":"Order","subModelDiffList":[{"fieldName":"Total Price","from":"\"500\"","to":"\"1,111\""}]},{"fieldName":"Commodity","subModelDiffList":[{"fieldName":"Price in Float","from":"\"$200\"","to":"\"$543\""},{"fieldName":"Price in Double","from":"\"$200\"","to":"\"$543\""},{"fieldName":"Price in BigDecimal","from":"\"$200\"","to":"\"$543\""},{"fieldName":"Price in Long","from":"\"$200\"","to":"\"$543\""},{"fieldName":"Price in Integer","from":"\"$200\"","to":"\"$543\""}]},{"fieldName":"Coupon","subModelDiffList":[{"fieldName":"Discount","from":"\"20.00%\"","to":"\"6.67%\""}]}]

opTime: Wed Sep 09 11:15:29 CST 2020

opType: UPDATE

modelClass: class com.github.djbing85.test.springboot.model.OrderChange

DefaultOpLog操做日誌

如上一小節的IOpLogHandler.handleDiff輸出, 就是一個DefaultOpLog, 咱們來看一下它都有哪些屬性

字段 說明 相關配置項
summary 方法簡要描述 OpLogJoinPoint.summary
operator 操做人 OpLogJoinPoint.operator
pre 操做前值 OpLogJoinPoint方法針對的BO實例, 參考modelClass選擇機制
post 操做後值 OpLogJoinPoint方法針對的BO實例, 參考modelClass選擇機制
diff 差別 在com.github.djbing85.aop.AbstructOpLogAOPInterceptor.getModelDiff(Class<BO>, Object, Object)方法中生成; 在IOpLogHandler.handleDiff中輸出
opTime 操做時間 java.util.Date實例
opType 操做類型 根據pre/post是否爲null, 分爲CREATE/UPDATE/DELETE三大類
modelClass 操做類 操做日誌的BO類, 參考modelClass選擇機制

注意事項

自定義constructor

若是BO有自定義constructor, 須要寫一個默認constructor.

記錄IP

有一些項目要求記錄操做者的IP地址, 這一類需求也是能夠知足的, 能夠把IP與操做人按必定的格式寫入OpLogJoinPoint.operator所標註的參數中, 在輸出日誌時分離兩項, 分別保存便可.

//service保存方法
@OpLogJoinPoint(summary = "保存訂單", operator = "ext")
public orderSave(UserOrder order, String ext) {
    //保存
    ...
}

...

//controller某個方法
public testOp() {
    String operator = "Admin";
    String ip = "127.0.0.1";
    
    //order change
    UserOrder order = loadOrder(1L);
    order.setTotalPrice(new BigDecimal(1234));
    //IP與操做人都寫在保存方法的ext字段中
    orderSave(order, operator + "@@@" + ip);
}

...
//IOpLogHandler.handleDiff
public void handleDiff(DefaultOpLog<UserOrder> log) {
    String [] strArray = log.getOperator().split("@@@");
    System.out.println("operator: " + strArray[0]);
    System.out.println("ip: " + strArray[1]);
    ...
}

相似地, 一些其它的信息也能夠經過一樣的方法傳遞到最終的操做日誌中來

組合bean生成操做日誌

組合bean必需能夠由惟一的一個ID加載, 且須要本身在handler中處理List類型的屬性

示例以下:

//use lombok
@Data
@OpLogModel(daoBeanId = "orderChangeService", method = "orderDetail")
public class OrderChange {
    //惟一ID
    @OpLogField(id = 0, fieldName = "Order ID")
    private Long orderId;
	//組合屬性以下
    //注意不要添加除fieldName外的其它屬性
    @OpLogField(fieldName = "Order")
    private UserOrder order;
    //注意不要添加除fieldName外的其它屬性
    @OpLogField(fieldName = "Commodity")
    private Commodity commodity;
    //注意不要添加除fieldName外的其它屬性
    @OpLogField(fieldName = "Coupon")
    private Coupon coupon;
}

//////////////////////////

//OrderChangeService是OrderChange的service實現類
@Service
public class OrderChangeService {
    
    ...
    
    //這個方法修改了OrderChange內部幾個組合bean的屬性
    @OpLogJoinPoint(summary = "Order Change", useReturn = true)
    public OrderChange orderChange(OrderChange change) {
        orderDao.updateTotalPrice(change.getOrderId(), change.getOrder().getTotalPrice());
        commodityDao.updatePrice(change.getCommodity().getId(), change.getCommodity().getPriceBigDecimal());
        couponDao.updateDiscount(change.getCoupon().getId(), change.getCoupon().getDiscountInt());
        return orderDetail(change.getOrderId());
    }

    //按orderId返回OrderChange, 用於生成操做日誌時加載pre-BO與post-BO
    public OrderChange orderDetail(Long orderId) {
        UserOrder order = orderDao.getById(orderId);
        Coupon coupon = couponDao.getById(order.getCouponId());
        Commodity commodity = commodityDao.getById(order.getCommodityId());
        OrderChange change = new OrderChange();
        change.setOrderId(orderId);
        UserOrder order2 = new UserOrder();
        BeanUtils.copyProperties(order, order2);
        Coupon coupon2 = new Coupon();
        BeanUtils.copyProperties(coupon, coupon2);
        Commodity commodity2 = new Commodity();
        BeanUtils.copyProperties(commodity, commodity2);
        
        change.setOrder(order2);
        change.setCoupon(coupon2);
        change.setCommodity(commodity2);
        return change;
    }
}

//////////////////////////
    //單元測試
    @Test
    public void orderChange() {
        //從DB中讀取數據
        OrderChange change = orderService.orderDetail(2L);
        //修改數據
        change.getOrder().setTotalPrice(new BigDecimal(1111));
        change.getCommodity().setPriceBigDecimal(new BigDecimal(543));
        change.getCoupon().setDiscountInt(667);
        //保存數據, 完成後OpLog4j會生成操做日誌
        orderService.orderChange(change);
    }

////////////////////測試輸出 
summary: Order Change
operator: null
pre: {"commodity":{"id":2,"img":"http://www.abc.org/path2/to/img.jpg","name":"hydrogen peroxide solution","priceBigDecimal":200,"priceDouble":200.0,"priceFloat":200.0,"priceInt":200,"priceLong":200},"coupon":{"discountDesc":"20.00%","discountInt":2000,"id":2,"name":"50% OFF","priceDouble":200.0},"order":{"commodityId":2,"couponId":2,"createdTime":1599534380355,"date":"2020-09-08","dateTime":"2020-09-08T11:06:20.355","orderId":3,"time":"11:06:20.355","totalPrice":500,"userId":2},"orderId":2}
post: {"commodity":{"id":2,"img":"http://www.abc.org/path2/to/img.jpg","name":"hydrogen peroxide solution","priceBigDecimal":543,"priceDouble":543.0,"priceFloat":543.0,"priceInt":543,"priceLong":543},"coupon":{"discountDesc":"6.67%","discountInt":667,"id":2,"name":"50% OFF","priceDouble":200.0},"order":{"commodityId":2,"couponId":2,"createdTime":1599534380355,"date":"2020-09-08","dateTime":"2020-09-08T11:06:20.355","orderId":3,"time":"11:06:20.355","totalPrice":1111,"userId":2},"orderId":2}
diff:         Order: 
            Total Price: 500 --> 1,111
        Commodity: 
            Price in Float: $200 --> $543
            Price in Double: $200 --> $543
            Price in BigDecimal: $200 --> $543
            Price in Long: $200 --> $543
            Price in Integer: $200 --> $543
        Coupon: 
            Discount: 20.00% --> 6.67%
opTime: Tue Sep 08 11:06:20 CST 2020
opType: UPDATE
modelClass: class com.github.djbing85.test.springboot.model.OrderChange

在測試的輸出結果中的diff部分, 咱們能夠看到, orderChange.order.totalPrice等等幾個內部bean的變動被如實地記錄了下來.

Collection屬性

當前版本的OpLog4j尚不支持Collection屬性的變動, 有這部分需求的開發者須要自行比較pre/post中的list對象, 參考IOpLogHandler接口

國際化

OpLog4j支持對fieldName和summary進行國際化

須要在輸出的diff中實現國際化的開發者, 須要額外進行如下代碼配置:

OpLog4j的配置中使用JsonDiffOpLogAOPInterceptor

配置國際化攔截器

源碼裏國際化工具類使用了OpLog4jMessageUtils, 而一般你應該使用本身的國際化工具類實現一樣的功能.

@Configuration
public class I18nConf {

    /**
     * default LocaleResolver
     */
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.US);
        return localeResolver;
    }

    /**
     * localeInterceptor, "lang" is the parameter name
     */
    @Bean
    public WebMvcConfigurer localeInterceptor() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
                localeInterceptor.setParamName("lang");
                registry.addInterceptor(localeInterceptor);
            }
        };
    }
    
    //若是你已經有一個ResourceBundleMessageSource, 那麼在OpLog4jMessageUtils中直接使用便可, 不須要重複定義這個bean
    @Bean
    public ResourceBundleMessageSource messageSource() {
        Locale.setDefault(Locale.CHINESE);
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasenames("i18n/messages/messages");// path and name of the resource bundle
        source.setUseCodeAsDefaultMessage(true);
        source.setDefaultEncoding("UTF-8");
        return source;
    }
    
    //定義一個國際化工具類
    @Bean
    public OpLog4jMessageUtils opLog4jMessageUtils() {
        return new OpLog4jMessageUtils(messageSource());
    }
}

給每一個model配置對應語言的messages.properties

如源碼中:

/oplog4j/src/test/resources/i18n/messages/messages_zh_CN.properties

coupon.diff=差別
coupon.discount=折扣

total.price=總價

/oplog4j/src/test/resources/i18n/messages/messages_en_US.properties

coupon.diff=DIff
coupon.discount=Discount

total.price=Total Price

配置OpLogField中的fieldName

配置成messages.properties中對應的國際化配置項, 如:

//原來的配置
    //@OpLogField(fieldName = "Total Price", decimalFormat = "#,###.##")
    //國際化的fieldName配置
    @OpLogField(fieldName = "total.price", decimalFormat = "#,###.##")
    private BigDecimal totalPrice;

運行WEB

如在Eclipse中, 打開源碼的com.github.djbing85.test.springboot.OpLog4jApplication, 右鍵Run As --> 1 Java Application

訪問http://127.0.0.1:8080/i18nTest?lang=en_US

{"fieldName":"Coupon","subModelDiffList":[{"fieldName":"Discount","from":"\"20.00%\"","to":"\"6.67%\""}]}

訪問<http://127.0.0.1:8080/i18nTest?lang=zh_CN

{"fieldName":"Coupon","subModelDiffList":[{"fieldName":"折扣","from":"\"20.00%\"","to":"\"6.67%\""}]}

前端再根據需求顯示便可.

相關文章
相關標籤/搜索