源码分析 - 数据的变更比对(一)

需求:保存表单信息,记录与上一版本表单的区别,将需要关注的指定字段打上变更标记(颜色),效果如下图所示。我们需要做的,就是在新的对象数据中,将变更的字段及变更的内容标识出来。
数据变更比对示例

前言

因为博主之前没有做过这类数据处理,于是在码云上找到了一个开源的变更比对的工具类Rayen/Coconut,我们来一步步的分析它是如何完成数据变更比对的,并应用到实际的项目使用中去。

一、代码结构

工程类文件

二、业务对象

1.业务实体类

示例中使用到的Business实体类为一个复杂业务嵌套对象,一共包含两层嵌套,一共三级。

// 业务对象示例
{
	"id":"1",
    "bizNum":"BH2024102501",
    "businessDetailList":[{
        "id":"10",
        "bizId":"1",
        "desc":"",
        "employeeList":[{
              "id":"101",
        	  "detailID":"10",
       		  "birthDay":"1999-10-10"
    	},{
        "id":"11",
        "bizId":"1",
        "desc":"业务详情2描述.....",
        "employeeList":[{
              "id":"102",
        	  "detailID":"10",
       		  "birthDay":"2001-01-10"
    	}]
    }]
}

1)业务类-Business

业务类-Business

2)业务详情类-BusinessDetail

业务详情类-BusinessDetail

3)业务详情雇员类-Employee

在这里插入图片描述

三、源码分析

1.读取嵌套对象Business的字段及字段值

第一步:我们需要对嵌套对象Business进行解析,对所有字段进行遍历 -> 获取所有的字段值。
那么面对一个多层嵌套的对象,应该怎么去获取这些信息呢?

1)自定义注解

可以发现,在实体类中使用了许多自定义注解。

@DiffIgnore:忽略不比较的属性(字段)
@EntityTag:部件标识,区分各个复合属性
@PropertyField:属性域注解,可自定义属性别名,属性展示顺序
@UnionId:标识对象的唯一性,匹配新旧版本需要对比的对象,支持多个属性联合组成唯一ID
@CollectionFlag 集合/复合属性标识(基本类型、对象、集合)
@PropertyComparator 属性比较器,支持自定义属性比较方法(暂未使用到)

博主认为其中需要重点关注的是:@EntityTag、@UnionId、@CollectionFlag

2)嵌套对象内容读取

字段基础对象 TypePojo

package com.art.coconut.change;

import com.art.coconut.compare.Comparator;
import lombok.Data;

import java.lang.reflect.Type;

/**
 * 字段信息类 TypePojo
 */
@Data
public class TypePojo {
    // 排序
    private float order;
    // 字段名
    private String propertyName;
    // 字段类型
    private Type type;
    // 获取字段基础类型
    private String typeName;
    // 字段值
    private Object object;
    // 比较类
    private Comparator comparator;
    // 类
    private Class<?> typeClass;
    // 实体类标识
    private String entityTag;
}

流程处理类 ProcessHandle

package com.art.coconut.utils;

import com.art.coconut.annotation.*;
import com.art.coconut.change.TypePojo;
import com.art.coconut.compare.BaseComparator;
import com.art.coconut.enums.ExceptionEnum;
import com.art.coconut.enums.PropertyTypeEnum;
import com.art.coconut.exception.CoconutCommonException;

import java.lang.reflect.Field;
import java.util.*;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;
/**
 * 流程处理类
 */
public class ProcessHandle {

	 /**
     * 获取对象唯一id
     * 通过反射,根据@UnionId主解获取对象主键
     * eg:@UnionId 
     *     private String id;
     * @param obj
     * @return
     * @throws IllegalAccessException
     */
    public static String getUniqueId(Object obj) throws IllegalAccessException {
        String key = null;
        Field[]fields = obj.getClass().getDeclaredFields();
        StringBuilder idSb = new StringBuilder();
        boolean hasUniqueId = Boolean.FALSE;
        for (Field field : fields) {
            UnionId id = field.getAnnotation(UnionId.class);
            if(id!=null){
                hasUniqueId = true;
                field.setAccessible(true);
                idSb.append(field.get(obj));
                idSb.append("@");
            }
        }
        if(hasUniqueId){
            key = idSb.substring(0, idSb.lastIndexOf("@"));
        }
        if(key==null){
            throw new CoconutCommonException(ExceptionEnum.NO_ID_EXCEPTION.getCode(), ExceptionEnum.NO_ID_EXCEPTION.getMsg());
        }
        return key;
    }

	/**
     * 传入对象类,获取对象类注解名称
     * eg:@EntityTag("BUSINESS")
     * @param tClass
     * @return
     */
    public static String getEntityTag(Class<?> tClass){
        EntityTag tag = tClass.getAnnotation(EntityTag.class);
        return Objects.isNull(tag) ? null: tag.value();
    }

