基于Easy Excel的表头校验工具
ExcelHeaderValidator
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Excel表头校验工具类
* 用于校验上传的Excel文件表头是否与DTO中定义的表头一致
*
* @author Ryan
*/
@Slf4j
public class ExcelHeaderValidator {
/**
* 创建一个带表头校验的数据监听器
*
* @param modelClass DTO模型类
* @param templateTypeName 模板类型名称(用于错误提示)
* @param dataConsumer 数据消费者,处理每一行数据
* @param <T> DTO类型
* @return 带表头校验的监听器
*/
public static <T> AnalysisEventListener<T> createValidationListener(
Class<T> modelClass,
String templateTypeName,
Consumer<T> dataConsumer) {
Map<Integer, String> expectedHeaders = getExpectedHeaders(modelClass);
return new ValidationHeaderListener<>(expectedHeaders, templateTypeName, dataConsumer);
}
/**
* 从DTO类中提取期望的表头
*
* @param modelClass DTO模型类
* @return 表头索引与表头名称的映射
*/
public static Map<Integer, String> getExpectedHeaders(Class<?> modelClass) {
// 使用字段在类中的声明顺序作为索引,而不是annotation.index()
// 因为当注解未指定index时,默认值(-1或Integer.MAX_VALUE)会导致重复key异常
List<Field> fields = Arrays.stream(modelClass.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(ExcelProperty.class))
.collect(Collectors.toList());
Map<Integer, String> result = new java.util.LinkedHashMap<>();
for (int i = 0; i < fields.size(); i++) {
Field field = fields.get(i);
ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
String headerName = annotation.value().length > 0 ? annotation.value()[0] : field.getName();
result.put(i, headerName);
}
return result;
}
/**
* 按顺序获取对象中每个字段的值
*
* @param data 数据对象
* @param <T> 对象类型
* @return 字段索引与字段值的映射(值转换为String)
*/
public static <T> Map<Integer, String> getFieldValues(T data) {
if (data == null) {
return new java.util.LinkedHashMap<>();
}
List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
.collect(Collectors.toList());
Map<Integer, String> result = new java.util.LinkedHashMap<>();
for (int i = 0; i < fields.size(); i++) {
Field field = fields.get(i);
field.setAccessible(true);
try {
Object value = field.get(data);
result.put(i, value != null ? value.toString() : "");
} catch (IllegalAccessException e) {
log.error("获取字段值失败: {}", field.getName(), e);
result.put(i, "");
}
}
return result;
}
}
ValidationHeaderListener
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.util.ConverterUtils;
import com.szyk.car.common.web.config.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* <p> 导入表头校验监听器</p>
* <p> Create Time: 2026/2/4 </p>
*
* @author Ryan
*/
@Slf4j
public class ValidationHeaderListener<T> extends AnalysisEventListener<T> {
private final Map<Integer, String> expectedHeaders;
private final String templateTypeName;
private final Consumer<T> dataConsumer;
private boolean headerValidated = false;
public ValidationHeaderListener(Map<Integer, String> expectedHeaders, String templateTypeName, Consumer<T> dataConsumer) {
this.expectedHeaders = expectedHeaders;
this.templateTypeName = templateTypeName;
this.dataConsumer = dataConsumer;
}
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
Map<Integer, String> convertHeadMap = ConverterUtils.convertToStringMap(headMap, context);
// 保存实际表头,供 invoke 方法中的兜底校验使用
if (!headerValidated) {
if (ObjectUtil.isEmpty(convertHeadMap)) {
throw new BusinessException("Excel文件表头为空");
}
validateHeaders(convertHeadMap, expectedHeaders, templateTypeName);
headerValidated = true;
}
}
@Override
public void invoke(T data, AnalysisContext context) {
// 双重保障:确保在处理第一行数据前表头已校验
if (!headerValidated) {
// 通过readRowHolder获取当前行索引,第一行数据(除去表头)的索引为1
Integer currentRowIndex = context.readRowHolder().getRowIndex();
if (currentRowIndex != null && currentRowIndex == 1) {
// 从data的类定义中获取期望表头
Map<Integer, String> expectedHeadersFromClass = ExcelHeaderValidator.getFieldValues(data);
// 获取实际表头进行校验
if (ObjectUtil.isEmpty(expectedHeadersFromClass)) {
throw new BusinessException("Excel文件表头为空");
}
validateHeaders(expectedHeadersFromClass, expectedHeaders, templateTypeName);
headerValidated = true;
return;
}
}
dataConsumer.accept(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
/**
* 校验表头是否匹配
*
* @param actualHeaders 实际表头
* @param expectedHeaders 期望表头
* @param templateTypeName 模板类型名称
* @throws IllegalArgumentException 表头不匹配时抛出异常
*/
private static void validateHeaders(Map<Integer, String> actualHeaders,
Map<Integer, String> expectedHeaders,
String templateTypeName) {
List<String> errors = new ArrayList<>();
// 检查缺少的列
for (Map.Entry<Integer, String> entry : expectedHeaders.entrySet()) {
String actualHeader = actualHeaders.get(entry.getKey());
if (actualHeader == null) {
errors.add(String.format("缺少第%d列【%s】", entry.getKey() + 1, entry.getValue()));
} else if (!actualHeader.equals(entry.getValue())) {
errors.add(String.format("第%d列应为【%s】,实际为【%s】",
entry.getKey() + 1, entry.getValue(), actualHeader));
}
}
// 检查多余的列
for (Map.Entry<Integer, String> entry : actualHeaders.entrySet()) {
if ("异常报文".equals(entry.getValue())){
continue;
}
String expectedHeader = expectedHeaders.get(entry.getKey());
if (expectedHeader == null) {
errors.add(String.format("存在多余的第%d列【%s】", entry.getKey() + 1, entry.getValue()));
}
}
if (!errors.isEmpty()) {
String errorMsg = String.format("%s模板不正确:%s", templateTypeName, String.join(";", errors));
log.error(errorMsg);
throw new BusinessException(String.format("%s模板不正确!", templateTypeName));
}
}
}

浙公网安备 33010602011771号