#### 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校验模板 
#### 解决思路 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层包含序号重复的校验,日期格式、数字格式和枚举类型的校验,具体的实现逻辑就不展现了,这不是重点要讲的,附一张代码格式的截图吧  重点: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 { } ``` 、 - 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中的错误信息
 产品大佬终于露出了满意的笑脸... --- 这块目前只是刚上线了最初版本,后面还有需要优化的点,未完待续... **Java is the best language in the world**