	/**
     * 将集合转为Map
     * eg:主键 -> Business(id=1, bizNum=100111100, businessDetailList=[BusinessDetail(id=11, bizId=1, desc=A, employeeList=[Employee(id=em_1, detailID=11, birthDay=Mon Dec 16 17:00:13 CST 2024)])])
     * @param version
     * @return
     * @throws IllegalAccessException
     */
    public static Map<Object, Object> collectionToMap(Collection<?> version) throws IllegalAccessException {
        Map<Object, Object> versionMap = new HashMap<>();
        for (Object o : version) {
            String key = getUniqueId(o);
            versionMap.put(key, o);
        }
        return versionMap;
    }

	/**
     * 将指定类对象转为统一TypePojo对象Map
     * eg:明细ID -> TypePojo(order=0.0, propertyName=明细ID, type=class java.lang.String, typeName=java.util.Map, object=11, comparator=com.art.coconut.compare.BaseComparator@21bd128b, typeClass=class com.art.coconut.example.model.BusinessDetail, entityTag=BUSINESS_DETAIL)
     * @param object
     * @param refClass
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public static Map<String, TypePojo> objectToMap(Object object,  Class<?> refClass) throws IllegalAccessException, InstantiationException {
        if(Objects.isNull(object)){
            return new LinkedHashMap<>();
        }

        // 获取类的自身的所有字段,不包括父类的字段。
        Field[]fields = refClass.getDeclaredFields();
        List<TypePojo> typePojoList = new ArrayList<>(fields.length);

        // 获取实体类标识
        String location = null;
        EntityTag entityTag = refClass.getAnnotation(EntityTag.class);
        if(entityTag != null){
            location = entityTag.value();
        }else{
            throw new CoconutCommonException(ExceptionEnum.NO_ENTITY_TAG_EXCEPTION.getCode(), ExceptionEnum.NO_ENTITY_TAG_EXCEPTION.getMsg());
        }

        // 遍历类字段
        for (Field field : fields) {
            // 排除标记比对忽略标识的字段
            DiffIgnore diffIgnore = field.getAnnotation(DiffIgnore.class);
            if(diffIgnore==null){
                // 初始化typePojo对象
                TypePojo typePojo = new TypePojo();
                // 设置反射时可访问私有变量
                field.setAccessible(true);
                // 获取字段基础类型
                typePojo.setTypeName(field.getType().getTypeName());
                // 获取字段名及排序
                PropertyField propertyNameAno = field.getAnnotation(PropertyField.class);
                String propertyName = field.getName();
                if(propertyNameAno!=null){
                    propertyName = propertyNameAno.value();
                    typePojo.setOrder(propertyNameAno.order());
                }
                // 获取字段属性标识:基本类型/对象/集合
                CollectionFlag collectionFlag = field.getAnnotation(CollectionFlag.class);
                if(collectionFlag == null){
                    // 不存在字段属性标识 -> 获取属性比较注解:是否自定义属性比较方法
                    PropertyComparator comparator = field.getAnnotation(PropertyComparator.class);
                    if(comparator!=null){
                        // 自定义属性比较方法
                        typePojo.setComparator(comparator.value().newInstance());
                    }else{
                        // 默认比较方法
                        typePojo.setComparator(new BaseComparator());
                    }
                    // 字段属性标识 默认是基本类型
                    typePojo.setTypeName(PropertyTypeEnum.BASE_TYPE.getTypeName());
                }else {
                    // 根据注解获取 字段属性标识
                    typePojo.setTypeName(collectionFlag.value().getTypeName());
                }

                // 指定类
                typePojo.setTypeClass(field.getDeclaringClass());
                // 字段名
                typePojo.setPropertyName(propertyName);
                // 字段类型
                typePojo.setType(field.getType());
                // 字段值
                typePojo.setObject(field.get(object));
                // 实体类标识
                typePojo.setEntityTag(location);
                typePojoList.add(typePojo);
//                resMap.put(propertyName, typePojo);
            }
        }
//        Map<String, TypePojo> linkedMap = new LinkedHashMap<>();
//        resMap.values().stream().sorted(Comparator.comparing(TypePojo::getOrder)).forEach(t->{
//            linkedMap.put(t.getPropertyName(), t);
//        });
        return typePojoList.stream()
                .sorted(Comparator.comparing(TypePojo::getOrder))
                .collect(Collectors.toMap(TypePojo::getPropertyName, e->e, throwingMerger(), LinkedHashMap::new));
    }
	
	/**
     * 重复key抛出异常
     * @return
     * @param <T>
     */
   private static <T> BinaryOperator<T> throwingMerger() {
        return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
    }
}

getUniqueId
collectionToMap

posted @ 2025-10-24 15:37  御坂10027  阅读(13)  评论(0)    收藏  举报  来源