校验监听器
public class AnalysisValidReadListener<T> extends AnalysisEventListener<T> {private static final Logger logger = LoggerFactory.getLogger(AnalysisValidReadListener.class);private static final int BATCH_COUNT = 1;private final List<T> dataList = new ArrayList<>(BATCH_COUNT);private final Class<T> type; private final int headRowNum; public AnalysisValidReadListener(Class<T> type, int headRowNum) {this.type = Objects.requireNonNull(type, "Type parameter cannot be null");this.headRowNum = headRowNum;}@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {int currentRow = context.readRowHolder().getRowIndex();if (currentRow == headRowNum - 1) { logger.info("解析到表头数据: {}", JSON.toJSONString(headMap));validateHeader(headMap);}}@Overridepublic void invoke(T data, AnalysisContext context) {int rowNumber = context.readRowHolder().getRowIndex() + 1;try {validateObject(data, rowNumber);dataList.add(data);} catch (RuntimeException e) {throw new RuntimeException("数据校验失败:" + e.getMessage());}}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {logger.info("所有数据解析完成,共 {} 行", dataList.size());}@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {if (exception instanceof ExcelDataConvertException) {ExcelDataConvertException ex = (ExcelDataConvertException) exception;String columnLetter = columnIndexToLetter(ex.getColumnIndex());throw new RuntimeException(String.format("Excel解析错误:第 %d 行, 第 %s 列数据转换异常。", ex.getRowIndex() + 1, columnLetter));}throw exception;}private void validateHeader(Map<Integer, String> headMap) {Map<Integer, String> expectedIndexHeaderMap = new HashMap<>(); Set<String> expectedNameHeaders = new HashSet<>(); for (Field field : type.getDeclaredFields()) {ExcelProperty fieldAnnotation = field.getAnnotation(ExcelProperty.class);if (fieldAnnotation != null) {String expectedHead = fieldAnnotation.value()[0];int index = fieldAnnotation.index();if (index >= 0) {expectedIndexHeaderMap.put(index, expectedHead);} else {expectedNameHeaders.add(expectedHead);}}}if (headMap.size() < expectedIndexHeaderMap.size() + expectedNameHeaders.size()) {throw new RuntimeException("导入模板错误,表头列数不匹配");}for (Map.Entry<Integer, String> entry : expectedIndexHeaderMap.entrySet()) {int expectedIndex = entry.getKey();String expectedHead = entry.getValue();if (!Objects.equals(headMap.get(expectedIndex), expectedHead)) {throw new RuntimeException(String.format("表头错误,第 %d 列【%s】应为【%s】", expectedIndex + 1, headMap.get(expectedIndex), expectedHead));}}for (String expectedHead : expectedNameHeaders) {if (!headMap.containsValue(expectedHead)) {throw new RuntimeException(String.format("表头错误,缺少列名【%s】", expectedHead));}}}public static <T> void validateObject(T obj, int rowNumber) {Class<?> clazz = obj.getClass();Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {field.setAccessible(true);Object fieldValue = null;try {fieldValue = field.get(obj);} catch (IllegalAccessException e) {throw new RuntimeException("无法访问字段: " + field.getName(), e);}ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);String fieldName = (excelProperty != null) ? String.join(", ", excelProperty.value()) : field.getName();ExcelValid excelValid = field.getAnnotation(ExcelValid.class);if (excelValid == null) {continue; }if (excelValid.require() && Objects.isNull(fieldValue)) {throw new RuntimeException(String.format("第%d行,%s 数据不能为空!", rowNumber, fieldName));}if (fieldValue instanceof Number) {long numValue = ((Number) fieldValue).longValue();if (numValue < excelValid.min() || numValue > excelValid.max()) {throw new RuntimeException(String.format("第%d行,%s 数据必须在 %d 和 %d 之间!",rowNumber, fieldName, excelValid.min(), excelValid.max()));}}if (!excelValid.pattern().isEmpty() && fieldValue instanceof String) {String regex = excelValid.pattern();String value = (String) fieldValue;if (!value.matches(regex)) {throw new RuntimeException(String.format("第%d行,%s 数据格式不正确!", rowNumber, fieldName));}}if (!excelValid.dateFormat().isEmpty()) {final String expectedFormat = excelValid.dateFormat();final DateTimeFormatter formatter;try {formatter = DateTimeFormatter.ofPattern(expectedFormat);} catch (IllegalArgumentException e) {throw new RuntimeException("无效的日期格式配置: " + expectedFormat, e);}if (fieldValue instanceof String) {String dateStr = (String) fieldValue;ParsePosition pos = new ParsePosition(0);formatter.parseUnresolved(dateStr, pos);if (pos.getErrorIndex() != -1 || pos.getIndex() != dateStr.length()) {throw new RuntimeException(String.format("第%d行,%s 数据格式不正确,应为 %s 格式!", rowNumber, fieldName, excelValid.dateFormat()));}} else {TemporalAccessor temporal = convertToTemporalAccessor(fieldValue);try {formatter.format(temporal); } catch (DateTimeException e) {throw new RuntimeException(String.format("第%d行,%s 数据格式不正确,应为 %s 格式!", rowNumber, fieldName, excelValid.dateFormat()));}}}if (excelValid.yesNo() && Objects.nonNull(fieldValue) &&!(Objects.equals(fieldValue, "是") || Objects.equals(fieldValue, "否"))) {throw new RuntimeException("第 " + rowNumber + " 行," + fieldName + " 数据只能为【是/否】!");}if (Objects.isNull(fieldValue) && !excelValid.defaultValue().isEmpty()) {try {field.set(obj, excelValid.defaultValue());} catch (IllegalAccessException e) {throw new RuntimeException(String.format("第%d行,%s 赋默认值失败!", rowNumber, fieldName));}}if (Objects.nonNull(fieldValue)) {for (String rule : excelValid.replaceValues()) {String[] parts = rule.split("="); if (parts.length == 2) {String target = parts[0].trim(); String replacement = parts[1].trim(); if (Objects.equals(fieldValue, target)) {try {field.set(obj, replacement); } catch (IllegalAccessException e) {throw new RuntimeException(String.format("第%d行,%s 替换 '%s' 失败!", rowNumber, fieldName, target));}}}}}}}public String columnIndexToLetter(int columnIndex) {StringBuilder columnName = new StringBuilder();while (columnIndex >= 0) {columnName.insert(0, (char) ('A' + columnIndex % 26)); columnIndex = columnIndex / 26 - 1; }return columnName.toString();}private static TemporalAccessor convertToTemporalAccessor(Object value) {if (value instanceof LocalDateTime) {return (LocalDateTime) value;} else if (value instanceof LocalDate) {return (LocalDate) value;} else if (value instanceof Date) {return ((Date) value).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();}throw new IllegalArgumentException("不支持的日期类型: " + value.getClass());}}
校验注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface ExcelValid {boolean require() default false; long min() default Long.MIN_VALUE; long max() default Long.MAX_VALUE; String pattern() default ""; boolean yesNo() default false; String dateFormat() default ""; String[] replaceValues() default {}; String defaultValue() default ""; }
通用日期转换器
package com.cloud.sa.base.common.convert;import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public class UniversalConverter implements Converter<Object> {private static final List<DateTimeFormatter> DATE_FORMATTERS = Arrays.asList(DateTimeFormatter.ofPattern("yyyy.MM"), DateTimeFormatter.ofPattern("yyyy-MM-dd"), DateTimeFormatter.ofPattern("yyyy-MM"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), DateTimeFormatter.ofPattern("yyyy/MM/dd"), DateTimeFormatter.ofPattern("yyyyMMdd"), DateTimeFormatter.ofPattern("yyyy年MM月dd日"), DateTimeFormatter.ofPattern("MM/dd/yyyy"), DateTimeFormatter.ofPattern("dd-MMM-yyyy", Locale.ENGLISH) );@Overridepublic Class<?> supportJavaTypeKey() {return Object.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}@Overridepublic Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {if (cellData == null) {return null;}switch (cellData.getType()) {case STRING:String value = cellData.getStringValue().trim();return parseStringToDateTime(value);case NUMBER:return convertExcelNumberToDate(cellData.getNumberValue().doubleValue());default:throw new IllegalArgumentException("无法解析的Excel单元格类型: " + cellData.getType());}}private LocalDateTime parseStringToDateTime(String value) {for (DateTimeFormatter formatter : DATE_FORMATTERS) {try {TemporalAccessor temporal = formatter.parseBest(value, LocalDateTime::from, LocalDate::from, YearMonth::from);if (temporal instanceof LocalDateTime) {return (LocalDateTime) temporal;} else if (temporal instanceof LocalDate) {return ((LocalDate) temporal).atStartOfDay();} else if (temporal instanceof YearMonth) {return ((YearMonth) temporal).atDay(1).atStartOfDay();}} catch (DateTimeParseException ignored) {}}throw new IllegalArgumentException("无法识别的日期格式: " + value);}private LocalDateTime convertExcelNumberToDate(double number) {int wholeDays = (int) Math.floor(number);int millisecondsInDay = (int) ((number - wholeDays) * 24 * 60 * 60 * 1000 + 0.5);LocalDateTime base = LocalDateTime.of(1899, 12, 31, 0, 0);LocalDateTime dateTime = base.plusDays(wholeDays).plus(millisecondsInDay, ChronoUnit.MILLIS);if (wholeDays > 59) {dateTime = dateTime.minusDays(1);}return dateTime;}
}
使用方法
int headRowNum = 2;
AnalysisValidReadListener<ProjectFileEntrustedChildrenImportForm> listener = new AnalysisValidReadListener<>(ProjectFileEntrustedChildrenImportForm.class, headRowNum);
List<ProjectFileEntrustedChildrenImportForm> dataList = EasyExcel.read(file.getInputStream(), ProjectFileEntrustedChildrenImportForm.class, listener)
.sheet()
.headRowNumber(headRowNum)
.doReadSync();
设置日期转换器
@Schema(description = "建设开始时间")
@ExcelProperty(value = "建设开始时间",converter = UniversalConverter.class)
@ExcelValid(require = true, dateFormat = "yyyy.MM")
private LocalDateTime buildStartDate;