构建灵活可扩展的FastExcel导入字段校验系统
一、背景与需求
在企业级应用开发中,Excel文件导入是常见的功能需求。然而,用户上传的Excel数据往往存在各种问题,如格式错误、数据不规范、包含非法字符等。传统的手动校验方式效率低下且容易出错,因此我们需要一个统一、灵活、可扩展的Excel字段校验系统。
核心需求:
-
灵活配置:支持通过注解方式配置校验规则
-
多种校验类型:支持字符类型、长度、数字范围、正则表达式等多种校验
-
精确控制:能够精确控制哪些字符允许或禁止,特别是Emoji表情的处理
-
友好反馈:提供详细的错误信息,方便用户修改数据
-
易用性:简化使用方式,减少重复代码
-
可扩展性:支持自定义校验器和分组校验
-
**行级数据校验:**支持行级数据校验,可合并输出
二、系统设计
2.1 架构设计
整个校验系统采用注解驱动的方式,通过以下组件协同工作:
┌─────────────────────────────────────────────────────┐
│ Excel导入模块 │
├─────────────────────────────────────────────────────┤
│ 监听器(Listener) → 校验工具类 → 结果收集 → 返回前端 │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ExcelValid │ │ 校验枚举 │ │ 错误结果实体 │
│ 注解 │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
2.2 核心组件
-
校验注解 (
ExcelValid):定义字段的校验规则 -
字符类型枚举 (
CharacterValidationType):定义支持的字符类型 -
校验工具类 (
ExcelValidationUtil):执行具体的校验逻辑 -
结果实体:封装校验结果和错误信息
-
监听器 (CustomExcelValidListener):集成到Excel解析流程中
三、实现细节
3.1 包引入
XML
<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId>
<version>5.1.1</version>
</dependency>
3.2 字符类型枚举
CharacterValidationType 枚举定义了系统支持的所有字符类型,每种类型都包含正则表达式和错误信息:
java
package com.fantaibao.enums;
import lombok.Getter;
import java.math.BigDecimal;
import java.util.regex.Pattern;
/**
* 字符类型枚举
*/
@Getter
public enum CharacterValidationType {
/**
* 中文字符(包括中文标点)
*/
CHINESE("^[\\u4e00-\\u9fa5\\u3000-\\u303F\\uFF00-\\uFFEF\\u201C-\\u201F\\u3001-\\u301F]*$", "只能包含中文"),
/**
* 中文字符和中文符号
*/
CHINESE_WITH_SYMBOL("^[\\u4e00-\\u9fa5\\u3000-\\u303F\\uFF00-\\uFFEF]*$", "只能包含中文和中文符号"),
/**
* 英文字母(大小写)
*/
ENGLISH("^[A-Za-z]*$", "只能包含英文字母"),
/**
* 英文字母和英文符号
*/
ENGLISH_WITH_SYMBOL("^[A-Za-z\\p{P}\\p{S}]*$", "只能包含英文字母和英文符号"),
/**
* 数字(0-9)
*/
DIGIT("^[0-9]*$", "只能包含数字"),
/**
* 正整数(不包括0)
*/
POSITIVE_INTEGER("^[1-9]\\d*$", "只能为正整数"),
/**
* 非负整数(包括0)
*/
NON_NEGATIVE_INTEGER("^\\d+$", "只能为非负整数"),
/**
* 负整数
*/
NEGATIVE_INTEGER("^-[1-9]\\d*$", "只能为负整数"),
/**
* 整数(包括正负整数和0)
*/
INTEGER("^-?\\d+$", "只能为整数"),
/**
* 正小数(正浮点数)
*/
POSITIVE_DECIMAL("^[+]?([0-9]*\\.)?[0-9]+$", "只能为正小数"),
/**
* 负小数(负浮点数)
*/
NEGATIVE_DECIMAL("^-([0-9]*\\.)?[0-9]+$", "只能为负小数"),
/**
* 小数/浮点数(包括正负)
*/
DECIMAL("^-?([0-9]*\\.)?[0-9]+$", "只能为小数"),
/**
* 英文符号(常见标点符号)
*/
ENGLISH_SYMBOL("^[\\p{P}\\p{S}]*$", "只能包含英文符号"),
/**
* 中文符号
*/
CHINESE_SYMBOL("^[\\u3000-\\u303F\\uFF00-\\uFFEF]*$", "只能包含中文符号"),
/**
* 特殊符号(自定义的特殊字符)
*/
SPECIAL_SYMBOL("^[`~!@#$%^&*()_\\-+=\\[\\]{}|;:'\",.<>/?]*$", "只能包含特殊符号"),
/**
* 字母和数字
*/
ENGLISH_DIGIT("^[A-Za-z0-9]*$", "只能包含字母和数字"),
/**
* 中文字母数字
*/
CHINESE_ENGLISH_DIGIT("^[\\u4e00-\\u9fa5A-Za-z0-9]*$", "只能包含中文、字母和数字"),
/**
* 字母、数字和英文符号
*/
ENGLISH_DIGIT_SYMBOL("^[A-Za-z0-9\\p{P}\\p{S}]*$", "只能包含字母、数字和英文符号"),
NULL(null, "");
/**
* 正则表达式
*/
private final String pattern;
/**
* 错误信息
*/
private final String message;
CharacterValidationType(String pattern, String message) {
this.pattern = pattern;
this.message = message;
}
/**
* 校验字符串是否符合该字符类型
*/
public boolean validate(String value) {
if (value == null || value.isEmpty()) {
// 空值不校验,如果需要校验空值,请配合@ExcelValid的required属性
return false;
}
return !Pattern.matches(pattern, value);
}
/**
* 校验是否为数字(整数或小数) - 修正版
* 修正:原方法逻辑反了,应该返回true表示是数字
*/
public static boolean isNumeric(String value) {
if (value == null || value.isEmpty()) {
return false;
}
try {
new BigDecimal(value);
return true;
} catch (NumberFormatException e) {
return false;
}
}
/**
* 校验小数位数
* @param value 要校验的值
* @param decimalPlaces 要求的小数位数
* @return 是否符合要求
*/
public static boolean validateDecimalPlaces(String value, int decimalPlaces) {
if (value == null || value.isEmpty() || !isNumeric(value)) {
return true;
}
BigDecimal decimal = new BigDecimal(value);
int scale = decimal.scale();
// 如果是整数,scale为0
return scale > decimalPlaces;
}
/**
* 校验数字范围
* @param value 要校验的值
* @param min 最小值(可为null)
* @param max 最大值(可为null)
* @return 是否在范围内
*/
public static boolean validateNumberRange(String value, BigDecimal min, BigDecimal max) {
if (value == null || value.isEmpty() || !isNumeric(value)) {
return true;
}
BigDecimal decimal = new BigDecimal(value);
if (min != null && decimal.compareTo(min) < 0) {
return true;
}
return max != null && decimal.compareTo(max) > 0;
}
}
设计要点:
-
使用正则表达式定义字符类型范围
-
为
MIXED类型特殊处理,只排除Emoji而不限制其他字符 -
提供静态方法支持数字范围、小数位数等高级校验
3.3 校验注解
ExcelValid 注解是系统的核心配置接口,支持丰富的校验选项:
java
package com.fantaibao.annotation;
import java.lang.annotation.*;
/**
* Excel字段校验注解
* @author 石头
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelValid {
/**
* 是否跳过校验
* 默认false,表示进行校验;true表示跳过所有校验
*/
boolean skip() default false;
/**
* 是否必填
*/
boolean required() default false;
/**
* 必填时的错误提示信息
*/
String requiredMessage() default "不能为空";
/**
* 最小长度
*/
int minLength() default 0;
/**
* 最大长度
*/
int maxLength() default Integer.MAX_VALUE;
/**
* 长度校验失败时的错误信息
*/
String lengthMessage() default "长度不符合要求";
/**
* 自定义正则表达式(优先级高于allowedTypes)
*/
String regex() default "";
/**
* 自定义正则表达式校验失败时的错误信息
*/
String regexMessage() default "格式不正确";
/**
* 是否为数字(整数或小数)
*/
boolean numeric() default false;
/**
* 数字校验失败时的错误信息
*/
String numericMessage() default "必须为数字";
/**
* 是否为整数
*/
boolean integer() default false;
/**
* 整数校验失败时的错误信息
*/
String integerMessage() default "必须为整数";
/**
* 是否为正整数(大于0)
*/
boolean positiveInteger() default false;
/**
* 正整数校验失败时的错误信息
*/
String positiveIntegerMessage() default "必须为正整数";
/**
* 是否为非负整数(大于等于0)
*/
boolean nonNegativeInteger() default false;
/**
* 非负整数校验失败时的错误信息
*/
String nonNegativeIntegerMessage() default "必须为非负整数";
/**
* 是否为负整数
*/
boolean negativeInteger() default false;
/**
* 负整数校验失败时的错误信息
*/
String negativeIntegerMessage() default "必须为负整数";
/**
* 是否为小数
*/
boolean decimal() default false;
/**
* 小数位数限制(仅当decimal=true时有效,-1表示不限制)
*/
int decimalPlaces() default -1;
/**
* 小数位数校验失败时的错误信息
*/
String decimalMessage() default "小数位数不符合要求";
/**
* 最小值(对于数字)
*/
String minValue() default "";
/**
* 最大值(对于数字)
*/
String maxValue() default "";
/**
* 数值范围校验失败时的错误信息
*/
String rangeMessage() default "数值不在允许范围内";
/**
* 是否包含Emoji
*/
boolean allowEmoji() default false;
/**
* Emoji校验失败时的错误信息
*/
String emojiMessage() default "不能包含Emoji表情";
}
设计要点:
-
默认值设计合理,简化使用
-
支持多种校验类型,可组合使用
-
提供详细的错误信息配置
3.4 校验工具类
ExcelValidationUtil 是系统的核心执行引擎,负责解析注解并执行校验:
java
package com.fantaibao.utils.excelUtil;
import cn.hutool.core.collection.CollUtil;
import cn.idev.excel.annotation.ExcelProperty;
import com.fantaibao.annotation.ExcelValid;
import com.fantaibao.enums.CharacterValidationType;
import com.fantaibao.vo.ExcelValidationBaseResultVo;
import com.fantaibao.vo.ExcelValidationErrorVo;
import com.vdurmont.emoji.EmojiParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Excel校验工具类 - 返回校验结果,不抛出异常
*/
@Slf4j
public class ExcelValidationUtil {
/**
* 校验单个对象的字段
* @param obj 要校验的对象
* @param rowIndex 行索引(0-based)
* @return 错误列表,如果校验通过返回空列表
*/
public static List<ExcelValidationErrorVo> validate(Object obj, int rowIndex) {
List<ExcelValidationErrorVo> errors = new ArrayList<>();
if (Objects.isNull(obj)) {
return errors;
}
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ExcelValid excelValid = field.getAnnotation(ExcelValid.class);
if (excelValid == null) {
continue;
}
// 如果设置了skip=true,则跳过该字段的所有校验
if (excelValid.skip()) {
continue;
}
field.setAccessible(true);
try {
String value = (String) field.get(obj);
String fieldName = getFieldDisplayName(field);
// 执行校验
List<ExcelValidationErrorVo> fieldErrors = doValidate(value, excelValid, fieldName, rowIndex);
if (!fieldErrors.isEmpty()) {
errors.addAll(fieldErrors);
}
} catch (IllegalAccessException e) {
log.error("反射获取字段值失败", e);
errors.add(ExcelValidationErrorVo.builder()
// Excel行号从1开始
.rowNumber(rowIndex + 1)
.fieldName(field.getName())
.errorMessage("字段访问失败")
.skipRow(false)
.build());
} catch (ClassCastException e) {
// 如果不是String类型,跳过校验(或根据需要处理)
log.debug("字段{}不是String类型,跳过校验", field.getName());
}
}
return errors;
}
/**
* 获取字段的显示名称(优先使用ExcelProperty注解的值)
*/
private static String getFieldDisplayName(Field field) {
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (excelProperty != null && excelProperty.value().length > 0) {
return excelProperty.value()[0];
}
return field.getName();
}
/**
* 执行具体的校验逻辑
*/
private static List<ExcelValidationErrorVo> doValidate(String value, ExcelValid excelValid, String fieldName, int rowIndex) {
List<ExcelValidationErrorVo> errors = new ArrayList<>();
// 1. 必填校验
if (excelValid.required() && !StringUtils.hasText(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.requiredMessage())
// 如果必填字段为空,跳过该行
.skipRow(excelValid.required())
.build());
// 如果必填为空,直接返回
return errors;
}
// 如果没有值,且不是必填,直接返回(不进行后续校验)
if (!StringUtils.hasText(value)) {
return errors;
}
// 2. 长度校验
int length = value.length();
if (length < excelValid.minLength() || length > excelValid.maxLength()) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(String.format("%s,要求长度%d-%d,实际长度%d", excelValid.lengthMessage(), excelValid.minLength(), excelValid.maxLength(), length))
.skipRow(false)
.build());
}
// 3. 自定义正则校验(优先级最高)
if (StringUtils.hasText(excelValid.regex())) {
if (!Pattern.matches(excelValid.regex(), value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName).originalValue(value)
.errorMessage(excelValid.regexMessage())
.skipRow(false)
.build());
// 正则校验失败,直接返回
return errors;
}
}
// 4. 数字类型校验
if (excelValid.numeric() && CharacterValidationType.isNumeric(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.numericMessage())
.skipRow(false)
.build());
}
// 5. 整数类型校验
if (excelValid.integer() && CharacterValidationType.INTEGER.validate(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.integerMessage())
.skipRow(false)
.build());
}
// 6. 正整数校验(新增)
if (excelValid.positiveInteger() && CharacterValidationType.POSITIVE_INTEGER.validate(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.positiveIntegerMessage())
.skipRow(false)
.build());
}
// 7. 非负整数校验(新增)
if (excelValid.nonNegativeInteger() && CharacterValidationType.NON_NEGATIVE_INTEGER.validate(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.nonNegativeIntegerMessage())
.skipRow(false)
.build());
}
// 8. 负整数校验(新增)
if (excelValid.negativeInteger() && CharacterValidationType.NEGATIVE_INTEGER.validate(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.negativeIntegerMessage())
.skipRow(false)
.build());
}
// 9. 小数类型校验
if (excelValid.decimal() && CharacterValidationType.DECIMAL.validate(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.decimalMessage())
.skipRow(false)
.build());
} else if (excelValid.decimal() && excelValid.decimalPlaces() >= 0) {
if (CharacterValidationType.validateDecimalPlaces(value, excelValid.decimalPlaces())) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(String.format("%s,最多允许%d位小数", excelValid.decimalMessage(), excelValid.decimalPlaces()))
.skipRow(false)
.build());
}
}
// 10. 数值范围校验
if (StringUtils.hasText(excelValid.minValue()) || StringUtils.hasText(excelValid.maxValue())) {
BigDecimal min = null;
BigDecimal max = null;
try {
if (StringUtils.hasText(excelValid.minValue())) {
min = new BigDecimal(excelValid.minValue());
}
if (StringUtils.hasText(excelValid.maxValue())) {
max = new BigDecimal(excelValid.maxValue());
}
if (CharacterValidationType.validateNumberRange(value, min, max)) {
String rangeDesc = "";
if (min != null && max != null) {
rangeDesc = String.format("应在%s-%s之间", min, max);
} else if (min != null) {
rangeDesc = String.format("应大于等于%s", min);
} else if (max != null) {
rangeDesc = String.format("应小于等于%s", max);
}
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(String.format("%s,%s", excelValid.rangeMessage(), rangeDesc))
.skipRow(false)
.build());
}
} catch (NumberFormatException e) {
log.warn("minValue或maxValue格式不正确", e);
}
}
// 11. Emoji校验
if (excelValid.allowEmoji() && isIncludedEmoji(value)) {
errors.add(ExcelValidationErrorVo.builder()
.rowNumber(rowIndex + 1)
.fieldName(fieldName)
.originalValue(value)
.errorMessage(excelValid.emojiMessage())
.skipRow(false)
.build());
}
return errors;
}
/**
* 判断字符串是否包含Emoji字符
*
* @param input 待验证的字符串
* @return 如果字符串包含Emoji字符,则返回 true;否则返回 false
*/
public static boolean isIncludedEmoji(String input) {
if (input == null || input.isEmpty()) {
return false;
}
// 提取所有Emoji
List<String> emojis = EmojiParser.extractEmojis(input);
return CollUtil.isNotEmpty(emojis);
}
/**
* 批量校验
* @param dataList 数据列表
* @param <T> 数据类型
* @return 所有校验错误的列表
*/
public static <T> List<ExcelValidationErrorVo> validateBatch(List<T> dataList) {
List<ExcelValidationErrorVo> allErrors = new ArrayList<>();
if (dataList == null || dataList.isEmpty()) {
return allErrors;
}
for (int i = 0; i < dataList.size(); i++) {
List<ExcelValidationErrorVo> errors = validate(dataList.get(i), i);
if (!errors.isEmpty()) {
allErrors.addAll(errors);
}
}
return allErrors;
}
/**
* 批量校验并分类数据
* @param dataList 原始数据列表
* @return 包含成功数据和错误信息的复合结果
*/
public static <T> ExcelValidationBaseResultVo<T> validateAndClassify(List<T> dataList) {
ExcelValidationBaseResultVo<T> result = new ExcelValidationBaseResultVo<>();
result.setTotalCount(dataList.size());
for (int i = 0; i < dataList.size(); i++) {
T data = dataList.get(i);
List<ExcelValidationErrorVo> errors = validate(data, i);
if (errors.isEmpty()) {
result.getSuccessData().add(data);
result.setSuccessCount(result.getSuccessCount() + 1);
} else {
// 判断是否需要跳过该行(比如必填字段为空)
boolean skipRow = errors.stream().anyMatch(ExcelValidationErrorVo::getSkipRow);
if (!skipRow) {
// 如果不跳过,可能只记录第一个错误
result.getExcelErrorList().add(errors.get(0));
} else {
// 如果跳过,添加错误信息
result.getExcelErrorList().addAll(errors);
}
}
}
// 检查是否全部成功
result.setAllSuccess(result.getExcelErrorList().isEmpty());
return result;
}
}
算法亮点:
-
校验顺序优化:按照优先级执行校验,失败后及时返回
-
Emoji精确检测:使用精确的码点检测,避免将常见符号误判为Emoji
-
错误收集:支持一行多错,但默认只返回第一个错误
3.5 结果实体
系统提供两种结果实体,支持灵活的返回方式:
java
package com.fantaibao.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExcelValidationBaseResultVo<T> {
/**
* 成功导入条数
*/
private Integer successCount;
/**
* 成功数据列表
*/
private List<T> successData;
/**
* 异常导入数据集合
*/
private List<ExcelValidationErrorVo> excelErrorList;
/**
* 是否全部成功
*/
private boolean allSuccess;
/**
* 总行数
*/
private Integer totalCount;
}
java
package com.fantaibao.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ExcelValidationErrorVo {
/**
* 行号(Excel行号,从1开始)
*/
private Integer rowNumber;
/**
* 字段名称
*/
private String fieldName;
/**
* 原始值
*/
private String originalValue;
/**
* 错误信息
*/
private String errorMessage;
/**
* 是否跳过该行(当一行有多个错误时,可能只需要一个错误信息)
*/
@Builder.Default
private Boolean skipRow = false;
}
3.6 Excel监听器
CustomExcelValidListener将校验系统集成到Excel解析流程中:
java
package com.fantaibao.listener;
import cn.hutool.core.collection.CollUtil;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.read.listener.ReadListener;
import cn.idev.excel.util.StringUtils;
import com.fantaibao.utils.excelUtil.ExcelValidationUtil;
import com.fantaibao.vo.ExcelValidationBaseResultVo;
import com.fantaibao.vo.ExcelValidationErrorVo;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Slf4j
@Getter
@RequiredArgsConstructor
public class CustomExcelValidListener<T> implements ReadListener<T> {
/**
* 存储所有解析的数据(包括成功和失败的)
*/
private List<T> allDataList;
/**
* 存储成功的数据
*/
private List<T> successDataList;
/**
* 存储校验错误
*/
private List<ExcelValidationErrorVo> errorList;
/**
* 最终返回结果
*/
private ExcelValidationBaseResultVo<T> resultVo;
/**
* 表头字段列表
*/
private List<String> fields;
/**
* 是否启用字段校验(默认启用)
*/
private boolean enableValidation = true;
/**
* 是否立即校验(在invoke方法中校验)
*/
private boolean validateImmediately = true;
/**
* 是否严格模式(表头校验失败则终止)
*/
private boolean strictMode = true;
/**
* 默认构造方法
*/
public CustomExcelValidListener(Class<T> clazz) {
this(clazz, true, true, true);
}
/**
* 构造方法
*/
public CustomExcelValidListener(Class<T> clazz, boolean enableValidation, boolean validateImmediately, boolean strictMode) {
this.allDataList = new ArrayList<>();
this.successDataList = new ArrayList<>();
this.errorList = new ArrayList<>();
this.resultVo = new ExcelValidationBaseResultVo<>();
this.fields = getFields(clazz);
this.enableValidation = enableValidation;
this.validateImmediately = validateImmediately;
this.strictMode = strictMode;
}
/**
* 根据class通过反射获取字段上@ExcelProperty注解的值
*/
private List<String> getFields(Class<T> clazz) {
List<String> fields = CollUtil.newArrayList();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field field : declaredFields) {
ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
if (annotation != null) {
String[] value = annotation.value();
fields.addAll(Arrays.asList(value));
}
}
return fields;
}
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
// 在这里检查表头是否正确
List<String> headerErrors = new ArrayList<>();
for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) {
String headerValue = entry.getValue().getStringValue();
if (!isValidHeader(headerValue)) {
String error = String.format("表头'%s'不匹配", headerValue);
headerErrors.add(error);
}
}
// 检查是否所有必要的表头都被匹配
if (!fields.isEmpty()) {
String error = String.format("缺少必要的表头: %s", String.join(", ", fields));
headerErrors.add(error);
}
// 处理表头错误
if (!headerErrors.isEmpty()) {
String errorMessage = String.join("; ", headerErrors);
if (strictMode) {
throw new RuntimeException("模板错误: " + errorMessage);
} else {
// 非严格模式下,记录错误但不中断
log.warn("表头校验失败: {}", errorMessage);
ExcelValidationErrorVo errorVo = ExcelValidationErrorVo.builder()
// 表头错误行号为0
.rowNumber(0)
.fieldName("表头")
.errorMessage("模板错误: " + errorMessage)
.build();
errorList.add(errorVo);
}
}
}
@Override
public void invoke(T data, AnalysisContext context) {
int rowIndex = context.readRowHolder().getRowIndex();
allDataList.add(data);
if (enableValidation && validateImmediately) {
// 立即校验当前行
List<ExcelValidationErrorVo> errors = ExcelValidationUtil.validate(data, rowIndex);
if (errors.isEmpty()) {
successDataList.add(data);
} else {
// 判断是否需要跳过该行(比如必填字段为空)
boolean skipRow = errors.stream().anyMatch(ExcelValidationErrorVo::getSkipRow);
if (!skipRow) {
// 如果不跳过,可能只记录第一个错误
errorList.add(errors.get(0));
} else {
// 如果跳过,添加所有错误信息
errorList.addAll(errors);
}
}
} else {
// 不进行立即校验,先全部加入成功列表(后续统一校验)
successDataList.add(data);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 如果未启用立即校验,则在所有解析完成后批量校验
if (enableValidation && !validateImmediately) {
// 清空之前的成功列表,重新校验
successDataList.clear();
errorList.clear();
for (int i = 0; i < allDataList.size(); i++) {
T data = allDataList.get(i);
List<ExcelValidationErrorVo> errors = ExcelValidationUtil.validate(data, i);
if (errors.isEmpty()) {
successDataList.add(data);
} else {
// 判断是否需要跳过该行
boolean skipRow = errors.stream().anyMatch(ExcelValidationErrorVo::getSkipRow);
if (!skipRow) {
// 如果不跳过,只记录第一个错误
errorList.add(errors.get(0));
} else {
// 如果跳过,添加所有错误信息
errorList.addAll(errors);
}
}
}
}
// 构建返回结果
resultVo.setSuccessData(new ArrayList<>(successDataList));
resultVo.setSuccessCount(successDataList.size());
resultVo.setExcelErrorList(new ArrayList<>(errorList));
resultVo.setTotalCount(allDataList.size());
resultVo.setAllSuccess(errorList.isEmpty());
log.info("Excel解析完成,sheet名: {},总行数: {},成功: {},失败: {}",
context.readSheetHolder().getSheetName(),
resultVo.getTotalCount(),
resultVo.getSuccessCount(),
errorList.size());
}
/**
* 获取最终结果
*/
public ExcelValidationBaseResultVo<T> getResult() {
return resultVo;
}
private boolean isValidHeader(String header) {
if (StringUtils.isBlank(header)) {
return true;
}
// 移除空白字符后比较
String trimmedHeader = header.trim();
for (String field : fields) {
if (field.equals(trimmedHeader)) {
fields.remove(field);
return true;
}
}
return false;
}
}
四、使用示例
4.1 实体类配置
java
package com.fantaibao.module.vo.appDish;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import com.fantaibao.annotation.ExcelValid;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@ColumnWidth(25)
@AllArgsConstructor
@NoArgsConstructor
@ExcelIgnoreUnannotated
public class DishAppRankingListVo1 {
/**
* 菜品名称
*/
@ColumnWidth(30)
@ExcelProperty("菜品名称")
// 不需要校验
@ExcelValid(skip = true)
private String menuName;
/**
* 理想销量排名
*/
@ColumnWidth(20)
@ExcelProperty("理想销量排名")
@ExcelValid(
minValue = "1",
maxValue = "9999",
positiveInteger = true
)
private String salesRanking;
/**
* 理想实收排名
*/
@ColumnWidth(20)
@ExcelProperty("理想实收排名")
@ExcelValid(
minValue = "1",
maxValue = "9999",
positiveInteger = true
)
private String turnoverRanking;
/**
* 理想毛利额排名
*/
@ColumnWidth(15)
@ExcelProperty("理想毛利额排名")
@ExcelValid(
minValue = "1",
maxValue = "9999",
positiveInteger = true
)
private String profitRanking;
}
4.2 业务层使用
java
@Override
public ExcelValidationBaseResultVo<DishAppRankingListVo1> importExcelRankingTest(String fileUrl) {
RLock lock = redissonClient.getLock(String.format(DATA_ANALYSIS_STANDARD_RANKING_LOCK_KEY, UserProvider.getUser().getTenantId()));
Assert.isFalse(lock.isLocked(), "当前租户标准菜品排名配置正在变更中,请稍后再试");
try {
if (!lock.tryLock(60, -1, TimeUnit.SECONDS)) {
throw new RuntimeException("无法获取分布式锁,请稍后再试");
}
try {
// 创建监听器
CustomExcelValidListener<DishAppRankingListVo1> listener = new CustomExcelValidListener<>(DishAppRankingListVo1.class, true, true, true);
// 读取Excel文件并填充菜品列表
EasyExcel.read(new URL(fileUrl).openStream(), DishAppRankingListVo1.class, listener)
.sheet(1)
.doRead();
// 获取结果
ExcelValidationBaseResultVo<DishAppRankingListVo1> result = listener.getResult();
// 如果存在成功数据,可以进一步处理(如保存到数据库)
if (result.getSuccessCount() > 0) {
}
// 记录错误信息
if (!result.isAllSuccess()) {
logImportErrors(result);
}
return result;
} catch (IOException e) {
log.error("读取文件失败", e);
return ExcelValidationBaseResultVo.<DishAppRankingListVo1>builder()
.successCount(0)
.allSuccess(false)
.build();
} catch (Exception e) {
// 处理表头错误等严重异常
log.error("Excel导入失败", e);
return ExcelValidationBaseResultVo.<DishAppRankingListVo1>builder()
.successCount(0)
.allSuccess(false)
.build();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
五、关键技术点
5.1 校验优先级设计
系统按照以下优先级执行校验,确保校验效率和准确性:
-
必填校验:失败则立即返回,不进行后续校验
-
长度校验:基础校验,失败不立即返回
-
自定义正则:优先级最高,失败则立即返回
-
数字类型校验:按类型分别校验
-
范围校验:依赖于数字类型校验
-
特殊字符校验:检查特殊符号和Emoji
-
字符类型校验:最后执行的综合校验
5.2 性能优化
-
正则表达式预编译:避免重复编译正则表达式
-
校验短路:失败后及时返回,减少不必要的校验
-
批量处理:支持批量校验,减少反射开销
-
对象复用:复用校验对象,避免重复创建
