告别重复编码!SpringBoot 字段变更(新旧值)日志工具类的规范化设计与优雅实现

一、序言

👉 在项目开发中,「字段变更日志记录」是贯穿多业务场景的高频需求 ------ 无论是用户信息修改(如手机号、邮箱更新),还是配置参数调整,都需要精准记录字段新旧值、操作人、操作时间等关键信息,用于后续审计追溯、问题排查或数据回溯。

1. 传统实现的痛点:重复与混乱

但在实际多人协作开发中,这类需求往往陷入「重复造轮子」的困境,主要体现在两大问题:

  1. 重复编码严重:每个业务接口(如用户模块、订单模块)都需单独封装日志记录逻辑:从获取实体新旧值、对比字段差异,到构建日志对象、写入数据库,核心逻辑高度一致,仅实体类和字段不同,却要重复编写大量冗余代码(典型的「复制粘贴式开发」)。
  2. 标准杂乱无章 :缺乏统一日志标准,不同开发者实现逻辑差异显著:例如新旧值比较规则不一致、存储格式混乱(如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. 核心组件分层:职责单一与接口抽象

为实现流程复用和灵活扩展,工具类采用四层分层设计,对应上述核心流程,分层原则为 "职责单一 + 接口抽象"(每层仅承担单一职责,确保组件解耦):

  1. 对比层:统一对比规则,仅负责 "判断是否变更",避免业务代码中散落重复的对比逻辑;
  2. 格式化层:日志内容的 "标准化处理器",将任意类型原始值转为统一、可读的字符串,解决不同数据类型日志展示不一致问题;
  3. 记录层:日志输出的 "执行器",负责具体的日志信息存储(如数据库、MQ 等);
  4. 协调层:工具类的 "对外入口 + 流程调度中枢",封装前三层交互逻辑,让调用方无需关注内部细节,以极简代码完成操作,降低使用门槛。

3. 组件规范化设计:接口定义与交互流程

分层后,针对各层组件定义标准化接口,通过 "接口定标准、实现类落地细节" 的方式,确保组件间仅依赖接口、不依赖具体实现,提升扩展性:

组件 职责描述
FieldComparator<T> 字段对比顶层接口,定义字段新旧值的对比逻辑
ValueFormatter 值格式化接口,简化自定义格式化逻辑(如日期转 "yyyy-MM-dd" 格式)
DiffLogRecorder<T> 变更日志记录接口,屏蔽不同存储渠道的实现差异(如数据库、MQ)
FieldDiffRecordUtil 对外暴露 API,支持单字段 / 多字段添加,内部自动串联 "对比→筛选→格式化→记录" 全流程

完整交互流程从业务调用开始:

  1. 业务侧通过DiffLogRecordUtil添加需要对比的字段,可配置指定对比器、格式化器(可选,默认使用通用实现)。
  2. DiffLogRecordUtil 调用比较器 FieldComparator,完成新旧字段值的对比,筛选出发生变更的字段。
  3. DiffLogRecordUtil 携带变更字段信息,调用日志记录器 DiffLogRecorder
  4. 日志记录器 DiffLogRecorder 调用格式化器 ValueFormatter,对变更字段的旧值、新值进行统一格式化(如日期转字符串、敏感信息脱敏)。
  5. 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>声明字段类型,使其能适配任意数据类型。无论是基础类型(如BigDecimalDate)还是自定义实体(如UserOrder),都能通过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 特性:自动生成 getterequalshashCode 等方法,简化数据载体的定义;
  • 内置变更判断逻辑:通过 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() {}

提供了两种静态入口方法,适配单字段直接对比、多字段批量对比的使用场景,:

  1. 多字段入口(无初始字段)

    java 复制代码
    public static FieldDiffLogUtil fieldDiff() {
        return new FieldDiffLogUtil();
    }

    适用于需要批量添加多个字段的场景,创建空实例后通过链式调用 addField 方法逐个添加字段,例如:

    java 复制代码
    FieldDiffLogUtil.fieldDiff()
        .addField("用户名", "oldName", "newName")
        .addField("年龄", 18, 20)
  2. 单字段入口(直接初始化字段)

    java 复制代码
    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;
    }

    适用于仅需对比单个字段的场景,直接传入字段信息、比较器和格式化器,自动完成字段添加和对比,简化调用流程。

