对象对比差异解析工具
一、项目背景
对用户业务操作记录时,需要记录到具体某个字段变更,例如:收货地址发生变更:详细地址由【"西红门2栋603"】修改成【"西红门2栋600"】
如果采用纯手动判断修改前修改后的值是否变化,则需要写如下判断:
if (val != newVal) {
print("收货地址发生变更:详细地址由【val】修改成【newVal】")
}
当需要判断的属性逐渐增多,则会冗余很多这种逻辑代码。故想利用Java反射来对比对象,从而对象间的获取差异。
二、实现效果
// 测试用例CompareTest
DiffResult diff = DiffUtil.resolve(GoodsOrder.class, getBefore(), getAfter());
log.info("是否有差异:{}", diff.isHasDiff());
log.info("差异内容:{}", diff.getDiffs());
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 完整示例
/**
* 订单收货地址
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class OrderAddress {
@DiffKey(name = "手机号")
private String phone;
@DiffKey(name = "联系人")
private String name;
@DiffKey(name = "详细地址")
private String address;
}
/**
* 商品明细
*/
@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;
}
/**
* 商品订单
*/
@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<GoodsItem> 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 自定义属性格式化文案
在/resourcesMETA-INF/services
目录下新建com.jumper.property.comparer.pattern.DiffPattern
文件
可以按需重写文案规则,${val}
修饰变量,重写保证原来的变量同时存在
@SPI
public class CustomDiffPattern extends DefaultDiffPattern {
@Override
public String getPropertyDiffPattern() {
return "${property}值由原值【${before}】变成修改后的值【${after}】";
}
}
重写属性规则后输出差异结果:
// 修改前输出
自动配送由【"是"】修改成【"否"】
// 修改后输出
自动配送值由原值【"是"】变成修改后的值【"否"】
其他需重写规则参见:DefaultDiffPattern
五、集成mybatis
5.1引入依赖
<!-- 日志依赖 -->
<dependency>
<groupId>com.jumper</groupId>
<artifactId>property-comparer-starter</artifactId>
<version>1.0.0</version>
</dependency>
5.2添加注解 @EnableBizLog
@EnableBizLog
@SpringBootApplication
public class LogDemoApplication {
public static void main(String[] args) {
SpringApplication.run(LogDemoApplication.class, args);
}
}
5.3编写日志相关代码
5.3.1 日志业务表
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 日志业务枚举
/**
* 日志表
*/
@Getter
@AllArgsConstructor
public enum LogTableEnum implements LogTable {
ORDER_LOG("order_log", "订单日志");
private final String logTableName;
private final String logTableDesc;
}
5.3.3 日志业务manager
/**
* 订单日志管理器
*/
@Component
public class OrderLogManager extends AbstractLogManager<String> {
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 简单调用
/**
* 创建订单日志
*/
@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 测试用例
六、gitee地址
https://gitee.com/MyAngle/java-property-comparer