Java工具类:CompareUtils(比较对象字段值变化)

目录

前言

示例引入

工具文件

CompareUtils

Compare 

CompareNode 


前言

我们在工作中,可能会在日志中记录数据的变化情况,这样可以有助于运维人员对问题排查。

或者在公共处理的数据增加一个日志页面,记录每次修改的变化。比如:小王在 2022-05-05 23:58:27 时间点修改了年龄,把 28 改为了 18), 这样也可以方便用户的数据追踪,这样公共数据一旦出了问题,我们从日志进行跟踪,那么最后是谁造成的,也就一目了然。

可见,展示字段值的变化,是一个较为实用的功能。

示例引入

那么,我们既然要记录每次保存前后的字段值变化,则肯定需要进行字段值的比较。

比如,我们下面有一个 User 对象:

@Data
public class User {

    private String name;
    private int age;

}

我们如果要记录每次 User 对象保存的前后变化,可能大家最容易想到的,就是对每个字段依次进行比较,获取结果,具体操作如下:

@Data
public class User {

    private String name;
    private int age;

    /**
     * 获取比较结果
     * tips: 这里我们做演示,先忽略比较对象和比较值为 null 的情况
     *
     * @param u 待比较对象
     * @return 比较结果
     */
    public String compare(User u) {
        StringBuilder sb = new StringBuilder();
        if (!u.getName().equals(this.name)) {
            sb.append(String.format("[姓名:%s -> %s], ", u.getName(), this.name));
        }
        if (u.getAge() != this.age) {
            sb.append(String.format("[年龄:%d -> %d], ", u.getAge(), this.age));
        }
        return sb.toString();
    }

}

 我们不妨来写个示例测试一下:

public class Test {

    public static void main(String[] args) {
        // 模拟一个保存前的数据对象
        User u1 = new User();
        u1.setName("大乔");
        u1.setAge(24);
        // 模拟一个保存后的数据对象
        User u2 = new User();
        u2.setName("小乔");
        u2.setAge(22);
        // 获取保存前后的字段变化情况
        String result = u2.compare(u1);
        System.out.println(result);
    }

}

 我们运行程序,可以发现,已经成功获取到了每个字段的数据变化情况:

设想下,如果随着业务的拓展,当我们的 2 个字段增加到多个字段的时候。

如果这个时候,我们再依次去比较每个字段,是不是就显得有点繁琐了。并且逻辑性也高度重合,这时的代码就会显得十分的臃肿,那么,这个时候,我们就不妨考虑写一个通用的方法了。

工具文件

在写这个通用方法时,我们应该考虑到以下几点:

(1)可以接收任何对象的比较,但比较的对象应该是同个对象;

(2)可以给字段进行一个备注,因为我们看到的最终内容,应该是一个中文名称;

(3)一个对象中,可以忽略某些字段进行比较,只要我需要的字段进行比较。

于是,针对上面的需求,我写了一个通用的比较方法,具体包含下面三个文件:

CompareUtils

package com.zyqok.utils.compare;

import java.lang.reflect.Field;
import java.util.*;

/**
 * 使用须知: <br>
 * (1)该工具类主要用于两个同类对象的属性值比较; <br>
 * (2)使用本工具类前,请将对应的类属性上打上 @Compare("xxx") 注解,其中xxx为字段的表达名称;<br>
 * (3)为了比较灵活,只有打了该注解才会进行比较,不打的字段则不会进行比较 <br>
 * (4)比较后,只会返回有变化的字段,无变化的字符则不返回 <br>
 *
 * @author zyqok
 * @since 2021/05/05
 */
public class CompareUtils<T> {

    private static final String COMMA = ",";

    /**
     * 属性比较
     *
     * @param source 源数据对象
     * @param target 目标数据对象
     * @return 对应属性值的比较变化
     */
    public String compare(T source, T target) {
        return compare(source, target, null);
    }


    /**
     * 属性比较
     *
     * @param source 源数据对象
     * @param target 目标数据对象
     * @param ignoreCompareFields 忽略比较的字段
     * @return 对应属性值的比较变化
     */
    public String compare(T source, T target, List<String> ignoreCompareFields) {
        if (Objects.isNull(source) && Objects.isNull(target)) {
            return "";
        }
        Map<String, CompareNode> sourceMap = this.getFiledValueMap(source);
        Map<String, CompareNode> targetMap = this.getFiledValueMap(target);
        if (sourceMap.isEmpty() && targetMap.isEmpty()) {
            return "";
        }
        // 如果源数据为空,则只显示目标数据,不显示属性变化情况
        if (sourceMap.isEmpty()) {
            return doEmpty(targetMap, ignoreCompareFields);
        }
        // 如果源数据为空,则显示属性变化情况
        String s = doCompare(sourceMap, targetMap, ignoreCompareFields);
        if (!s.endsWith(COMMA)) {
            return s;
        }
        return s.substring(0, s.length() - 1);
    }

