3 数据对比框架
重头戏数据对比框架实现,直接传入要对比的数据实体和规则实现类即可:
1 对比框架方法及返回结果定义
对比框架的基本方法和返回结果定义如下:
/** * * @Name DateCompareFramework * * @Description * * @author tianmaolin * * @Date 2022/9/7 */ @NoArgsConstructor @Service public class DataCompareFramework { /** * 同一数据对比 * * @return the compare entity result */ public static <T> CompareResult<T> doCompare(List<T> originEntity, List<T> standardEntity, DataCompare<T> compareRule) { List<DataCompare<T>> originCompares = compareRule.toCompareList(originEntity); List<DataCompare<T>> standardCompares = compareRule.toCompareList(standardEntity); Set<String> originUniqueSet = originCompares.stream().map(DataCompare::getUniqueKey).collect(Collectors.toSet()); Set<String> standardUniqueSet = standardCompares.stream().map(DataCompare::getUniqueKey).collect(Collectors.toSet()); CompareResult<T> compareResult = new CompareResult<>(); // 1. 三方标准数据较当前数据多的 List<T> needAddList = standardCompares.stream().filter(c -> !originUniqueSet.contains(c.getUniqueKey())).map(DataCompare::getData).collect(Collectors.toList()); compareResult.setNeedAddEntityList(needAddList); // 2. 三方标准数据较当前数据少的 List<T> needDeleteList = originCompares.stream().filter(c -> !standardUniqueSet.contains(c.getUniqueKey())).map(DataCompare::getData).collect(Collectors.toList()); compareResult.setNeedDeleteEntityList(needDeleteList); // 3. 当前数据与三方标准数据交集的数据 List<DataCompare<T>> intersectionList = originCompares.stream().filter(c -> standardUniqueSet.contains(c.getUniqueKey())).collect(Collectors.toList()); if (!CollectionUtils.isEmpty(intersectionList)) { Map<Long, List<ObjectDiffUtil.ModifiedPropertyInfo>> diffInfos = new HashMap<>(64); intersectionList.forEach(x -> { DataCompare<T> standardDate = standardCompares.stream().filter(y -> y.getUniqueKey().equals(x.getUniqueKey())).findFirst().orElse(null); if (Objects.isNull(standardDate)) { return; } List<ObjectDiffUtil.ModifiedPropertyInfo> diffInfo = ObjectDiffUtil.getDifferentProperty(x.getData(), standardDate.getData(), x.getComparePropertyConfig()); if (CollectionUtils.isEmpty(diffInfo)) { return; } diffInfos.put(x.getIdByUniqueKey(), diffInfo); }); compareResult.setIntersectionDiffInfo(diffInfos); } return compareResult; } /** * 数据对比结果,从当前数据集合视角出发 * * @param <T> the type parameter */ @Data @NoArgsConstructor public static class CompareResult<T> { /** * 当前数据较三方标准数据多的,待删除数据集合 */ private List<T> needDeleteEntityList = Lists.newArrayList(); /** * 当前数据较三方标准数据少的,待补充数据集合 */ private List<T> needAddEntityList = Lists.newArrayList(); /** * 交集数据属性对比 */ private Map<Long, List<ObjectDiffUtil.ModifiedPropertyInfo>> intersectionDiffInfo = new HashMap<>(); /** * 交集数据中当前数据缺失关键属性 */ private List<T> lackInfoEntityList = Lists.newArrayList(); } }
2 反射实现交集数据对比
这里用到的交集数据对比方法实现如下,主要通过反射拿到实体所有属性和值,然后进行遍历比较:
/** * * @Name ObjectDiffUtil * * @Description * * @author tianmaolin * * @Date 2022/9/6 */ @Slf4j public class ObjectDiffUtil { /** * 比较两个对象属性值是否相同 * 如果不同返回修改过的属性信息 * * @param <T> the type parameter * @param oldObj the old obj * @param newObj the new obj * @return 修改过的属性字段 different property */ public static <T> List<ModifiedPropertyInfo> getDifferentProperty(T oldObj, T newObj, CompareProperty compareProperty) { // 1 校验是否需要比较 if (Objects.isNull(oldObj) || Objects.isNull(newObj) || oldObj.equals(newObj)) { return Collections.emptyList(); } // 2 准备比较数据 List<PropertyModelInfo> oldObjectPropertyValue = getObjectPropertyValue(oldObj, compareProperty); if (CollectionUtils.isEmpty(oldObjectPropertyValue)) { return Collections.emptyList(); } List<ModifiedPropertyInfo> modifiedPropertyInfos = new ArrayList<>(oldObjectPropertyValue.size()); List<PropertyModelInfo> newObjectPropertyValue = getObjectPropertyValue(newObj, compareProperty); Map<String, Object> objectMap = new HashMap<>(newObjectPropertyValue.size()); for (PropertyModelInfo propertyModelInfo : newObjectPropertyValue) { String propertyName = propertyModelInfo.getPropertyName(); Object value = propertyModelInfo.getValue(); objectMap.put(propertyName, value); } // 3 比较并返回比较结果 for (PropertyModelInfo propertyModelInfo : oldObjectPropertyValue) { String propertyName = propertyModelInfo.getPropertyName(); Object value = propertyModelInfo.getValue(); if (objectMap.containsKey(propertyName)) { Object newValue = objectMap.get(propertyName); ModifiedPropertyInfo modifiedPropertyInfo = new ModifiedPropertyInfo(); if (isObjectEmpty(value) && isObjectEmpty(newValue)) { continue; } boolean notEqual = false; if (value instanceof BigDecimal) { if (((BigDecimal) value).compareTo((BigDecimal) newValue) != 0) { notEqual = true; } } else if (!value.equals(newValue)) { notEqual = true; } if (notEqual) { modifiedPropertyInfo.setPropertyName(propertyName); modifiedPropertyInfo.setOriginValue(value); modifiedPropertyInfo.setStandardValue(newValue); modifiedPropertyInfos.add(modifiedPropertyInfo); } } } return modifiedPropertyInfos; } /** * 通过反射获取对象的属性名称、getter返回值类型、属性值等信息 * * @param <T> the type parameter * @param obj the obj * @return object property value */ private static <T> List<PropertyModelInfo> getObjectPropertyValue(T obj, CompareProperty compareProperty) { if (Objects.isNull(obj)) { return Collections.emptyList(); } Class<?> objClass = obj.getClass(); PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(objClass); List<PropertyModelInfo> modelInfos = new ArrayList<>(propertyDescriptors.length); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { Method readMethod = propertyDescriptor.getReadMethod(); String name = propertyDescriptor.getName(); boolean isNeedDiffProperty = false; // 如果对比属性配置类型为忽视,则不在忽视列表该属性参与对比 if (PropertyConfigTypeEnum.IGNORE.getCode().equals(compareProperty.getType())) { isNeedDiffProperty = (readMethod != null && (CollectionUtils.isEmpty(compareProperty.getCompareProperty()) || !compareProperty.getCompareProperty().contains(name))); } // 如果对比属性配置类型为关注,则在关注列表该属性才参与对比 if (PropertyConfigTypeEnum.FOCUS.getCode().equals(compareProperty.getType())) { isNeedDiffProperty = (readMethod != null && !CollectionUtils.isEmpty(compareProperty.getCompareProperty()) && compareProperty.getCompareProperty().contains(name)); } if (isNeedDiffProperty) { Object invoke; Class<?> returnType = readMethod.getReturnType(); try { invoke = readMethod.invoke(obj); PropertyModelInfo propertyModelInfo = new PropertyModelInfo(); propertyModelInfo.setPropertyName(name); propertyModelInfo.setValue(invoke); propertyModelInfo.setReturnType(returnType); modelInfos.add(propertyModelInfo); } catch (IllegalAccessException | InvocationTargetException e) { LOGGER.error("反射获取类【" + objClass.getName() + "】方法异常,", e); } } } return modelInfos; } /** * 判断Object对象为空或空字符串 * * @param obj * @return */ public static Boolean isObjectEmpty(Object obj) { String str = ObjectUtils.toString(obj, ""); return StringUtils.isBlank(str); } /** * 返回差异数据 */ @Data public static class ModifiedPropertyInfo implements Serializable { /** * 属性名称 */ private String propertyName; /** * 原始值 */ private Object originValue; /** * 标准值 */ private Object standardValue; } /** * 属性字段模型 */ @Data public static class PropertyModelInfo { /** * 属性名 */ private String propertyName; /** * 属性值 */ private Object value; /** * 类型 */ private Class<?> returnType; } }
4 方法调用
方法调用非常简单,所有逻辑通过配置方式实现,通用实现逻辑内部封装:
public DataCompareFramework.CompareResult<CompanyShareholderEntity> comShareHolderInfoDiffByTyc(String comTaxNo) { List<CompanyShareholderEntity> companyShareholderEntities = companyShareHolderRepo.getByCompanyTaxNo(comTaxNo); if (CollectionUtils.isEmpty(companyShareholderEntities)) { throw new ServiceException("当前系统中不存在该公司下股东信息,comTaxNo:{}", comTaxNo); } List<CompanyShareholderEntity> tycShareholderEntities = companySyncService.getShareHolderEntityByTyc(comTaxNo); if (CollectionUtils.isEmpty(tycShareholderEntities)) { throw new ServiceException("天眼查不存在该公司下股东信息,comTaxNo:{}", comTaxNo); } return DataCompareFramework.doCompare(companyShareholderEntities, tycShareholderEntities, shareHolderDateCompare); }
总结一下
当前这套框架其实可以基于相同实体类型的数据集合进行对比,包括多的、少的、交的。如需新增对比,只需要新实现一个规则器就行了。但是还有问题尚待解决就是:
- 数据获取需要自己获取,后续可以通过配置数据源、配置脚本段的方式来获取数据
- 数据获取到需要转换为同一实体,这部分代码也有开发量,未来想着通过配置实现,指定字段映射,然后将映射后的json转为对象实体,再走后续的通用流程
- 数据对比结果拿到后后续其实还可以配置处理策略,例如newValue覆盖oldValue、舍弃newValue或是其它,拿到数据最好还是能处理一下
目前这个框架其实也不能称之为框架,只能定制化的解决当前的问题,只不过代码写的扩展度较高,后续随着业务的需要和发展迭代演进吧,再回顾下三年前那篇通过动态JsonSchem进行数据比对的文章,其实有相似之处,定义JsonSchem其实就是统一设计一个通用实体类,定制的节点规则其实就可以设计在增强的实体规则类里,
- Schema的好处可能是新增时候除非具体比较规则变更(equal类似),否则代码不会变更,只需将适配新比较实体的JsonSchem写入数据库或配置文件,而对比框架则新增数据Json时需要配置实体和实体规则器,不过实体规则器配置化或者持久化后也貌似一个效果。
- 对比框架的好处是不需要引入Schema这样一个复杂的概念,也不需要构造字典树,直接通过实体对象的反射功能就能轻松匹配数据,而Schema则需要构造字典数,不过好在代码只写一遍即可。而且对比框架可以输出差集,不仅仅是一对一的交集对比。
总的来说,感觉二者相似度也很高,只要对比框架再配置化一些就能抹平Schema的优势,而Schema也就是字典树构建又较框架反射实体麻烦,但好在构建代码只需写一遍(而且我也实现了),而Schema的差集结果输出也可以再通过制定字典树关键属性区分,也就是再优化代码的事儿。不相伯仲吧,实现思路还是类似的,都是对同一类格式的数据(无论是对象还是Json串)配置规则,只不过规则配置方式不同,匹配实现方式不同而已。