如同三年前【工作中问题解决实践 四】动态解析Json结构最佳实践一样,又遇到了切换业务系统的事情,那么就涉及到了数据的迁移操作,迁移来的数据又需要和三方系统进行对比来验证其有效性。简而言之背景问题就是:我当前系统有一批公司数据,我要巡检一下这批公司数据和天眼查的数据是否能匹配的上,那么就需要对比这些数据而公司数据又是由多个实体组成的,每个实体又有自己的唯一性规则,例如公司的唯一性是税号,人员的唯一性则是人员类型+姓名,股东的唯一性是:股东姓名+股东类型+出资+占比,如果把这些逻辑全部冗余进去,不仅复杂,以后代码也会越写越臃肿,不好扩展。
那么如何才能对一批数据进行可扩展的非侵入式的校验呢?这就需要开发一个数据对比框架,类似于CollectionUtils
的对比方法,但实际上又较之更为复杂。于是仿照CollectionUtils
的写法写了一套相对业务定制的对比框架,整体分为如下几个部分:
这套设计想法也来自于之前学习的设计模式的灵感:代理模式和简单版的装饰器模式,虽然我这里的具体实体没有功能,不能完全套用代理模式或装饰器模式,但是也提供了一个思路,实体被规则器代理或装饰后没有增强自己的方法而是把自己作为规则器方法实现的数据来源。对于代理模式和装饰器模式感兴趣的可以跳转直戳。
1 规则器接口定义
其实简单来看也可以理解为功能增强接口,只是把数据实体用作了数据来源而非原始功能的提供方,这个就定义了增强规则所应该具备的能力,为了适配不同的实体类型,这里的接口方法定义成泛型:
/** * * @Name Compare * * @Description * * @author tianmaolin * * @Date 2022/9/7 * * @param <T> the type parameter */ public interface DataCompare<T> { /** * 获取对比属性配置 * * @return the compare property config */ CompareProperty getComparePropertyConfig(); /** * 数据比对依据关键key * * @return the string */ String getUniqueKey(); /** * 通过数据唯一标识获取原始数据id * * @return the id by unique key */ Long getIdByUniqueKey(); /** * 待对比数据 * * @return the data */ T getData(); /** * 数据集合转为比较实体集合 * * @param list the list * @return the list */ List<DataCompare<T>> toCompareList(final List<T> list); }
需要注意的是这里的属性对比类中的属性配置:
/** * * @Name CompareProperty * * @Description * * @author tianmaolin * * @Date 2022/9/8 */ @Data @AllArgsConstructor @NoArgsConstructor public class CompareProperty { /** * 对比类型 * * @see PropertyConfigTypeEnum */ private Integer type; /** * 对比属性列表 */ private List<String> compareProperty; }
枚举指明了我们在交集数据对比时对待属性列表的态度
/** * * @Name ComStoreBindStatusEnum * * @Description * * @author tianmaolin * * @Date 2022/4/29 */ @Getter public enum PropertyConfigTypeEnum { FOCUS(1, "对比时仅关注的属性"), IGNORE(2, "对比时忽略的属性"); private final Integer code; private final String desc; PropertyConfigTypeEnum(int code, String desc) { this.code = code; this.desc = desc; } public static PropertyConfigTypeEnum codeOf(Integer code) { for (PropertyConfigTypeEnum value : values()) { if (value.code.equals(code)) { return value; } } throw new IllegalArgumentException("no PropertyConfigTypeEnum for code" + code); } }
当实体的属性大多数都需要对比时,例如20个字段有18个需要对比,那么就将类型标志位IGNORE
,反之如果20个字段仅有1个字段在数据对比时需要关心,则将类型标志位设置为FOCUS
2 规则实现类
基于装饰器比较接口我们明确了在对比数据时装饰类一定要具备的功能(实现的方法)
1 规则器实现配置
这里我们以公司的股东信息为例:
/** * * @Name ShareHolderDateCompare * * @Description 股东信息对比类 * * @author tianmaolin * * @Date 2022/9/7 */ @Service public class ShareHolderDataCompare implements DataCompare<CompanyShareholderEntity> { private CompanyShareholderEntity companyShareholderEntity; @Override public String getUniqueKey() { List<String> uniqueEnums = new ArrayList(); uniqueEnums.add(StringUtils.trimToEmpty(companyShareholderEntity.getShareholderName())); uniqueEnums.add(companyShareholderEntity.getType().toString()); uniqueEnums.add(StringUtils.trimToEmpty(companyShareholderEntity.getAmomon())); uniqueEnums.add(StringUtils.trimToEmpty(companyShareholderEntity.getPercent())); return String.join(",", uniqueEnums); } @Override public Long getIdByUniqueKey() { return companyShareholderEntity.getId(); } @Override public CompareProperty getComparePropertyConfig() { List<String> ignoreProperties = new ArrayList<>(); ignoreProperties.add(FieldNameUtil.noPrefix(CompanyShareholderEntity::getId)); ignoreProperties.add(FieldNameUtil.noPrefix(CompanyShareholderEntity::getCreateTime)); return new CompareProperty(PropertyConfigTypeEnum.IGNORE.getCode(), ignoreProperties); } @Override public CompanyShareholderEntity getData() { return companyShareholderEntity; } @Override public List<DataCompare<CompanyShareholderEntity>> toCompareList(List<CompanyShareholderEntity> list) { if (null == list) { return Lists.newArrayList(); } return list.stream().map(x -> { ShareHolderDataCompare shareHolderDateCompare = new ShareHolderDataCompare(); shareHolderDateCompare.companyShareholderEntity = x; return shareHolderDateCompare; }).collect(Collectors.toList()); } }
这里只是股东,如果想要扩展别的实体对比,那么只需配置好对比规则类就可以直接对比了。
2 Hutool获取属性名称
值得注意的是这里在设置关键属性时为了避免魔法值使用,直接基于开源的hutool
做了处理,通过get或者set方法获取属性名称,并可以定制样式:
/** * * @Name FieldUtil * * @Description * * @author tianmaolin * * @Date 2022/9/5 */ import cn.hutool.core.util.StrUtil; import java.io.Serializable; import java.lang.invoke.SerializedLambda; import java.lang.reflect.Method; /** * 属性工具类,用来获取 Getter 和 Setter 属性的名称。支持首字母小写样式,下划线的样式和自定义样式 */ public class FieldNameUtil { /** * 下划线样式,小写 * * @param <T> the type parameter * @param fn the fn * @return the string */ public static <T> String underline(IGetter<T> fn) { return toSymbolCase(fn, '_'); } /** * 下划线样式,大写 * * @param <T> the type parameter * @param fn the fn * @return the string */ public static <T> String underlineUpper(IGetter<T> fn) { return underline(fn).toUpperCase(); } /** * 依据符号转换样式 * * @param <T> the type parameter * @param fn the fn * @param symbol the symbol * @return the string */ public static <T> String toSymbolCase(IGetter<T> fn, char symbol) { return StrUtil.toSymbolCase(noPrefix(fn), symbol); } /*** * 转换getter方法引用为属性名,首字母小写 * @param <T> the type parameter * @param fn the fn * @return the string */ public static <T> String noPrefix(IGetter<T> fn) { return getGeneralField(fn); } /** * 下划线样式,小写 * * @param <T> the type parameter * @param <R> the type parameter * @param fn the fn * @return the string */ public static <T, R> String underline(ISetter<T, R> fn) { return toSymbolCase(fn, '_'); } /** * 下划线样式,大写 * * @param <T> the type parameter * @param <R> the type parameter * @param fn the fn * @return the string */ public static <T, R> String underlineUpper(ISetter<T, R> fn) { return underline(fn).toUpperCase(); } /** * 依据符号转换样式 * * @param <T> the type parameter * @param <R> the type parameter * @param fn the fn * @param symbol the symbol * @return the string */ public static <T, R> String toSymbolCase(ISetter<T, R> fn, char symbol) { return StrUtil.toSymbolCase(noPrefix(fn), symbol); } /** * 转换setter方法引用为属性名,首字母小写 * * @param <T> the type parameter * @param <R> the type parameter * @param fn the fn * @return the string */ public static <T, R> String noPrefix(ISetter<T, R> fn) { return getGeneralField(fn); } /** * 获得set或get或is方法对应的标准属性名,其它前缀的方法名使用原名 */ private static String getGeneralField(Serializable fn) { SerializedLambda lambda = getSerializedLambda(fn); String getOrSetMethodName = lambda.getImplMethodName(); final String generalField = StrUtil.getGeneralField(getOrSetMethodName); return StrUtil.isEmpty(generalField) ? getOrSetMethodName : generalField; } /*** * 获取类对应的Lambda */ private static SerializedLambda getSerializedLambda(Serializable fn) { //先检查缓存中是否已存在 SerializedLambda lambda; try { //提取SerializedLambda并缓存 Method method = fn.getClass().getDeclaredMethod("writeReplace"); method.setAccessible(Boolean.TRUE); lambda = (SerializedLambda) method.invoke(fn); } catch (Exception e) { throw new IllegalArgumentException("获取SerializedLambda异常, class=" + fn.getClass().getSimpleName(), e); } return lambda; } /** * getter方法接口定义 * * @param <T> the type parameter */ @FunctionalInterface public interface IGetter<T> extends Serializable { /** * Apply object. * * @param source the source * @return the object */ Object apply(T source); } /** * setter方法接口定义 * * @param <T> the type parameter * @param <U> the type parameter */ @FunctionalInterface public interface ISetter<T, U> extends Serializable { /** * Accept. * * @param t the t * @param u the u */ void accept(T t, U u); } }