一、序言
👉 在项目开发中,「字段变更日志记录」是贯穿多业务场景的高频需求 ------ 无论是用户信息修改(如手机号、邮箱更新),还是配置参数调整,都需要精准记录字段新旧值、操作人、操作时间等关键信息,用于后续审计追溯、问题排查或数据回溯。
1. 传统实现的痛点:重复与混乱
但在实际多人协作开发中,这类需求往往陷入「重复造轮子」的困境,主要体现在两大问题:
- 重复编码严重:每个业务接口(如用户模块、订单模块)都需单独封装日志记录逻辑:从获取实体新旧值、对比字段差异,到构建日志对象、写入数据库,核心逻辑高度一致,仅实体类和字段不同,却要重复编写大量冗余代码(典型的「复制粘贴式开发」)。
- 标准杂乱无章 :缺乏统一日志标准,不同开发者实现逻辑差异显著:例如新旧值比较规则不一致、存储格式混乱(如
BigDecimal精度不统一、null值直接存为 "null" 字符串或被省略),导致日志数据碎片化、可读性差,后续排查或统计时需反复核对规则,效率极低。
2. 传统实现 vs 工具化实现
传统方式
每个字段都需要重复编写「比较逻辑 + 对象构建 + 添加集合」的模板代码,字段越多,冗余越严重,且易因复制粘贴导致字段名拼写错误;同时对比规则、存储方式被硬编码,后续需调整时需逐行修改,维护成本极高。
java
public static void main(String[] args) {
String oldName = "张三", newName = "张三";
Integer oldAge = 18, newAge = 19;
List<ChgsEntity> chgsList = new ArrayList<>();
// 比较用户姓名是否发生变更
if(!ObjectUtils.nullSafeEquals(oldName, newName)) {
ChgsEntity chgsEntity = new ChgsEntity();
chgsEntity.setKey("user_name");
chgsEntity.setOldVal(oldName);
chgsEntity.setNewVal(newName);
chgsList.add(chgsEntity);
}
// 比较用户年龄是否发生变更
if(!ObjectUtils.nullSafeEquals(oldAge, newAge)) {
ChgsEntity chgsEntity = new ChgsEntity();
chgsEntity.setKey("user_age");
chgsEntity.setOldVal(String.valueOf(oldAge));
chgsEntity.setNewVal(String.valueOf(newAge));
chgsList.add(chgsEntity);
}
// 记录 CHGS 变更日志
chgsDao.batchInsert(chgsList);
}
工具化方式
java
public static void main(String[] args) {
ChgsLogRecorder chgsLogRecorder = new ChgsLogRecorder();
String oldName = "张三", newName = "张三";
Integer oldAge = 18, newAge = 19;
// 批量比较字段是否发生变更并记录 CHGS 变更日志
FieldDiffLogUtil.fieldDiff()
.addField("user_name",oldName,newName)
.addField("user_age",oldAge,newAge)
.batchRecord(chgsLogRecorder);
}
工具化实现通过链式调用封装了所有重复逻辑:自动完成字段对比、日志对象构建、批量存储等操作;基于函数式接口驱动的设计,支持开发者按需自定义核心逻辑 ------ 无需修改工具源码,即可适配不同业务场景(如金融场景的高精度对比、日志分级存储需求等),不仅解决了传统方式的冗余与混乱问题,更通过统一标准提升了日志数据的可用性,兼顾开发效率与代码规范性。
本文基于 SpringBoot 生态,详解这套「对比逻辑统一、调用简洁、扩展灵活、适配多场景」的字段对比 + 变更日志记录方案,从核心原理到实战案例,带你掌握函数式接口在工具类设计中的灵活运用,兼顾开发效率与代码规范性。
二、工具化设计思路
工具类的核心设计思路是 "流程归一化 + 组件分层解耦":先将 "字段变更→日志落地" 的全链路拆解为固定步骤,再按 "单一职责" 拆分组件,通过接口定义标准、实现类落地细节 ------ 既保证通用场景开箱即用,又为特殊业务需求预留灵活扩展入口。
1. 全链路拆解:从差异到日志的闭环流程
从 "原始数据" 到 "最终日志",字段变更日志的核心流程遵循 "对比→筛选→格式化→记录" 的闭环,每一步聚焦单一目标,职责不交叉:
- 对比:判断字段新旧值是否发生变更(解决 "变没变" 的问题);
- 筛选:从对比出的差异中,筛选出 "需要记录" 的变更(解决 "记什么" 的问题);
- 格式化:将不同类型的原始值(日期、金额、枚举等)转为统一可读的字符串(解决 "怎么展示" 的问题);
- 记录:将格式化后的变更信息存储到目标渠道(解决 "记到哪" 的问题)。
2. 核心组件分层:职责单一与接口抽象
为实现流程复用和灵活扩展,工具类采用四层分层设计,对应上述核心流程,分层原则为 "职责单一 + 接口抽象"(每层仅承担单一职责,确保组件解耦):
- 对比层:统一对比规则,仅负责 "判断是否变更",避免业务代码中散落重复的对比逻辑;
- 格式化层:日志内容的 "标准化处理器",将任意类型原始值转为统一、可读的字符串,解决不同数据类型日志展示不一致问题;
- 记录层:日志输出的 "执行器",负责具体的日志信息存储(如数据库、MQ 等);
- 协调层:工具类的 "对外入口 + 流程调度中枢",封装前三层交互逻辑,让调用方无需关注内部细节,以极简代码完成操作,降低使用门槛。
3. 组件规范化设计:接口定义与交互流程
分层后,针对各层组件定义标准化接口,通过 "接口定标准、实现类落地细节" 的方式,确保组件间仅依赖接口、不依赖具体实现,提升扩展性:
| 组件 | 职责描述 |
|---|---|
FieldComparator<T> |
字段对比顶层接口,定义字段新旧值的对比逻辑 |
ValueFormatter |
值格式化接口,简化自定义格式化逻辑(如日期转 "yyyy-MM-dd" 格式) |
DiffLogRecorder<T> |
变更日志记录接口,屏蔽不同存储渠道的实现差异(如数据库、MQ) |
FieldDiffRecordUtil |
对外暴露 API,支持单字段 / 多字段添加,内部自动串联 "对比→筛选→格式化→记录" 全流程 |
完整交互流程从业务调用开始:

