【工作中问题解决实践 六】基于反射及类装饰模式的数据对比框架(下)

简介: 【工作中问题解决实践 六】基于反射及类装饰模式的数据对比框架(下)

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);
    }

总结一下

当前这套框架其实可以基于相同实体类型的数据集合进行对比,包括多的、少的、交的。如需新增对比,只需要新实现一个规则器就行了。但是还有问题尚待解决就是:

  1. 数据获取需要自己获取,后续可以通过配置数据源、配置脚本段的方式来获取数据
  2. 数据获取到需要转换为同一实体,这部分代码也有开发量,未来想着通过配置实现,指定字段映射,然后将映射后的json转为对象实体,再走后续的通用流程
  3. 数据对比结果拿到后后续其实还可以配置处理策略,例如newValue覆盖oldValue、舍弃newValue或是其它,拿到数据最好还是能处理一下

目前这个框架其实也不能称之为框架,只能定制化的解决当前的问题,只不过代码写的扩展度较高,后续随着业务的需要和发展迭代演进吧,再回顾下三年前那篇通过动态JsonSchem进行数据比对的文章,其实有相似之处,定义JsonSchem其实就是统一设计一个通用实体类,定制的节点规则其实就可以设计在增强的实体规则类里,

  • Schema的好处可能是新增时候除非具体比较规则变更(equal类似),否则代码不会变更,只需将适配新比较实体的JsonSchem写入数据库或配置文件,而对比框架则新增数据Json时需要配置实体和实体规则器,不过实体规则器配置化或者持久化后也貌似一个效果。
  • 对比框架的好处是不需要引入Schema这样一个复杂的概念,也不需要构造字典树,直接通过实体对象的反射功能就能轻松匹配数据,而Schema则需要构造字典数,不过好在代码只写一遍即可。而且对比框架可以输出差集,不仅仅是一对一的交集对比。

总的来说,感觉二者相似度也很高,只要对比框架再配置化一些就能抹平Schema的优势,而Schema也就是字典树构建又较框架反射实体麻烦,但好在构建代码只需写一遍(而且我也实现了),而Schema的差集结果输出也可以再通过制定字典树关键属性区分,也就是再优化代码的事儿。不相伯仲吧,实现思路还是类似的,都是对同一类格式的数据(无论是对象还是Json串)配置规则,只不过规则配置方式不同,匹配实现方式不同而已。

相关文章
《深入理解高并发编程(第2版)》八大篇章,共433页,打包发布!!
大家好,我是冰河~~ 在 冰河技术 微信公众号中的【精通高并发系列】专题,更新了不少文章,有些读者反馈说,在公众号中刷历史文章不太方便,有时会忘记自己看到哪一篇了,当打开一篇文章时,似乎之前已经看过了,但就是不知道具体该看哪一篇了。相信很多小伙伴都会有这样的问题。那怎么办呢? 最好的解决方案就是我把这些文章整理成PDF电子书,免费分享给大家,这样,小伙伴们看起来就方便多了。
1302 0
《深入理解高并发编程(第2版)》八大篇章,共433页,打包发布!!
|
12月前
|
人工智能 大数据
光速矩阵:开创AIGC时代的矩阵营销与就业平台
在全球数字经济迅速发展的背景下,**光速矩阵**应运而生,成为引领AIGC(人工智能生成内容)时代的矩阵营销与就业平台。依托前沿AIGC技术和人力资本大模型,光速矩阵为品牌、企业及劳动者提供全方位的数字化创作、智能化管理和高效传播服务,重新定义企业营销与人才就业模式。平台积极响应国家战略,赋能数字化就业,通过与政府、大学和企业合作,构建全国AIGC人才生态网络,免费培养数百万“光速创客”,提升乡村就业率,并助力品牌全球化。光速矩阵不仅推动了城乡经济平衡发展,还致力于构建全球数字经济生态系统,促进全球经济繁荣。
光速矩阵:开创AIGC时代的矩阵营销与就业平台
|
9月前
|
人工智能 自然语言处理 架构师
字节面试: es怎么提升性能和精准度?(尼恩独家,史上最全)
本文由40岁老架构师尼恩撰写,针对ES(Elasticsearch)提升搜索性能和精准度的面试题进行详细解析。文章首先指出,提升ES速度和精准度是两个独立的问题,分别涉及性能优化和精准度优化。这些内容不仅有助于应对面试中的难题,还能帮助开发者在实际项目中构建更高效的搜索系统。尼恩强调,掌握这些知识后可以在面试中“吊打”面试官,轻松获得理想Offer。同时,他还提供了《尼恩Java面试宝典PDF》等资源供读者学习参考。
|
11月前
|
存储 数据可视化 API
重磅干货,免费三方网络验证[用户系统+CDK]全套API接口分享教程。
本套网络验证系统提供全面的API接口,支持用户注册、登录、数据查询与修改、留言板管理等功能,适用于不想自建用户系统的APP开发者。系统还包含CDK管理功能,如生成、使用、查询和删除CDK等。支持高自定义性,包括20个自定义字段,满足不同需求。详细接口参数及示例请参考官方文档。
|
SQL 缓存 Java
mybatis 一对多查询
mybatis 一对多查询
292 0
|
存储 分布式计算 Hadoop
Hadoop性能问题
【7月更文挑战第12天】
193 11
|
Linux
Linux下采集摄像头的图像再保存为JPG图片存放到本地(YUYV转JPG)
Linux下采集摄像头的图像再保存为JPG图片存放到本地(YUYV转JPG)
2245 2
Linux下采集摄像头的图像再保存为JPG图片存放到本地(YUYV转JPG)
|
存储 Java 关系型数据库
【Elasticsearch 技术分享】—— 十张图带大家看懂 ES 原理 !明白为什么说:ES 是准实时的!
说到 Elasticsearch ,其中最明显的一个特点就是 near real-time 准实时 —— 当文档存储在Elasticsearch中时,将在1秒内以几乎实时的方式对其进行索引和完全搜索。那为什么说 ES 是准实时的呢?
1504 0
|
缓存 JavaScript 前端开发
Vue3——Router4教程(小满版本)(二)
Vue3——Router4教程(小满版本)
400 0
|
XML 开发框架 监控
面试题:springboot比spring有哪些优点?
面试题:springboot比spring有哪些优点?
385 0