Hibernate Validator -集合对象验证(三)(可能是东半球最全的讲解了)

Tips

在上线到测试环境之后,关于Spring boot的国际化问题,烦扰了我整整一天的时间,大家可以参考这篇文章去处理国际化的问题

前情提示

前两篇文章已经介绍了Hibernate Validator的对象基础验证对象分组验证,没有看过的童鞋可以先去回顾一下,本章节主要解决第一章节所说的具体的业务需求

业务需求

最近在做和Excel导入、导出相关的需求,需求是对用户上传的整个Excel的数据进行验证,如果不符合格式要求,在表头最后增加一列“错误信息”描述此行数据错误原因。例如:代理人列不能为空;代理人手机号列如果填写有值肯定是需要符合手机号格式;结算方式列只允许填写“全保费、净保费”字段...
Excel校验模板
Hibernate Validator -集合对象验证(三)(可能是东半球最全的讲解了)

解决思路

1.根据模板分析,大体有以下几种校验规则

  • 列数据唯一校验
  • 字段为空校验(表头中带*号的都需要非空判断)
  • 日期列格式校验
  • 数值列格式校验 (金额保留最多保留2为小数,比例最多保留6为小数)
  • 手机号格式校验
  • 身份证号格式校验
  • 具体的业务逻辑(某些列只能填写固定的值;保单类型为批单时,批单号必须有值...)
    2.算法思想
  • 首先将Excel中的行数据全部读进来,转化为List<Map<String, Object>>,某些校验是无法在bean实体类上去校验的,实体类上只能做单个对象或者单个属性的校验,像序号列唯一性校验,这种需要依赖于整个Excel所有行数据去进行判断是否重复,只能在List<Map<String, Object>>去处理
  • 所以校验会分为两个层次,一层是在List层次的校验,一层是Bean层的数据校验
    基本就是这些吧,具体设计到业务逻辑的我就不说了,下面拿一种财务数据上传场景去看代码实现

代码实现

