Hibernate Validator -集合對象驗證(三)(可能是東半球最全的講解了)

#### 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%7CimageView2/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%7CimageView2/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%7CimageView2/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%7CimageView2/2/w/1240)
產品大佬終於露出了滿意的笑臉...
--- 
這塊目前只是剛上線了最初版本,後面還有需要優化的點,未完待續...
**Java is the best language in the world**

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章