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

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

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串)配置规则,只不过规则配置方式不同,匹配实现方式不同而已。

相关文章
|
7月前
|
机器学习/深度学习 人工智能 搜索推荐
什么叫生成式人工智能?职业技能的范式转移与能力重构
生成式人工智能(Generative AI)是AI领域的重要分支,其核心在于通过学习数据分布生成新内容,如文本、图像、音乐等。与传统判别式模型不同,生成式AI基于深度学习技术(如Transformer架构),展现出“创造力”,但其本质仍是概率计算的结果。它正在重塑内容创作、编程、设计等多个职业领域,推动职业技能的范式转移。 掌握生成式AI需要理解其技术原理、能力边界及伦理挑战。职业技能培训应聚焦提示设计、结果评估和混合创作三大能力,帮助从业者在人机协作中发挥主导作用。未来,生成式AI将向多模态、个性化发展,而人类的独特价值在于为技术注入人文关怀与道德框架。
|
10月前
|
网络协议 前端开发 应用服务中间件
nginxconf.sh 自动生成 nginx tcp 转发配置文件 conf
该脚本由 eisc.cn 开发,用于自动生成 Nginx 代理配置。它根据预设的域名、IP 和端口信息,为多个项目(如 www、work、sou 等)创建对应的 Nginx 配置文件,设置前端转发、端口对端口及后端转发规则,并生成日志和 301 跳转配置。支持自动创建 CGI 解析目录,确保各项目能够正确访问。运行时需具备 root 权限或使用 sudo。
332 9
|
前端开发
flowable流程设计器的几个bug修改记录
flowable流程设计器的几个bug修改记录
323 0
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
511 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
STM32CubeMX FreeRTOS 任务的挂起和恢复
STM32CubeMX FreeRTOS 任务的挂起和恢复
459 12
|
SQL 数据处理 调度
实时计算 Flink版产品使用合集之实现批量抽取如何解决
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
缓存 监控 Java
【工作中问题解决实践 十】一次内存泄露排查-MAT使用指南
【工作中问题解决实践 十】一次内存泄露排查-MAT使用指南
441 0
|
存储 JSON 前端开发
【工作中问题解决实践 十二】使用@JsonTypeInfo实现请求数据对象多态
【工作中问题解决实践 十二】使用@JsonTypeInfo实现请求数据对象多态
854 1
|
XML 开发框架 监控
面试题:springboot比spring有哪些优点?
面试题:springboot比spring有哪些优点?
502 0
|
数据采集 移动开发 API
vue-router 的不同的历史记录模式详解
前几天面试官问我能不能讲一下 vue-router 两种模式,好家伙不就是 #/path 和 /path 这还用问?面试官浅笑一下,呵呵,讲得有点表面,深层的地方没有讲出来

热门文章

最新文章