# java-property-comparer **Repository Path**: rebornable/java-property-comparer ## Basic Information - **Project Name**: java-property-comparer - **Description**: Java数据修改日志解析工具,对用户业务操作记录时,需要记录到具体某个字段变更, 例如:收货地址发生变更:详细地址由【"西红门2栋603"】修改成【"西红门2栋600"】 - **Primary Language**: Java - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 7 - **Created**: 2023-07-11 - **Last Updated**: 2023-07-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 对象对比差异解析工具 ## 一、项目背景 对用户业务操作记录时,需要记录到具体某个字段变更, `例如:收货地址发生变更:详细地址由【"西红门2栋603"】修改成【"西红门2栋600"】` 如果采用纯手动判断修改前修改后的值是否变化,则需要写如下判断: ```java if (val != newVal) { print("收货地址发生变更:详细地址由【val】修改成【newVal】") } ``` 当需要判断的属性逐渐增多,则会冗余很多这种逻辑代码。故想利用Java反射来对比对象,从而对象间的获取差异。 ## 二、实现效果 ```java // 测试用例CompareTest DiffResult diff = DiffUtil.resolve(GoodsOrder.class, getBefore(), getAfter()); log.info("是否有差异:{}", diff.isHasDiff()); log.info("差异内容:{}", diff.getDiffs()); ``` ```text true 订单编号由【"10086"】修改成【"10086001"】 自动配送由【"是"】修改成【"否"】 下单日期由【"2022-04-05"】修改成【"2022-05-05"】 订单有效时间由【"2022-03-30" - "2022-04-30"】修改成【"2022-04-30" - "2022-05-30"】 收货地址发生变更:联系人由【"肯德基"】修改成【"肯德基001"】,详细地址由【"西红门2栋603"】修改成【"西红门2栋600"】 商品列表发生变更: 商品列表,新增行【"小米手机"】 商品列表,删除行【"华为手机"】 商品列表,删除行【"苹果手机"】 商品列表,更新行【"三星手机"】,变更内容: 商品名称由【"三星手机"】修改成【"三星手机001"】,购买数量由【"20"】修改成【"22.22"】 ``` ## 三、接入 ### 3.1 注解说明 #### 3.1.1 @DiffKey 标识此属性会发生变化,无此注解属性,不记录差异 #### 3.1.2 @UnionKey 用户列表对象,标识列表明细对象的唯一标识(可对多个属性配置) 用户判断列表明细对象,是否发生,新增、修改、删除动作 #### 3.1.3 @UnionDisplayKey 用户列表对象新增、修改、删除动作时展示的名称(可对多个属性配置) #### 3.1.4 @BooleanFormat 对Boolean类型属性格式化 #### 3.1.5 @DateFormat 对Date类型属性格式化 #### 3.1.6 @LocalDateFormat 对LocalDate类型属性格式化 #### 3.1.7 @LocalDateTimeFormat 对LocalDateTime类型属性格式化 ### 3.2 完整示例 ```java /** * 订单收货地址 */ @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class OrderAddress { @DiffKey(name = "手机号") private String phone; @DiffKey(name = "联系人") private String name; @DiffKey(name = "详细地址") private String address; } ``` ```java /** * 商品明细 */ @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class GoodsItem { @UnionKey @UnionDisplayKey @DiffKey(name = "商品编码") private String code; @UnionDisplayKey @DiffKey(name = "商品名称") private String name; @DiffKey(name = "购买数量") private BigDecimal count; @DiffKey(name = "购买金额(元)") private BigDecimal amount; } ``` ```java /** * 商品订单 */ @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class GoodsOrder { @DiffKey(name = "订单编号") private String orderNo; @BooleanFormat @DiffKey(name = "自动配送") private boolean autoDelivery; @LocalDateTimeFormat(pattern = "yyyy-MM-dd") @DiffKey(name = "有效开始时间", type = DiffConstant.COMBINATION_PROPERTY, combinationName = "订单有效时间") private LocalDateTime startTime; @LocalDateTimeFormat(pattern = "yyyy-MM-dd") @DiffKey(name = "有效结束时间", type = DiffConstant.COMBINATION_PROPERTY, combinationName = "订单有效时间") private LocalDateTime endTime; @DiffKey(name = "商品列表", type = DiffConstant.ARRAY_PROPERTY, subCls = GoodsItem.class, whenArrayFullProperties = false) private List items; @DiffKey(name = "收货地址", type = DiffConstant.OBJECT_PROPERTY) private OrderAddress address; @LocalDateFormat(pattern = "yyyy-MM-dd") @DiffKey(name = "下单日期") private LocalDate orderDate; @DateFormat(pattern = "yyyy-MM-dd HH") @DiffKey(name = "支付时间") private Date paymentTime; } ``` ## 四、拓展 ### 4.1 自定义属性格式化文案 在`/resources/META-INF/services`目录下新建`com.jumper.property.comparer.core.pattern`文件 可以按需重写文案规则,`${val}`修饰变量,重写保证原来的变量同时存在 ```java @SPI public class CustomDiffPattern extends DefaultDiffPattern { @Override public String getPropertyDiffPattern() { return "${property}值由原值【${before}】变成修改后的值【${after}】"; } } ``` 重写属性规则后输出差异结果: ```text // 修改前输出 自动配送由【"是"】修改成【"否"】 // 修改后输出 自动配送值由原值【"是"】变成修改后的值【"否"】 ``` 其他需重写规则参见:DefaultDiffPattern ## 五、集成mybatis ### 5.1引入依赖 ```xml com.jumper property-comparer-starter 1.0.0 ``` ### 5.2添加注解 @EnableBizLog ```java @EnableBizLog @SpringBootApplication public class LogDemoApplication { public static void main(String[] args) { SpringApplication.run(LogDemoApplication.class, args); } } ``` ### 5.3编写日志相关代码 #### 5.3.1 日志业务表 ```sql CREATE TABLE `order_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `gmt_created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00' COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT '1000-01-01 00:00:00' COMMENT '更新时间', `action` varchar(100) NOT NULL COMMENT '执行动作', `data_key` varchar(100) NOT NULL DEFAULT '' COMMENT '数据标识', `log_value` text NOT NULL COMMENT '日志内容', `opt_user_name` varchar(60) NOT NULL DEFAULT '' COMMENT '操作用户名称', `opt_user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '操作用户ID', PRIMARY KEY (`id`), KEY `idx_g_c` (`gmt_created`) COMMENT '创建时间', KEY `idx_d_k` (`data_key`) COMMENT '数据标识' ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='业务日志-订单日志'; ``` #### 5.3.2 日志业务枚举 ```java /** * 日志表 */ @Getter @AllArgsConstructor public enum LogTableEnum implements LogTable { ORDER_LOG("order_log", "订单日志"); private final String logTableName; private final String logTableDesc; } ``` #### 5.3.3 日志业务manager ```java /** * 订单日志管理器 */ @Component public class OrderLogManager extends AbstractLogManager { public OrderLogManager(LogRepository repository) { super(repository); } @Autowired private OrderService orderService; @Override public LogTable getBusType() { return LogTableEnum.ORDER_LOG; } @Override public Object getOptData(String orderNum) { return orderService.getOrder(orderNum); } } ``` #### 5.3.4 简单调用 ```java /** * 创建订单日志 */ @SneakyThrows @Test void createOrderLog() { GoodsOrder order = createOrder(); logManager.logAction(order.getOrderNo(), "创建", "创建订单", User.of("1", "张三")); // 默认异步写日志 Thread.sleep(2000); } /** * 修改订单日志 */ @SneakyThrows @Test void updateOrderLog() { GoodsOrder order = createOrder(); logManager.execute(order.getOrderNo(), "修改", GoodsOrder.class, User.of("1", "张三"), (query) -> { transactionTemplate.execute(status -> { try { // 更新订单 updateOrder(order); // 最好事务内执行 query.executeQuery(); } catch (Exception ex) { status.setRollbackOnly(); throw ex; } return null; }); }); // 默认异步写日志 Thread.sleep(2000); } /** * 获取订单日志 */ @SneakyThrows @Test void queryOrderLog() { LogPageParam pageParam = new LogPageParam(); pageParam.setPageNum(1); pageParam.setPageSize(10); pageParam.setDataKey("10086"); LogEntityPage logPage = logManager.getLogPage(pageParam); return; } ``` #### 5.3.4 其他 用例代码见 log-demo 测试用例