//将Excel中数据全部读进来转化指定的列;比如序号,rowNo:1
List<Map<String, Object>> mapList = fileInfoService.getExcelMaps(template.getId(), file, null, false);
if (CollectionUtils.isEmpty(mapList)) {
      return RestResponse.failedMessage("当前文件中数据为空");
}
//调取校验接口,并在Excel中增加一列错误信息列
RestResponse validateExcel = validateService.validMapListAndWriteFile(fileInfoService.getById(fileId), mapList, sourceCodeEnum);
if (!validateExcel.isSuccess()) {
    return validateExcel;
}
public RestResponse validMapListAndWriteFile(FileInfo fileInfo, List<Map<String, Object>> mapList, SourceCodeEnum sourceCode) {
        RestResponse restResponse = validMapList(mapList, sourceCode, SourceCodeModel.SOURCE_CODE_BEAN_CLASS_MAP.get(sourceCode));
        if (!restResponse.isSuccess()) {
            try {
                ValidationErrorWriter.errorWriteToExcel(fileInfo, (Map<Integer, List<ValidationErrorResult>>) restResponse.getRestContext());
                return RestResponse.failedMessage("表格数据校验未通过,请点击源文件下载");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return restResponse;
    }

其中SourceCodeModel.SOURCE_CODE_BEAN_CLASS_MAP.get(sourceCode)根据每一个上传模板获取对应的实体类信息

public static final Map<SourceCodeEnum, Class> SOURCE_CODE_BEAN_CLASS_MAP = new ImmutableMap.Builder<SourceCodeEnum, Class>()
            .put(SourceCodeEnum.BUSINESS_AUTO, DataPoolAutoValidModel.class)
            .put(SourceCodeEnum.BUSINESS_UNAUTO, DataPoolUnAutoValidModel.class)
            .put(SourceCodeEnum.BUSINESS_AUTO_SUPPLY, BusinessSupplyAutoModel.class)
            .put(SourceCodeEnum.BUSINESS_UNAUTO_SUPPLY, BusinessSupplyUnAutoModel.class)
            .put(SourceCodeEnum.COMMISSION_AUTO, CommissionApplyAutoModel.class)
            .put(SourceCodeEnum.COMMISSION_UNAUTO, CommissionApplyUnAutoModel.class)
            .put(SourceCodeEnum.SETTLEMENT_AUTO, SettlementApplyAutoModel.class)
            .put(SourceCodeEnum.SETTLEMENT_UNAUTO, SettlementApplyUnAutoModel.class)
            .put(SourceCodeEnum.COMMISSION_DETAIL_AUTO, CommissionBackAutoModel.class)
            .put(SourceCodeEnum.COMMISSION_DETAIL_UNAUTO, CommissionBackUnAutoModel.class)
            .put(SourceCodeEnum.BACK_AUTO, SettlementPayBackAutoModel.class)
            .put(SourceCodeEnum.BACK_UNAUTO, SettlementPayBackUnAutoModel.class)
            .put(SourceCodeEnum.COMMISSION_RESULT, CommissionResultBackModel.class)
            .build();

最终校验方法

public RestResponse validMapList(List<Map<String, Object>> mapList, SourceCodeEnum sourceCode, Class beanClazz) {
        if (CollectionUtils.isEmpty(mapList)) {
            return RestResponse.success();
        }

        Map<String, String> refMap = LoadTemplateInfoMap.getMappingInfoBySourceCode(sourceCode);
        Map<Integer, List<ValidationErrorResult>> errorMap;
        List beanClassList = null;
        try {
            //是否需要在Map层校验数据是否重复
            boolean businessImport = SourceCodeModel.businessImport(sourceCode);
            //车险非车险
            DataAutoType dataAutoType = SourceCodeModel.businessDataAutoType(sourceCode);
            //Map层校验
            Map<Integer, List<ValidationErrorResult>> validMap = mapValidationService.valid(mapList, businessImport, dataAutoType);
            //Bean层次校验
            beanClassList = CommonUtils.transMap2BeanForList(mapList, beanClazz);
            Map<Integer, List<ValidationErrorResult>> validBean = beanValidationService.validBeanList(beanClassList, beanClazz);
            if (haveErrorMessage(validMap) || haveErrorMessage(validBean)) {
                errorMap = makeValidErrorMap(validMap, validBean);
                setCorrectColumnName(errorMap, refMap);
                return RestResponse.failed(errorMap);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return RestResponse.success(beanClassList);
    }

Map层包含序号重复的校验,日期格式、数字格式和枚举类型的校验,具体的实现逻辑就不展现了,这不是重点要讲的,附一张代码格式的截图吧
Hibernate Validator -集合对象验证(三)(可能是东半球最全的讲解了)
重点:Bean层次的校验

  • 实体类
@Data
@EqualsAndHashCode(callSuper=false)
@CommissionValidType(groups = DataValidGroup.class)
@FinanceMatchType(groups = MatchGroup.class)
@MatchBusinessOrder(groups = MatchBusinessGroup.class)
@CommissionApplyTimes(groups = LimitTimesGroup.class)
public class CommissionApplyAutoModel extends Commission {

    @NotNull
    private Long rowNo;

    @NotNull
    private OrderType orderType;

    @NotBlank
    private String insuranceType;

    @NotBlank
    private String applicant;

    @NotNull
    private BigDecimal premium;

    @NotBlank
    private String policyNo;

    @NotBlank
    private String agent;

    @NotBlank
    private String agentBankNo;

    private String beneficiary;

    private BigDecimal commissionRate;

    private BigDecimal downstreamCommissionRate;

    private CommissionSettlementType coSettlementType;

    @Pattern(regexp = "^[1][3-9]\\d{9}$", message = "代理人手机号格式不正确")
    private String agentPhone;

    @NotBlank
    @Size(min = ValidateUtils.MIN_LICENSEPLATENO_LENGTH, max = ValidateUtils.MAX_LICENSEPLATENO_LENGTH,
            message = "车牌号长度在" + ValidateUtils.MIN_LICENSEPLATENO_LENGTH + "~" + ValidateUtils.MAX_LICENSEPLATENO_LENGTH + "之间")
    private String licensePlateNo;

}
  • 校验逻辑:校验实体类上的每一层 group,如果数据不符合规则,直接返回,某些group需要单独处理,eg:UniqueGroupBusinessSupplyGroup这些有些单独的处理场景,所以单列出来
public <T> Map<Integer, List<ValidationErrorResult>> validBeanList(List<T> dataList, Class clazz) throws InterruptedException {
        Map<Integer, List<ValidationErrorResult>> errorMap;
        //通过反射获取当前实体类上所有的group
        List<Class> groupList = getGroupClassFromBean(clazz);
        for (Class groupClass : groupList) {
            //去重校验
            if (groupClass == UniqueGroup.class) {
                errorMap = uniqueAndInsert(dataList, groupClass);
                if (ValidateService.haveErrorMessage(errorMap)) {
                    return errorMap;
                }
            } else if (groupClass == BusinessSupplyGroup.class) {
                //业务数据补足
                dataPoolService.updateBatchById((List<DataPool>) dataList, 1000);
            } else {
                //普通校验
                errorMap = valid(dataList, groupClass);
                if (ValidateService.haveErrorMessage(errorMap)) {
                    return errorMap;
                }
            }
        }
        return Collections.emptyMap();
    }
  • 关于group 都是一些空接口,上一章已经讲过,主要用来标注校验顺序的,没有其他深意
public interface BusinessSupplyGroup {
}

Hibernate Validator -集合对象验证(三)(可能是东半球最全的讲解了)

  • eg:注解@FinanceMatchType(groups = MatchGroup.class)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FinanceMatchTypeValidator.class)
@Documented
public @interface FinanceMatchType {

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default { };

    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        FinanceMatchType[] value();
    }

}
public class FinanceMatchTypeValidator extends PubValidator<FinanceMatchValidator, BusinessFinanceModel>
        implements ConstraintValidator<FinanceMatchType, BusinessFinanceModel> {
}
  • 多行数据校验过程中,使用多线程去校验
@Component
@Log4j2
public abstract class PubValidator<T extends Validator, E> {

    @Autowired
    private List<T> validatorList;
    @Autowired
    ThreadPool runThreadPool;

    public boolean isValid(E value, ConstraintValidatorContext context) {
        boolean result = true;
        List<Boolean> resultList = null;
        try {
            resultList = runThreadPool.submitWithResult(validatorList, validator -> validator.isValid(value, context));
        } catch (ExecutionException | InterruptedException e) {
            log.error("数据校验错误", e);
        }

        if (resultList != null && resultList.size() > 0) {
            result = resultList.stream().allMatch(i -> i);
        }
        return result;
    }

}
public interface FinanceMatchValidator extends Validator<BusinessFinanceModel> {
}
  • 当前层次有两个类型需要校验
  • 险种类型校验,车险业务,险种类型只能为{交强险,商业险};非车险业务,险种类别和性质不能为空
  • 保单类型校验,如果为批单,批单号不能为空
    对应到两个实现类
/**
 * author:Java
 * Date:2020/6/1 16:02
 * 校验:车险险种验证
 *      车险:险种是否为交强商业
 *      非车险:险种类别、性质
 */
@Component
public class InsuranceTypeValidateErrorMessage implements MatchGroupValidator, CommisssionDetailValidator, SettlementPayBackValidator, FinanceMatchValidator {

    @Override
    public boolean isValid(BusinessFinanceModel bfm, ConstraintValidatorContext context) {
        if (bfm.getDataAutoType() == DataAutoType.AUTO) {
            if (!validAutoInsuranceType(bfm.getInsuranceType(), bfm)) {
                setErrorMessage("insuranceType", "车险险种名称不能为空,只能填写:[交强险,商业险]", context);
                return false;
            }
        }
        return true;
    }

    private boolean validAutoInsuranceType(String insuranceType, BusinessFinanceModel bfm) {
        Long insuranceTypeId = null;
        InsuranceCategory insuranceCategory = null;
        for (Map.Entry<String, Long> entry : BusinessConstants.INSTRANCETYPE_ID.entrySet()) {
            if (insuranceType.contains(entry.getKey())) {
                insuranceTypeId = entry.getValue();
                insuranceCategory = insuranceTypeId == 1 ? InsuranceCategory.TRAFFIC_INSURANCE : InsuranceCategory.COMMERCIAL_INSURANCE;
                break;
            }
        }
        if (insuranceTypeId != null) {
            bfm.setInsuranceTypeId(insuranceTypeId);
            bfm.setInsuranceCategory(insuranceCategory);
            return true;
        }
        return false;
    }

}
/**
 * author:WangZhaoliang
 * Date:2020/6/1 15:48
 * 校验:保单类型不能为空
 *      保单类型为保单 || 被冲正保单 || 冲正保单时:保单号不能为空
 *      保单类型为被冲正批单 || 冲正批单 || 批单时:批单号不能为空
 */

@Component
public class OrderTypeValidateErrorMessage implements MatchGroupValidator, FinanceMatchValidator {

    @Override
    public boolean isValid(BusinessFinanceModel value, ConstraintValidatorContext context) {
        OrderType orderType = value.getOrderType();
        if (orderType == null) {
            setErrorMessage("orderType", "保单类型必填并且只能填写" + OrderType.getAllText(), context);
            return false;
        }

        // 保单、被冲正保单、冲正保单
        if (orderType == OrderType.POLICY || orderType == OrderType.REVERSED_POLICY || orderType == OrderType.CORRECTION_POLICY) {
            if (StringUtils.isBlank(value.getPolicyNo())) {
                setErrorMessage("policyNo", "保单类型为[保单、被冲正保单、冲正保单]时,保单号必须有值", context);
                return false;
            }
        } else if (OrderType.isBatchNo(orderType)) {
            if (StringUtils.isBlank(value.getBatchNo())) {
                setErrorMessage("batchNo", "保单类型为[批单、被冲正批单、冲正批单]时,批单号必须有值", context);
                return false;
            }
        }
        return true;
    }
}
/**
 * author:WangZhaoliang
 * Date:2020/6/3 11:34
 */
public interface ValidatorErrorMessage {

    default void setErrorMessage(String property, String message, ConstraintValidatorContext context) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message)
                .addPropertyNode(property)
                .addBeanNode()
                .addConstraintViolation();
    }

}

将所有实现了FinanceMatchValidator接口的实现类,都会加入到 PubValidator类中的List<T> validatorList中去,再调用 PubValidator类的isValid(E value, ConstraintValidatorContext context)方法去校验,将错误信息加入到ConstraintValidatorContext
每一层group都是如此校验,代码逻辑较多,就不再一一赘述了,最终将错误信息追加到Excel最后一列,反馈给用户
这样完成第一章节中提到的需求:校验整个Excel中的信息;看一下最终反馈到Excel中的错误信息

Hibernate Validator -集合对象验证(三)(可能是东半球最全的讲解了)
产品大佬终于露出了满意的笑脸...

这块目前只是刚上线了最初版本,后面还有需要优化的点,未完待续...
Java is the best language in the world

Hibernate Validator -集合对象验证(三)(可能是东半球最全的讲解了)

上一篇:1.0缓存:Login.aspx?


下一篇:【SQL】SQL优化:日期分组来统计数量