1 前言
大家平时写业务代码的时候,应该能感知到哪些是基础配置数据,哪些是实例数据。比如营销里的活动信息、促销信息就属于配置型数据,基于活动带来的订单参与活动信息属于实例数据。比如一些规则信息、流程信息等类似一种版本的概念。那么版本跟版本之间的差异、以及创建新版本的时候,什么也没变化到底生不生成版本信息呢?如何知道版本跟版本之间有没有差异呢?是不是就要比对两个版本之间的数据。
版本和版本之间的比对,我们大概能想到的方式:
- Json化后的比较,JSON常用的有 Jackson、Gson、Fastjson,这个的比较方式下节说
- Java实体间的比较,比如从数据库里取出来的实体信息和当前请求的实体信息间的比较,本节主要看下这个
- 实体按某种规则顺序化后的字符串或者某种规则文本的比较,比如将你的实体信息按字典或者某种规则化后得到一个字符串或者规则文本进行比较,有点类似 Json,Json本身也是一种有规则的文本,但这种方式更偏向于我们自己定义的规则而非Json,所以我单独拿出来
大概我能想到的是上边这三种方式,然后我们本节主要看下第二种方式,Java 实体间的比较。
2 实践
2.1 代码
我这里就直接贴代码了哈,大概定义了三个类:
- CompareResultInfo 比较结果实体信息
- CompareDetailInfo 差异明细信息
- CompareInfoUtil 入口以及核心比较类
CompareResultInfo 比较结果实体信息:
@Data public class CompareResultInfo {// 比较结果 true 表示两个对象相等,false 表示两个对象不相等private boolean equalFlag;// 比较开始时间private long startCompareTime;// 比较结束时间private long endCompareTime;// 比较总耗时private long totalCompareTime;// 比较差异详情private List<CompareDetailInfo> compareDetailInfos; }
CompareDetailInfo 差异明细信息:
@Builder @Data public class CompareDetailInfo {// 差异字段private Field field;// 旧值private Object oldVal;// 新值private Object newVal; }
CompareInfoUtil 入口以及核心比较类:
/*** 比较工具类* @author kuku* @param*/ @Data public class CompareInfoUtil {// 入口public static CompareResultInfo compareObj(Class clz, Object oldVal, Object newVal) {// 结果信息CompareResultInfo resultInfo = new CompareResultInfo();// 开始比较 startCompare(resultInfo);try {// 基础检查if (clz == null) throw new RuntimeException("clazz,比较的类不能为空");if (oldVal == null) throw new RuntimeException("oldVal,旧值不能为空");if (newVal == null) throw new RuntimeException("newVal,新值不能为空");// 执行比较List<CompareDetailInfo> compareDetailInfos = doCompareObj(clz, oldVal, newVal);// 差异resultInfo.setEqualFlag(compareDetailInfos.size() <= 0);resultInfo.setCompareDetailInfos(compareDetailInfos);return resultInfo;} finally {// 结束比较 endCompare(resultInfo);}}private static void startCompare(CompareResultInfo resultInfo) {// 设置开始时间 resultInfo.setStartCompareTime(System.currentTimeMillis());}private static void endCompare(CompareResultInfo resultInfo) {// 设置结束时间 resultInfo.setEndCompareTime(System.currentTimeMillis());// 计算耗时long startCompareTime = resultInfo.getStartCompareTime();long endCompareTime = resultInfo.getEndCompareTime();long total = endCompareTime - startCompareTime;resultInfo.setTotalCompareTime(total);System.out.println(String.format("开始时间:%s,结束时间:%s,耗费时间(毫秒):%s", startCompareTime, endCompareTime, total));}private static List<CompareDetailInfo> doCompareObj(Class clz, Object oldVal, Object newVal) {List<CompareDetailInfo> currentCompareDetailInfoList = new ArrayList<>();// 先比较父类的字段 除了 ObjectClass superclass = clz.getSuperclass();if (superclass != null && superclass != Object.class) {List<CompareDetailInfo> parentInfos = doCompareObj(superclass, oldVal, newVal);if (CollectionUtils.isNotEmpty(parentInfos)) {currentCompareDetailInfoList.addAll(parentInfos);}}// 获取当前类中的字段Field[] fields = clz.getDeclaredFields();// 逐个比较for (Field field : fields) {field.setAccessible(true);// 当前字段类型Class<?> fieldType = field.getType();Object ov = null;Object nv = null;try {ov = field.get(oldVal);nv = field.get(newVal);} catch (Exception e) {// ignore }// 比较当前属性boolean equalFlag = doCompareAttr(fieldType, ov, nv);// 不相等 汇总明细信息if (!equalFlag) {describeOneCompareDetail(currentCompareDetailInfoList, field, ov, nv);}}return currentCompareDetailInfoList;}private static boolean doCompareAttr(Class clz, Object oldVal, Object newVal) {boolean res = true;// 两个都为空 直接略过if (oldVal == null && newVal == null) {return res;}// 有一个为空if (oldVal == null || newVal == null) {res = false;} else {if (clz.isArray()) {res = doCompareArr(clz, oldVal, newVal);} else if (oldVal instanceof Collection) {res = doCompareCollection(clz, oldVal, newVal);} else if (oldVal instanceof Map) {res = doCompareMap(clz, oldVal, newVal);} else {// 为什么是 ov.getClass 不是 fieldType 呢// 因为当是基本数据类型的时候 比如 int 它的 fieldType 是 int 类// 没重写 equals 而得到的 ov nv 都是包装后的对象 所以直接拿对象的 getClass()if (hasOverriddenEquals(oldVal.getClass())) {res = oldVal.equals(newVal);} else {List<CompareDetailInfo> compareDetailInfos = doCompareObj(clz, oldVal, newVal);if (CollectionUtils.isNotEmpty(compareDetailInfos)) {res = false;}}}}return res;}/*** 比较 Map 类型的* @param clz* @param ov* @param nv* @return*/private static boolean doCompareMap(Class<?> clz, Object ov, Object nv) {boolean res = true;Map ov1 = (Map) ov;Map nv1 = (Map) nv;// 长度不相等直接略过if (ov1.size() != nv1.size()) {res = false;return res;}Set keys = ov1.keySet();for (Object key : keys) {Object o1 = ov1.get(key);Object o2 = nv1.get(key);res = doCompareAttr(o1.getClass(), o1, o2);if (!res) {break;}}return res;}/*** 比较集合类型的 这里我暂时直接把集合转成了数组 直接调数组的方法* @param clz* @param ov* @param nv* @return*/private static boolean doCompareCollection(Class<?> clz, Object ov, Object nv) {Collection ovC = (Collection) ov;Collection nvC = (Collection) nv;return doCompareArr(clz, ovC.toArray(), nvC.toArray());}/*** 比较数组类型的* @param clz* @param ov* @param nv* @return*/private static boolean doCompareArr(Class clz, Object ov, Object nv) {boolean res = true;String name = clz.getName();switch (name) {case "[Z": {boolean[] ov1 = (boolean[]) ov;boolean[] nv1 = (boolean[]) nv;res = Arrays.equals(ov1, nv1);}; break;case "[B": {byte[] ov1 = (byte[]) ov;byte[] nv1 = (byte[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[C": {char[] ov1 = (char[]) ov;char[] nv1 = (char[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[S": {short[] ov1 = (short[]) ov;short[] nv1 = (short[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[I": {int[] ov1 = (int[]) ov;int[] nv1 = (int[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[J": {long[] ov1 = (long[]) ov;long[] nv1 = (long[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[F": {float[] ov1 = (float[]) ov;float[] nv1 = (float[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;case "[D": {double[] ov1 = (double[]) ov;double[] nv1 = (double[]) nv;Arrays.sort(ov1);Arrays.sort(nv1);res = Arrays.equals(ov1, nv1);}; break;default: {// 转成数组Object[] ovArr = (Object[]) ov;Object[] nvArr = (Object[]) nv;// 长度不一样直接回去if (ovArr.length != nvArr.length) {res = false;return res;}// 数组元素没有顺序性的话 直接略过// 因为没顺序 没法比较到底哪个跟哪个进行比较呢if (!(ovArr[0] instanceof Comparable)) {return res;}// 排序 Arrays.sort(ovArr);Arrays.sort(nvArr);// 逐个比较呗for (int i = 0; i < ovArr.length; i++) {Object o1 = ovArr[i];Object o2 = nvArr[i];// 断路 发现不相等的直接结束res = doCompareAttr(o1.getClass(), o1, o2);if (!res) {break;}}}}return res;}/*** 判断是否重写了 equals 方法* @param clazz* @return*/private static boolean hasOverriddenEquals(Class<?> clazz) {try {Method equalsMethod = clazz.getMethod("equals", Object.class);return !equalsMethod.isSynthetic();} catch (NoSuchMethodException e) {return false;}}/*** 描述一个比较结果* @param list* @param field* @param oldVal* @param newVal*/private static void describeOneCompareDetail(List<CompareDetailInfo> list, Field field, Object oldVal, Object newVal) {CompareDetailInfo detailInfo = CompareDetailInfo.builder().field(field).oldVal(oldVal).newVal(newVal).build();list.add(detailInfo);}}
暂时先写了一版,还有些考虑还没实现,比如 ignoreFields 忽略哪些属性的比较因为像有些创建时间等没必要比较的需要忽略,还有差异信息的汇总不够直白,最后要呈现给用户还需要再转译一下等。
2.2 测试效果
我这里简单拿了三个类进行了一下实验,我贴出来,方便大家实验:
Base 基础类:
@Data public class Base {// 创建时间private LocalDateTime createTime;// 创建人private String createUserName;// 更新时间private LocalDateTime modifyTime;// 更新人private String modifyUserName;// 逻辑删除标志private Boolean deleted; }
User 用户类:
@Data @Builder public class User extends Base{// 姓名private String name;// 年龄private Integer age;// 性别private Short gender;// 其他类型实验private int num1;private boolean boolean1;private float float1;private char char1;private String[] strArr;private int[] intArr;private Integer[] integerArr;private UserSub[] userArr;private List<UserSub> userList; }
UserSub 关联类:
@Data public class UserSub implements Comparable<UserSub> {private String subName;private Integer subAge;@Overridepublic int compareTo(UserSub o) {String oSubName = o.getSubName();if (this.subName == null && oSubName == null) {return 0;}if (this.subName == null) {return -1;}if (oSubName == null) {return 1;}return this.subName.compareTo(oSubName);}}
测试类:
/*** @author: xjx* @description*/ public class CompareObj {public static void main(String[] args) {User oldVal = User.builder().build();User newVal = User.builder().build();oldVal.setCreateTime(LocalDateTime.now());oldVal.setStrArr(new String[]{"1"});newVal.setStrArr(new String[]{"12"});oldVal.setIntArr(new int[]{1,2});newVal.setIntArr(new int[]{1,2});oldVal.setIntegerArr(new Integer[]{1, 2});newVal.setIntegerArr(new Integer[]{1, 2});oldVal.setUserList(Lists.newArrayList(new UserSub()));newVal.setUserList(Lists.newArrayList(new UserSub()));UserSub userSub = new UserSub();userSub.setSubName("111");oldVal.setUserArr(new UserSub[]{userSub});newVal.setUserArr(new UserSub[]{new UserSub()});CompareResultInfo resultInfo = CompareInfoUtil.compareObj(User.class, oldVal, newVal);List<CompareDetailInfo> detailInfoList = resultInfo.getCompareDetailInfos();for (CompareDetailInfo detailInfo : detailInfoList) {System.out.println(detailInfo);}} }
目前效果:
结果后续还需要转译一下,翻译给用户能看懂的数据信息。
3 小结
暂时先写一版哈,还请大佬们指点一二,看看核心的比较逻辑对不对哈。