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

    技术2022-07-11  128

    #### Tips 在上线到测试环境之后,关于Spring boot的国际化问题,烦扰了我整整一天的时间,大家可以参考[这篇文章](https://www.jianshu.com/p/e2eae08f3255)去处理国际化的问题 #### 前情提示 前两篇文章已经介绍了Hibernate Validator的[对象基础验证](https://blog.csdn.net/Zeroooo00/article/details/106855474)和[对象分组验证](https://blog.csdn.net/Zeroooo00/article/details/106855580),没有看过的童鞋可以先去回顾一下,本章节主要解决第一章节所说的具体的业务需求 #### 业务需求 最近在做和Excel导入、导出相关的需求,需求是对用户上传的整个Excel的数据进行验证,如果不符合格式要求,在表头最后增加一列“错误信息”描述此行数据错误原因。例如:代理人列不能为空;代理人手机号列如果填写有值肯定是需要符合手机号格式;结算方式列只允许填写“全保费、净保费”字段... Excel校验模板 ![数据上传模板](https://upload-images.jianshu.io/upload_images/3803125-f43a5d667eb0aa83.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1240)

    #### 解决思路 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层包含序号重复的校验,日期格式、数字格式和枚举类型的校验,具体的实现逻辑就不展现了,这不是重点要讲的,附一张代码格式的截图吧 ![List<Map<String, Object>>层次校验](https://upload-images.jianshu.io/upload_images/3803125-dd0deee33d7834f6.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 重点: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:`UniqueGroup`、`BusinessSupplyGroup`这些有些单独的处理场景,所以单列出来 ``` 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 { } ``` ![group](https://upload-images.jianshu.io/upload_images/3803125-668ec53f84fbd291.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1240)、 - 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中的错误信息

    ![最终错误提示信息](https://upload-images.jianshu.io/upload_images/3803125-9ca630c00e705821.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 产品大佬终于露出了满意的笑脸... ---  这块目前只是刚上线了最初版本,后面还有需要优化的点,未完待续... **Java is the best language in the world**

    Processed: 0.019, SQL: 9