字段添加(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();
}

筛选逻辑依赖 FieldItemisChanged 方法(下文解析)。

日志记录(recordDiffLog)

日志记录提供两种记录方式(单条记录/批量记录),方法负责将调用 DiffLogRecorder 日志记录器记录日志,实现 "对比逻辑" 与 "日志存储" 的解耦:

  • 批量记录 :针对多字段场景,调用 DiffLogRecordUtil.batchRecord() 方法批量记录日志

    java 复制代码
    public int batchRecord(DiffLogRecorder<FieldItem<?>> recorder) {
        ...
        DiffLogRecordUtil.batchRecord(...);
        ...
    }
  • 单条记录 :针对单字段场景,取 changedFieldItems 中的第一个元素,通过 DiffLogRecordUtil.record() 记录单条日志

    java 复制代码
    public 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实现,例如提供的三个对比器:

  1. 默认对比器:

    java 复制代码
    public static final FieldComparator<Object> DEFAULT_COMPARATOR = 
        (o1, o2) -> !ObjectUtils.nullSafeEquals(o1, o2);

    依赖Spring 提供的 ObjectUtils.nullSafeEquals 方法支持所有对象类型,实现 null 安全的全量匹配。

    当新旧值不同时返回 true(表示需要记录变更),相同时返回 false(无需记录)。

  2. 字符串忽略大小写对比器:

    java 复制代码
    public 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" 视为相同)。

  3. BigDecimal 自定义精度对比器:

    java 复制代码
    public 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接口即可。以下两种自定义方式可分别应对不同场景:

  1. FieldComparatorUtil中扩展通用对比器(适用于简单逻辑)
  2. 实现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 方法,主要支持以下两种扩展方式,可根据实际场景灵活选择:

  1. ValueFormatter 接口中扩展通用格式化器
  2. 实现 ValueFormatter 接口自定义复杂格式化器
在 ValueFormatter 接口中扩展通用格式化器

以静态常量(固定格式场景)或静态方法(带参数场景)的形式嵌入 ValueFormatter 接口,支持通过 Lambda 表达式简化实现。适用于逻辑简单、无状态(无需维护配置参数)且可全局复用的格式化规则(例如固定格式的日期转换、通用数字格式化等场景)。

示例:固定格式日期时间格式化器

LocalDateTimeDate 类型值统一格式化为 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 工具类支持单字段变更和多字段变更的场景:

  1. 单字段变更 :记录单条日志可直接链式调用 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());
  2. 多字段变更 :多字段变更支持 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 仓库,可直接克隆使用或参考学习:

相关推荐
哥谭居民00011 小时前
需求分析,领域划分到选择套用业务模式到转化落地,两个基本案例
java·大数据·需求分析
Tao____1 小时前
适合中小型项目的物联网平台
java·物联网·mqtt·开源·iot
小马爱打代码1 小时前
Spring AI:多模态 AI 大模型
java·人工智能·spring
开心猴爷1 小时前
Bundle Id 创建与管理的工程化方法,一次团队多项目协作中的流程重构
后端
databook1 小时前
用样本猜总体的秘密武器,4大抽样分布总结
后端·python·数据分析
李贺梖梖1 小时前
day07 方法、面向对象1
java
除了代码啥也不会1 小时前
Java基于SSE流式输出实战
java·开发语言·交互
虹科网络安全1 小时前
艾体宝干货 | Redis Java 开发系列#2 数据结构
java·数据结构·redis
sg_knight1 小时前
SSE 技术实现前后端实时数据同步
java·前端·spring boot·spring·web·sse·数据同步