- 业务侧通过
DiffLogRecordUtil添加需要对比的字段,可配置指定对比器、格式化器(可选,默认使用通用实现)。 DiffLogRecordUtil调用比较器FieldComparator,完成新旧字段值的对比,筛选出发生变更的字段。DiffLogRecordUtil携带变更字段信息,调用日志记录器DiffLogRecorder。- 日志记录器
DiffLogRecorder调用格式化器ValueFormatter,对变更字段的旧值、新值进行统一格式化(如日期转字符串、敏感信息脱敏)。 DiffLogRecorder结合格式化后的字段值,构建日志实体,并将其持久化到目标存储介质(如数据库)。
这种设计的优势在于形成了 "低耦合、高扩展" 的联动机制:
- 从依赖关系来看,顶层接口是各层的 "行为规范",实现类是规范的具体落地,工具类则封装通用实现供直接复用。
- 协调层依赖前三层的接口完成流程串联,不与具体实现类绑定。任意一层的实现都可独立替换(如将 "数据库记录" 改为 "MQ 发送",仅需替换
DiffLogRecorder实现类)。 - 新增功能(如自定义对比规则)只需新增接口实现,不改动原有代码,同时协调层的封装让业务使用更简洁,真正实现 "通用场景开箱即用,特殊场景灵活扩展"。
三、核心组件
1. 字段对比接口(FieldComparator)
为了实现对比逻辑的灵活扩展与解耦,我们抽象出了FieldComparator接口 ------ 作为对比层的核心组件,仅专注于 "判断两个字段值是否一致" 这一核心行为,不关心具体的对比逻辑(例如是字符串全等对比、金额精度对比还是日期格式对比)。
接口定义如下:
java
import java.util.function.BiFunction;
/**
* 字段对比顶层接口
* @param <T> 字段类型
*/
@FunctionalInterface
public interface FieldComparator<T> extends BiFunction<T, T, Boolean> {
/**
* 字段对比核心方法
* @param oldVal 旧值
* @param newVal 新值
* @return true=值一致,false=值不一致
*/
boolean compare(T oldVal, T newVal);
// 适配BiFunction,支持函数式调用
@Override
default Boolean apply(T oldVal, T newVal) {
return compare(oldVal, newVal);
}
}
接口通过泛型<T>声明字段类型,使其能适配任意数据类型。无论是基础类型(如BigDecimal、Date)还是自定义实体(如User、Order),都能通过FieldComparator<T>定义针对性的对比规则,避免了类型强转带来的冗余代码:
java
public interface FieldComparator<T>
compare 抽象方法是接口的核心,它直接定义了 "如何判断两个值是否一致":返回true表示值未变更,false表示值已变更。所有实现类都需要重写该方法,注入具体的对比逻辑(例如金额对比时忽略小数点后两位之后的精度差异):
java
boolean compare(T oldVal, T newVal);
继承 Java 原生函数式接口BiFunction<T, T, Boolean>,是为了兼容函数式接口的调用场景,通过default方法实现apply方法,将调用转发至compare方法:
java
// 适配 BiFunction 接口的 apply 方法,转发至核心 compare 方法
@Override
default Boolean apply(T oldVal, T newVal) {
return compare(oldVal, newVal);
}
FieldComparator的抽象设计和函数式接口的特性,让开发者只需实现compare方法即可自定义对比规则,同时泛型与BiFunction适配确保了接口的通用性,既能处理简单类型,也能支持复杂业务实体的对比。
2. 字段值格式化接口(ValueFormatter)
在记录字段变更日志时,不同类型的字段(如日期、枚举、集合等)需要不同的字符串展示形式(例如日期需格式化为yyyy-MM-dd,枚举需显示中文描述)。通过接口定义格式化规则,可避免硬编码转换逻辑,实现 "规则与业务分离"。
函数式接口 ValueFormatter 实现如下:
java
/**
* 字段值格式化接口:自定义新旧值的字符串转换规则
*/
@FunctionalInterface
public interface ValueFormatter {
/**
* 格式化字段值
* @param value 原始值(旧值/新值)
* @return 格式化后的字符串
*/
String format(Object value);
/**
* 默认格式化器:等同于String.valueOf(兼容原有逻辑)
*/
ValueFormatter DEFAULT_FORMATTER = value -> value == null ? "" : String.valueOf(value);
}
ValueFormatter 接口仅含一个抽象方法format,用于定义字段值(新旧值)的字符串转换逻辑 ,支持 lambda 表达式简化实现,便于快速定制格式。提供默认的格式化器DEFAULT_FORMATTER 处理简单场景的默认需求。
3. 日志记录接口(DiffLogRecorder)
DiffLogRecorder 是日志记录层的顶层规范,核心是定义 "日志落地" 的统一接口,它不关心日志最终存在哪里(数据库 / 缓存 / MQ),只定义 "必须提供哪些记录能力"。
实现代码如下:
java
/**
* 变更日志记录顶层接口:统一日志记录规范,支持自定义实现
* @param <T> 变更字段封装类型
*/
public interface DiffLogRecorder<T> {
/**
* 记录变更日志核心方法
* @param changedFields 变更字段列表
*/
void record(List<T> changedFields);
/**
* 单字段日志记录(重载,适配单字段场景)
* @param fieldName 字段名
* @param oldVal 旧值
* @param newVal 新值
*/
void record( String fieldName, Object oldVal, Object newVal, ValueFormatter formatter);
}
接口提供了两个重载的 record 方法,分别对应 "多字段批量记录" 和 "单字段自定义格式化记录"。
4. 日志工具类(FieldDiffLogUtil)
核心工具类 FieldDiffLogUtil 负责字段新旧值的对比、变更筛选及日志记录触发。
代码如下:
java
public class FieldDiffLogUtil {
private final List<FieldItem<?>> fieldItems = new ArrayList<>();
private List<FieldItem<?>> changedFieldItems;
private FieldDiffLogUtil() {
}
// 多字段入口
public static FieldDiffLogUtil fieldDiff() {
return new FieldDiffLogUtil();
}
// 单字段入口
public static <T> FieldDiffLogUtil fieldDiff(String key, T oldVal, T newVal, FieldComparator<T> comparator, ValueFormatter formatter) {
FieldDiffLogUtil util = new FieldDiffLogUtil();
util.fieldItems.add(new FieldItem<>(key, oldVal, newVal, comparator, formatter));
util.checkFieldDiff();
return util;
}
// 多字段添加
public <T> FieldDiffLogUtil addField(String fieldName, T oldVal, T newVal, FieldComparator<T> comparator, ValueFormatter formatter) {
fieldItems.add(new FieldItem<>(fieldName, oldVal, newVal, comparator, formatter));
return this;
}
public FieldDiffLogUtil checkFieldDiff() {
this.changedFieldItems = filterChangedFields();
return this;
}
// ---------------------- 多字段记录(支持格式化器) ----------------------
public int batchRecord(DiffLogRecorder<FieldItem<?>> recorder) {
if (changedFieldItems == null) {
this.changedFieldItems = filterChangedFields();
}
int changeCount = changedFieldItems.size();
if (changeCount > 0) {
DiffLogRecordUtil.batchRecord(recorder, changedFieldItems);
}
return changeCount;
}
// ---------------------- 单字段记录(支持格式化器) ----------------------
public int record(DiffLogRecorder<FieldItem<?>> recorder) {
if (changedFieldItems == null) {
this.changedFieldItems = filterChangedFields();
}
int changeCount = changedFieldItems.size();
if (changeCount > 0) {
FieldItem<?> first = changedFieldItems.getFirst();
DiffLogRecordUtil.record(recorder, first.fieldName(), first.oldVal(), first.newVal(), first.formatter());
}
return changeCount;
}
// 私有筛选方法(调用FieldItem内部isChanged())
private List<FieldItem<?>> filterChangedFields() {
return fieldItems.stream()
.filter(FieldItem::isChanged)
.toList();
}
// ---------------------- 内部封装类(字段信息+对比逻辑) ----------------------
public record FieldItem<T>(String fieldName, T oldVal, T newVal, FieldComparator<T> comparator,
ValueFormatter formatter) {
// 内部对比逻辑(泛型安全)
public boolean isChanged() {
return comparator.compare(oldVal, newVal);
}
}
数据载体(FieldItem)
FieldItem 是一个 record(Java 16+ 特性, immutable 数据载体),封装单个字段的完整信息(新旧值)及对比逻辑:
java
public record FieldItem<T>(String fieldName, T oldVal, T newVal,
FieldComparator<T> comparator, ValueFormatter formatter) {
// 判断字段是否变更(委托给比较器)
public boolean isChanged() {
return comparator.compare(oldVal, newVal);
}
}
- 基于 Java 16+ 的
record特性:自动生成getter、equals、hashCode等方法,简化数据载体的定义; - 内置变更判断逻辑:通过
isChanged()调用比较器判断字段是否变更,将对比逻辑委托给外部实现,保证灵活性(无需修改FieldDiffLogUtil即可扩展对比规则)。
核心属性(FieldItems)
类内部维护两个核心集合,支撑字段对比与筛选逻辑:
java
// 存储所有需要对比的字段信息(包含新旧值、比较器等)
private final List<FieldItem<?>> fieldItems = new ArrayList<>();
// 存储经过对比后确定为"已变更"的字段信息(延迟初始化)
private List<FieldItem<?>> changedFieldItems;
fieldItems:作为容器收集所有待对比的字段,通过addField方法添加。changedFieldItems:存储筛选后的变更字段,通过checkFieldDiff方法触发筛选,也可在记录日志时自动触发。
入口方法(fieldDiff)
工具类的构造方法被声明为 private,禁止直接通过 new 实例化:
java
private FieldDiffLogUtil() {}
提供了两种静态入口方法,适配单字段直接对比、多字段批量对比的使用场景,:
-
多字段入口(无初始字段)
javapublic static FieldDiffLogUtil fieldDiff() { return new FieldDiffLogUtil(); }适用于需要批量添加多个字段的场景,创建空实例后通过链式调用
addField方法逐个添加字段,例如:javaFieldDiffLogUtil.fieldDiff() .addField("用户名", "oldName", "newName") .addField("年龄", 18, 20) -
单字段入口(直接初始化字段)
javapublic static <T> FieldDiffLogUtil fieldDiff(String key, T oldVal, T newVal, FieldComparator<T> comparator, ValueFormatter formatter) { FieldDiffLogUtil util = new FieldDiffLogUtil(); util.fieldItems.add(new FieldItem<>(key, oldVal, newVal, comparator, formatter)); util.checkFieldDiff(); return util; }适用于仅需对比单个字段的场景,直接传入字段信息、比较器和格式化器,自动完成字段添加和对比,简化调用流程。
字段添加(addField)
字段添加方法 addField 用于添加字段比较配置,支持链式调用添加多字段:
java
public <T> FieldDiffLogUtil addField(String fieldName, T oldVal, T newVal,
FieldComparator<T> comparator, ValueFormatter formatter) { ... }
字段比较(checkFieldDiff)
checkFieldDiff 方法是触发字段对比的核心逻辑,负责从 fieldItems 中筛选出变更的字段,若未主动调用 checkFieldDiff(),则延迟执行到记录日志时自动执行。
java
public FieldDiffLogUtil checkFieldDiff() {
this.changedFieldItems = filterChangedFields();
return this;
}
private List<FieldItem<?>> filterChangedFields() {
return fieldItems.stream()
.filter(FieldItem::isChanged)
.toList();
}
筛选逻辑依赖 FieldItem 的 isChanged 方法(下文解析)。
日志记录(recordDiffLog)
日志记录提供两种记录方式(单条记录/批量记录),方法负责将调用 DiffLogRecorder 日志记录器记录日志,实现 "对比逻辑" 与 "日志存储" 的解耦:
-
批量记录 :针对多字段场景,调用
DiffLogRecordUtil.batchRecord()方法批量记录日志javapublic int batchRecord(DiffLogRecorder<FieldItem<?>> recorder) { ... DiffLogRecordUtil.batchRecord(...); ... } -
单条记录 :针对单字段场景,取
changedFieldItems中的第一个元素,通过DiffLogRecordUtil.record()记录单条日志javapublic int record(DiffLogRecorder<FieldItem<?>> recorder) { ... int changeCount = changedFieldItems.size(); if (changeCount > 0) { FieldItem<?> first = changedFieldItems.getFirst(); DiffLogRecordUtil.record(...); } ... }
5. 对比器工具类(FieldComparatorUtil)
FieldComparatorUtil 工具类用于集中管理不同类型字段的对比逻辑,通过静态常量或静态方法向外提供不同场景的对比器,让业务代码可以直接引用,无需重复开发。实现代码如下:
java
/**
* 对比器工具类:集中管理基础对比对比逻辑
*/
public class FieldComparatorUtil {
// 1. 基础通用对比器(静态常量,直接调用)
/** 默认对比:null安全,全量匹配 */
public static final FieldComparator<Object> DEFAULT_COMPARATOR =(o1,o2) -> !ObjectUtils.nullSafeEquals(o1,o2);
/** 字符串对比:忽略大小写 */
public static final FieldComparator<String> STR_IGNORE_CASE_COMPARATOR = (oldVal, newVal) -> {
if (ObjectUtils.nullSafeEquals(oldVal, newVal)) return true;
return oldVal != null && oldVal.equalsIgnoreCase(newVal);
};
/** 自定义BigDecimal精度对比 */
public static FieldComparator<BigDecimal> customBigDecimalComparator(int scale) {
return (oldVal, newVal) -> {
// 如果两个值相等,返回false
if (ObjectUtils.nullSafeEquals(oldVal, newVal)) return false;
// 如果有一个值为null,直接返回true
if (oldVal == null || newVal == null) return true;
// 将BigDecimal进行舍入处理
BigDecimal d1 = oldVal.setScale(scale, RoundingMode.HALF_UP);
BigDecimal d2 = newVal.setScale(scale, RoundingMode.HALF_UP);
// 如果不相等,返回true;相等返回false
return d1.compareTo(d2) != 0;
};
}
}
类中定义的对比器均基于上述定义的顶层函数式接口 FieldComparator实现,例如提供的三个对比器:
-
默认对比器:
javapublic static final FieldComparator<Object> DEFAULT_COMPARATOR = (o1, o2) -> !ObjectUtils.nullSafeEquals(o1, o2);依赖
Spring提供的ObjectUtils.nullSafeEquals方法支持所有对象类型,实现null安全的全量匹配。当新旧值不同时返回
true(表示需要记录变更),相同时返回false(无需记录)。 -
字符串忽略大小写对比器:
javapublic static final FieldComparator<String> STR_IGNORE_CASE_COMPARATOR = (oldVal, newVal) -> { if (ObjectUtils.nullSafeEquals(oldVal, newVal)) return true; return oldVal != null && oldVal.equalsIgnoreCase(newVal); };针对字符串类型,忽略大小写差异的对比(如 "ABC" 和 "abc" 视为相同)。
-
BigDecimal 自定义精度对比器:
javapublic static FieldComparator<BigDecimal> customBigDecimalComparator(int scale) { return (oldVal, newVal) -> { if (ObjectUtils.nullSafeEquals(oldVal, newVal)) return false; if (oldVal == null || newVal == null) return true; BigDecimal d1 = oldVal.setScale(scale, RoundingMode.HALF_UP); BigDecimal d2 = newVal.setScale(scale, RoundingMode.HALF_UP); return d1.compareTo(d2) != 0; }; }针对
BigDecimal类型,可按照指定精度 判断两个BigDecimal值是否发生变更(而非原始值的绝对相等),同时支持null安全对比,适配金额、数量等需要灵活控制精度的业务场景。
6. 日志记录工具类(DiffLogRecordUtil)
DiffLogRecordUtil 是连接FieldDiffLogUtil 工具类与 DiffLogRecorder 记录器的中间工具,仅负责 "日志记录流程控制"(参数校验、异常处理、方法路由)。
通过 DiffLogRecorder 接口依赖,降低 FieldDiffLogUtil 工具类对 DiffLogRecorder 接口实现类的直接依赖复杂度,将具体记录逻辑(如存库、发消息)与工具类解耦,调用者可根据业务需求实现不同的 DiffLogRecorder,工具类无需修改即可适配,有效解决重复编码问题。
实现代码如下:
java
public class DiffLogRecordUtil {
private DiffLogRecordUtil() {}
// ---------------------- 批量记录(带格式化器) ----------------------
public static <T> void batchRecord(DiffLogRecorder<T> recorder, List<T> changedFields) {
if (recorder == null) {
log.error("日志记录器未指定,记录失败");
throw new IllegalArgumentException("DiffLogRecorder cannot be null");
}
if (changedFields == null || changedFields.isEmpty()) {
return;
}
try {
recorder.record(changedFields);
} catch (Exception e) {
log.error("批量日志记录失败");
throw new RuntimeException("批量日志记录异常", e);
}
}
// ---------------------- 单字段记录(带格式化器) ----------------------
public static <T> void record(DiffLogRecorder<T> recorder, String key, Object oldVal, Object newVal, ValueFormatter formatter) {
if (recorder == null) {
log.error("日志记录器未指定,记录失败");
throw new IllegalArgumentException("DiffLogRecorder cannot be null");
}
try {
recorder.record(key, oldVal, newVal, formatter);
} catch (Exception e) {
log.error("单字段日志记录失败");
throw new RuntimeException("单字段日志记录异常", e);
}
}
public static <T> void record(DiffLogRecorder<T> recorder, String fieldName, Object oldVal, Object newVal) {
record(recorder, fieldName, oldVal, newVal, ValueFormatter.DEFAULT_FORMATTER);
}
}
类中提供了静态方法(batchRecord 用于批量记录,record 用于单字段记录)简化日志记录调用:
batchRecord():用于一次性记录多个字段的变更信息(如对象整体更新时的多字段变更)record:记录单个字段的变更
三、扩展教程
1. 自定义比较器
FieldComparatorUtil工具类已内置常用对比器实现,若需自定义对比逻辑,只需实现FieldComparator接口即可。以下两种自定义方式可分别应对不同场景:
- 在
FieldComparatorUtil中扩展通用对比器(适用于简单逻辑) - 实现
FieldComparator接口创建复杂对比器(适用于复杂逻辑)
在 FieldComparatorUtil 中扩展通用对比器
该方式适用于逻辑简单、可复用的通用对比规则,可直接通过 lambda 表达式定义,以静态常量(适用于固定逻辑)或静态方法(适用于带配置参数的场景)的形式扩展到FieldComparatorUtil中。
示例:字符串忽略前后空格对比
当需要忽略字符串前后空格判断是否有变化时,可定义如下对比器:
java
/** 字符串对比器:忽略前后空格后判断是否有变化 */
public static final FieldComparator<String> STR_IGNORE_WHITESPACE_COMPARATOR = (oldVal, newVal) -> {
// 空值校验:若原值与新值完全相等(包括均为null),则无变化
if (ObjectUtils.nullSafeEquals(oldVal, newVal)) {
return false;
}
// 非空处理:trim后对比
String trimmedOld = oldVal == null ? "" : oldVal.trim();
String trimmedNew = newVal == null ? "" : newVal.trim();
// trim后不相等则视为有变化
return !trimmedOld.equals(trimmedNew);
};
实现 FieldComparator 接口自定义复杂对比器
适用于逻辑复杂、需要维护状态(如配置参数)或复用性较低的场景(例如特定业务对象的对比)。此时需创建实现FieldComparator接口的类,封装完整的对比逻辑。
示例:日期时间范围对比器
以下对比器用于判断两个 LocalDateTime 对象的时间差是否超过指定分钟阈值,超过则视为有变化:
java
/**
* 日期时间范围对比器
* 功能:比较两个LocalDateTime对象,当时间差超过指定分钟阈值时视为有变化
*/
@Data
@AllArgsConstructor
public class DateTimeRangeComparator implements FieldComparator<LocalDateTime> {
// 时间差阈值(单位:分钟):超过此值则判定为有变化
private int minuteThreshold;
@Override
public boolean compare(LocalDateTime oldVal, LocalDateTime newVal) {
// 空值校验:若原值与新值完全相等(包括均为null),则无变化
if (ObjectUtils.nullSafeEquals(oldVal, newVal)) {
return false;
}
// 非对称空值:若一方为null另一方非null,视为有变化
if (oldVal == null || newVal == null) {
return true;
}
// 计算时间差(取绝对值,单位:分钟)
long minutesDiff = Math.abs(ChronoUnit.MINUTES.between(oldVal, newVal));
// 超过阈值则视为有变化
return minutesDiff > minuteThreshold;
}
}
2. 自定义格式化器
自定义格式化器需实现 ValueFormatter 接口并覆写 format 方法,主要支持以下两种扩展方式,可根据实际场景灵活选择:
- 在
ValueFormatter接口中扩展通用格式化器 - 实现
ValueFormatter接口自定义复杂格式化器
在 ValueFormatter 接口中扩展通用格式化器
以静态常量(固定格式场景)或静态方法(带参数场景)的形式嵌入 ValueFormatter 接口,支持通过 Lambda 表达式简化实现。适用于逻辑简单、无状态(无需维护配置参数)且可全局复用的格式化规则(例如固定格式的日期转换、通用数字格式化等场景)。
示例:固定格式日期时间格式化器
LocalDateTime 或 Date 类型值统一格式化为 yyyy-MM-dd HH:mm:ss,null 值返回空字符串。
java
/**
* 日期时间格式化器:将时间类型值格式化为 yyyy-MM-dd HH:mm:ss 格式
*/
static ValueFormatter dateFormatter() {
return value -> {
if (value == null) {
return "";
}
if (value instanceof LocalDateTime) {
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format((LocalDateTime) value);
}
if (value instanceof Date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
.format((Date) value);
}
// 非时间类型默认返回原值字符串
return String.valueOf(value);
};
}
实现 ValueFormatter 接口自定义复杂格式化器
创建独立类并实现 ValueFormatter<T> 接口,支持通过类的成员变量维护状态信息。适用于逻辑复杂、需要维护状态(如枚举映射关系、多格式配置)或仅针对特定业务场景的格式化规则(例如枚举值转中文名称、自定义业务编码解析等)。
示例:订单状态枚举格式化器
将订单状态编码(如 1、2、3)转换为对应的中文名称(如 "待支付"、"已支付")。
java
/**
* 订单状态枚举格式化器:将订单状态编码转换为对应的中文名称
*/
@Data
@AllArgsConstructor
public class OrderStatusFormatter implements ValueFormatter<Integer> {
// 支持自定义状态映射关系(灵活适配不同业务场景)
private final Map<Integer, String> statusMapping;
/**
* 提供默认映射关系的构造方法(简化常规场景使用)
*/
public OrderStatusFormatter() {
this.statusMapping = new HashMap<>();
statusMapping.put(1, "待支付");
statusMapping.put(2, "已支付");
statusMapping.put(3, "已取消");
statusMapping.put(4, "已完成");
}
@Override
public String format(Integer statusCode) {
// 处理空值或未定义的状态编码
if (ObjectUtils.isEmpty(statusCode) || !statusMapping.containsKey(statusCode)) {
return "未知状态";
}
return statusMapping.get(statusCode);
}
}
3. 自定义日志记录器
自定义日志记录器需要实现DiffLogRecorder接口,并指定泛型类型为FieldDiffLogUtil.FieldItem<?>,实现接口的两个record方法,在方法中可根据业务需求实现不同的日志记录逻辑(如用户日志记录器、商品日志记录器)。
示例:用户信息变更,记录 TB_CHGS 变更日志表
调用配置的ValueFormatter格式化器对字段(新旧值)进行格式化,使用格式化后的值构建日志实体,进行持久化:
java
@Slf4j
public class ChgsLogRecorder implements DiffLogRecorder<FieldDiffLogUtil.FieldItem<?>> {
@Override
public void record(List<FieldDiffLogUtil.FieldItem<?>> changedFields) {
// 批量转换为日志实体,插入chgs表
List<ChgsEntity> chgsList = changedFields.stream()
.map(item -> new ChgsEntity(
item.fieldName(),
item.formatter().format(item.oldVal()),
item.formatter().format(item.newVal())
))
.collect(Collectors.toList());
log.info("批量插入CHGS日志{}",chgsList);
}
@Override
public void record(String fieldName, Object oldVal, Object newVal, ValueFormatter formatter) {
ChgsEntity chgs = new ChgsEntity(
fieldName,
formatter.format(oldVal),
formatter.format(newVal)
);
log.info("单挑插入CHGS日志{}",chgs);
}
}
四、使用教程
FieldDiffLogUtil 工具类支持单字段变更和多字段变更的场景:
-
单字段变更 :记录单条日志可直接链式调用
fieldDiff(...).record(...),例如:java// 1. 初始化字段差异日志构建器 FieldDiffLogUtil // 2. 添加对比配置:参数为【字段名】、【旧值】、【新值】、【比较器】、【格式化器】 .fieldDiff("order_amount", new BigDecimal("222.222"), new BigDecimal("222.233"), FieldComparatorUtil.customBigDecimalComparator(2), ValueFormatter.DEFAULT_FORMATTER) // 3. 单字段记录变更日志: .record(new CommonChgsLogRecorder()); -
多字段变更 :多字段变更支持
fieldDiff().addField (...).addField (...).batchRecord (...)链式调用,例如:java// 1. 初始化字段差异日志构建器 FieldDiffLogUtil.fieldDiff() // 2. 依次添加对比配置:参数为【字段名】、【旧值】、【新值】、【比较器】、【格式化器】 .addField("user_name","111","111", FieldComparatorUtil.DEFAULT_COMPARATOR, ValueFormatter.DEFAULT_FORMATTER) .addField("user_email","111","111", FieldComparatorUtil.DEFAULT_COMPARATOR, ValueFormatter.DEFAULT_FORMATTER) .addField("user_phone","111","111", FieldComparatorUtil.DEFAULT_COMPARATOR, ValueFormatter.DEFAULT_FORMATTER) // 3. 批量记录变更日志:将所有有变更的字段(此处无变更则不记录)传递给通用日志记录器 .batchRecord(new CommonChgsLogRecorder());
支持重载方法 灵活配置,例如重载 addField() 方法,默认指定对比器、格式化器:
java
public FieldDiffLogUtil addField(String fieldName, Object oldVal, Object newVal) {
fieldItems.add(new FieldItem<>(fieldName, oldVal, newVal, FieldComparatorUtil.DEFAULT_COMPARATOR, ValueFormatter.DEFAULT_FORMATTER));
return this;
}
调用时如下:
java
FieldDiffLogUtil.fieldDiff()
.addField("user_name","111","111")
.addField("user_email","111","111")
.addField("user_phone","111","111")
.batchRecord(new CommonChgsLogRecorder());
五、使用示例
1. 单字段 + 默认比较器
java
/**
* 单字段 + 默认比较器示例
*/
private static void singleFieldExample() {
String oldStr = "Hello";
String newStr = "hello";
// 构建单字段比较器:使用字符串忽略大小写比较器,默认格式化器
int changeCount = FieldDiffLogUtil.fieldDiff(
"userName",
oldStr,
newStr,
).record(new ChgsLogRecorder()); // 批量记录变更日志到 TB_CHGS 表
System.out.println("单字段变更数量:" + changeCount);
}
2. 单字段 + 自定义比较器(字符串忽略大小写)
java
/**
* 单字段 + 自定义比较器(字符串忽略大小写)
*/
private static void singleFieldExample() {
String oldStr = "Hello";
String newStr = "hello";
// 构建单字段比较器:使用字符串忽略大小写比较器
int changeCount = FieldDiffLogUtil.fieldDiff(
"userName",
oldStr,
newStr,
FieldComparatorUtil.STR_IGNORE_CASE_COMPARATOR,
ValueFormatter.DEFAULT_FORMATTER
).record(new ChgsLogRecorder()); // 记录变更日志到 TB_CHGS 表
System.out.println("单字段变更数量:" + changeCount);
}
3. 多字段 + 自定义比较
java
/**
* 多字段 + 自定义比较器示例
*/
private static void multiFieldExample() {
// 测试数据
String oldName = "张三";
String newName = "李四";
BigDecimal oldPrice = new BigDecimal("10.123");
BigDecimal newPrice = new BigDecimal("10.126");
LocalDateTime oldTime = LocalDateTime.now().minusDays(1);
LocalDateTime newTime = LocalDateTime.now();
// 构建多字段比较器
int changeCount = FieldDiffLogUtil.fieldDiff()
// 字段1:姓名(默认比较器:精确匹配)
.addField("name", oldName, newName)
// 字段2:价格(自定义BigDecimal精度:保留2位小数)
.addField(
"price",
oldPrice,
newPrice,
FieldComparatorUtil.customBigDecimalComparator(2),
ValueFormatter.DEFAULT_FORMATTER
)
// 字段3:LocalDateTime(日期格式化:yyyy-MM-dd)
.addField(
"createTime",
oldTime,
newTime,
FieldComparatorUtil.DEFAULT_COMPARATOR,
ValueFormatter.dateFormatter("yyyy-MM-dd")
)
// 批量记录变更日志到 TB_CHGS 表
.batchRecord(new ChgsLogRecorder());
System.out.println("多字段变更数量:" + changeCount);
}
🔗 项目仓库地址:
为方便读者实操练习,本教程所有示例代码已开源至 GitCode/GitHub 仓库,可直接克隆使用或参考学习:
- GitHub:field-diff-log-tool
- GitCode:ShiJieCloud/field-diff-log-tool