    private String doEmpty(Map<String, CompareNode> targetMap, List<String> ignoreCompareFields) {
        StringBuilder sb = new StringBuilder();
        Collection<CompareNode> values = targetMap.values();
        int size = values.size();
        int current = 0;
        for (CompareNode node : values) {
            current++;
            Object o = Optional.ofNullable(node.getFieldValue()).orElse("");
            if (Objects.nonNull(ignoreCompareFields) && ignoreCompareFields.contains(node.getFieldKey())) {
                continue;
            }
            if (o.toString().length() > 0) {
                sb.append("[" + node.getFieldName() + ":" + o + "]");
                if (current < size) {
                    sb.append(COMMA);
                }
            }
        }
        return sb.toString();
    }

    private String doCompare(Map<String, CompareNode> sourceMap, Map<String, CompareNode> targetMap, List<String> ignoreCompareFields) {
        StringBuilder sb = new StringBuilder();
        Set<String> keys = sourceMap.keySet();
        int size = keys.size();
        int current = 0;
        for (String key : keys) {
            current++;
            CompareNode sn = sourceMap.get(key);
            CompareNode tn = targetMap.get(key);
            if (Objects.nonNull(ignoreCompareFields) && ignoreCompareFields.contains(sn.getFieldKey())) {
                continue;
            }
            String sv = Optional.ofNullable(sn.getFieldValue()).orElse("").toString();
            String tv = Optional.ofNullable(tn.getFieldValue()).orElse("").toString();
            // 只有两者属性值不一致时, 才显示变化情况
            if (!sv.equals(tv)) {
                sb.append(String.format("[%s:%s -> %s]", sn.getFieldName(), sv, tv));
                if (current < size) {
                    sb.append(COMMA);
                }
            }
        }
        return sb.toString();
    }

    private Map<String, CompareNode> getFiledValueMap(T t) {
        if (Objects.isNull(t)) {
            return Collections.emptyMap();
        }
        Field[] fields = t.getClass().getDeclaredFields();
        if (Objects.isNull(fields) || fields.length == 0) {
            return Collections.emptyMap();
        }
        Map<String, CompareNode> map = new LinkedHashMap();
        for (Field field : fields) {
            Compare compareAnnotation = field.getAnnotation(Compare.class);
            if (Objects.isNull(compareAnnotation)) {
                continue;
            }
            field.setAccessible(true);
            try {
                String fieldKey = field.getName();
                CompareNode node = new CompareNode();
                node.setFieldKey(fieldKey);
                node.setFieldValue(field.get(t));
                node.setFieldName(compareAnnotation.value());
                map.put(field.getName(), node);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return map;
    }

}

Compare 

package com.zyqok.utils.compare;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 字段标记注解
 *
 * @author zyqok
 * @since 2022/05/05
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Compare {

    /**
     * 字段名称
     */
    String value();
}

CompareNode 

package com.zyqok.utils.compare;

/**
 * @author zyqok
 * @since 2022/05/05
 */
public class CompareNode {

    /**
     * 字段
     */
    private String fieldKey;

    /**
     * 字段值
     */
    private Object fieldValue;

    /**
     * 字段名称
     */
    private String fieldName;

    public String getFieldKey() {
        return fieldKey;
    }

    public void setFieldKey(String fieldKey) {
        this.fieldKey = fieldKey;
    }

    public Object getFieldValue() {
        return fieldValue;
    }

    public void setFieldValue(Object fieldValue) {
        this.fieldValue = fieldValue;
    }

    public String getFieldName() {
        return fieldName;
    }

    public void setFieldName(String fieldName) {
        this.fieldName = fieldName;
    }
}

那么,我们在使用的时候,只需要将这三个文件复制在工程中即可:

我们去掉原比较方法,然后在对应字段上打上 @Compare 注解,写上对应字段名称。

最后测试,调用 CompareUtils 类中的 compare 方法即可,示例如下:

public class Test {

    public static void main(String[] args) {
        // 模拟一个保存前的数据对象
        User u1 = new User();
        u1.setName("大乔");
        u1.setAge(24);
        // 模拟一个保存后的数据对象
        User u2 = new User();
        u2.setName("小乔");
        u2.setAge(22);
        // 获取保存前后的字段变化情况
        String result = new CompareUtils<User>().compare(u1, u2);
        System.out.println(result);
    }

}

运行结果如下:可以看到,和我们之前的比较结果是一致的。

如果不想比较某个字段,则把对应的字段传入到参数中即可,比如,这里我不想比较 name 这个字段。

该场景适用于一个对象用于多个比较场景的时候,可以把不需要进行比较的字段进行过滤。

比如,对象中有 ABCD 四个字段,场景一只比较 ABC 字段,场景二只比较 BCD 字段,但两个场景都想共用一个实体,那么场景一进行比较的时候,就可以过滤 D 字段,场景二进行比较的时候,就可以过滤 A 字段。

  • 18
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值