在做 Excel 数据导入时,难免会需要对输入的数据做一些规则校验来验证合法性,但是有时候表格的字段很多,规则也会随之增长,如果不对规则进行模块化,全部堆积到一个方法,后期将会变得难以维护。
使用 EasyExcel 来解析我们的 Excel 表格,然后通过 JSR303 注解来对我们的数据规则做校验,将规则校验的代码与我们的业务代码做分离,便于维护和扩展。
EasyExcel 是阿里巴巴开源的一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。
他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能,具体的使用可以参考 EasyExcel的官方文档。
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。HibernateValidator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
| Constraint | 详细信息 |
|---|---|
| @Null | 被注释的元素必须为 null |
| @NotNull | 被注释的元素必须不为 null |
| @AssertTrue | 被注释的元素必须为 true |
| @AssertFalse | 被注释的元素必须为 false |
| @Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
| @Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
| @DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
| @DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
| @Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
| @Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
| @Past | 被注释的元素必须是一个过去的日期 |
| @Future | 被注释的元素必须是一个将来的日期 |
| @Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
| Constraint | 详细信息 |
|---|---|
| 被注释的元素必须是电子邮箱地址 | |
| @Length | 被注释的字符串的大小必须在指定的范围内 |
| @NotEmpty | 被注释的字符串的必须非空 |
| @Range | 被注释的元素必须在合适的范围内 |
如果你是基于SpringBoot开发的话,直接引入以下依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
假设我有一张如下表格,需要对年龄做一个校验,校验年龄必须大于等于18才算合法的。
| 姓名 | 年龄 | 生日 |
|---|---|---|
| 张三 | 16 | 2022-09-11 |
| 李四 | 32 | 1998-11-12 |
| 王五 | 22 | 1992-06-09 |
EasyExcel 解析 excel 表格的时候需要一个实体类作映射,然后每一行数据对应一个 User 对象,通过给 User 对象的age属性设置一个@Min 注解。
public class User {
@ExcelProperty(index = 0)
private String name;
@Min(value = 18, message = "年龄不能小于18")
@ExcelProperty(index = 1)
private Integer age;
@ExcelProperty(index = 2)
private Date birthday;
// 省略get set...
}
EasyExcel 在解析Excel的时候每读取到一行数据,就会通过invoke方法进行回调让我们来消费,在这个时候我们就可以对数据进行校验。
public class UserListener extends AnalysisEventListener<User> {
private final Validator validator;
{
// 初始化校验器对象
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
/**
* 每读取到一行数据后进行回调
*
* @param data 数据
* @param context 上下文对象
*/
@Override
public void invoke(User data, AnalysisContext context) {
// 校验数据获取校验信息
Set<ConstraintViolation<User>> constraintViolations = validator.validate(data);
if (Objects.nonNull(constraintViolations) && !constraintViolations.isEmpty()) {
System.out.println("不合规的数据:" + constraintViolations);
} else {
System.out.println("数据校验通过");
}
}
/**
* 当所有数据读取完毕后会被执行
*
* @param context 上下文对象
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
通过 validator 的 validate方法对我们的数据进行校验,会得到Set<ConstraintViolation<T>>集合,通过判断集合内是否有元素,如果有元素那说明该条数据有未通过校验的规则,会将不符合规则的信息存放到这个集合,如果没有元素说明数据符合规则校验通过。
通过上面的例子演示,我们可以对单个属性做规则校验,还是以上面那个用户表为例子,如果现在新增一个需求,需要结合生日判断用户的年龄填写是否正确,这时候我们需要两个属性作为一组输入去做校验。那么又该如何实现了,翻看Hibernate Validator 提供的注解都是对单个属性的规则校验,很显然已经无法满足我们的需求。这时可以通过自定义注解,来完成我们的定制化规则校验需求。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IsUpperValidator.class)
@Documented
public @interface CheckAge {
/**
* 错误提示信息
*/
String message() default "生日与年龄不符合";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
首先定义一个类级别的注解,然后@Constraint指定你自己的约束验证器,即可实现一个自己的约束注解。
public class CheckAgeValidator implements ConstraintValidator<CheckAge, User> {
@Override
public boolean isValid(User value, ConstraintValidatorContext context) {
// 计算年龄
int age = computeAge(value.getBirthday(), new Date());
return age == value.getAge();
}
private int computeAge(Date birthday, Date date) {
// TODO 计算出生年月距离现在的年份
}
}
最后笔者对通过上面的代码示例,向上做了一些封装,将一些错误信息,统一封装到CheckFailRow的对象,方便调用者获取。
public abstract class RuleCheckListener<T> extends AnalysisEventListener<T> {
/**
* 验证器对象,用来验证数据是否符合规范
*/
private final Validator validator;
/**
* 是否继续读取数据,默认为true,当读取到不符合规则的数据时,读取结束
*/
private boolean isContinue = true;
{
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
/**
* 每读到一行数据都会调用这个方法进行消费,由子类去实现扩展
*
* @param data 数据行
* @param context 上下文
*/
@Override
public abstract void invoke(T data, AnalysisContext context);
/**
* 读取完所有数据会来调用,可以用来做一些善后工作
*
* @param context 上下文
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
/**
* 是否读取下一行, 如果返回false将不会继续读下去
*
* @param context 上下文
* @return 是否继续读
*/
@Override
public final boolean hasNext(AnalysisContext context) {
return isContinue;
}
/**
* 停止读取数据行
*/
public final void stop() {
this.isContinue = false;
}
/**
* 对数据行进行规则校验
*
* @param data 数据行
* @param context 上下文
* @return 校验结果集, 如果为空说明校验通过
*/
public final List<CheckFailRow> check(T data, AnalysisContext context) {
List<CheckFailRow> list = new ArrayList<>();
ReadRowHolder readRowHolder = context.readRowHolder();
Integer rowIndex = readRowHolder.getRowIndex();
Set<ConstraintViolation<T>> validateResult = validator.validate(data);
if (Objects.nonNull(validateResult) && !validateResult.isEmpty()) {
for (ConstraintViolation<T> violation : validateResult) {
list.add(parse(rowIndex, violation));
}
}
return list;
}
/**
* 解析出错信息,并封装对象返回
*
* @param rowIndex 出错行
* @param violation 出错信息
* @return 出错信息对象
*/
private CheckFailRow parse(Integer rowIndex, ConstraintViolation<T> violation) {
CheckFailRow checkFailRow = new CheckFailRow();
checkFailRow.setRowIndex(rowIndex);
checkFailRow.setMessage(violation.getMessage());
checkFailRow.setProperty(violation.getInvalidValue());
return checkFailRow;
}
}
public class CheckFailRow {
/**
* 不符合数据校验规则的所在行位置
*/
private Integer rowIndex;
/**
* 不符合数据校验规则的提示信息
*/
private String message;
/**
* 不符合数据校验规则的属性值
*/
private Object property;
}
@RestController
@RequestMapping
public class UserController {
@RequestMapping("/upload")
public void upload(@RequestParam("user") MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), User.class, new UserListener()).sheet().doRead();
}
}
RuleCheckListener 抽象类实现自己的监听器对象public class UserListener extends RuleCheckListener<User> {
/**
* 每读取到一行数据后进行回调
*
* @param data 数据
* @param context 上下文对象
*/
@Override
public void invoke(User data, AnalysisContext context) {
List<CheckFailRow> check = check(data, context);
if (CollectionUtils.isEmpty(check)){
// 数据校验通过
}else {
// 数据校验未通过,可以遍历check集合拿到每一条错误信息
// 另外还可以通过调用stop方法停止继续往下读取
stop();
}
}
}
工具 — Aug 22, 2022