概要
传统的POI操作映射数据代码冗余,本文介绍一种基于自定义注解的解决方案,通过反射机制实现Excel行数据与Java对象的自动映射
核心设计
1、自定义注解 @ExcelColumn
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
/** 列索引(从0开始) */
int index();
/** 列名(可选,用于文档) */
String name() default "";
/** 是否必填 */
boolean required() default false;
/** 默认值 */
String defaultValue() default "";
}
功能特点:
- index:精确指定Excel列位置
- required:支持必填字段校验
- defaultValue:为空时提供默认值
2、 实体类字段映射示例
public class UserImportDTO {
@ExcelColumn(index = 0, name = "用户名", required = true)
private String username;
@ExcelColumn(index = 1, name = "年龄", defaultValue = "18")
private String age;
@ExcelColumn(index = 2, name = "邮箱")
private String email;
// Getter/Setter方法
// 可选:行号字段
private int rowNum;
public void setRowNum(int rowNum) { this.rowNum = rowNum; }
}
3、反射缓存优化 - ExcelFieldCache(可选)
核心价值: 避免重复反射操作,提升性能
public class ExcelFieldCache {
private static final Map<Class<?>, List<FieldMappingInfo>> CACHE = new ConcurrentHashMap<>();
@Data
public static class FieldMappingInfo {
private final Field field;
private final ExcelColumn annotation;
private final Method setter;
public FieldMappingInfo(Field field, ExcelColumn annotation, Method setter) {
this.field = field;
this.annotation = annotation;
this.setter = setter;
}
}
/**
* 获取类的字段映射信息(缓存)
*/
public static List<FieldMappingInfo> getFieldMappings(Class<?> clazz) {
return CACHE.computeIfAbsent(clazz, ExcelFieldCache::buildFieldMappings);
}
/**
* 构建字段映射信息
*/
private static List<FieldMappingInfo> buildFieldMappings(Class<?> clazz) {
List<FieldMappingInfo> mappings = new ArrayList<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
if (annotation != null) {
Method setter = getSetterMethod(clazz, field);
if (setter != null) {
mappings.add(new FieldMappingInfo(field, annotation, setter));
}
}
}
log.debug("为类 {} 构建了 {} 个字段映射", clazz.getSimpleName(), mappings.size());
return mappings;
}
private static Method getSetterMethod(Class<?> clazz, Field field) {
String setterName = "set" + capitalize(field.getName());
try {
return clazz.getMethod(setterName, field.getType());
} catch (NoSuchMethodException e) {
log.warn("未找到setter方法: {} for field: {}", setterName, field.getName());
return null;
}
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}
4、映射工具类 - ExcelAnnotationUtil
/**
* @ClassName: ExcelAnnotationUtil
* @Date: 2025/11/12 上午9:53
* @Author: mashengbo
* @Description: Excel工具类 - 基于注解的映射功能
* @Version: 1.0
*/
@Slf4j
public class ExcelAnnotationUtil {
private static final DataFormatter DATA_FORMATTER = new DataFormatter(Locale.CHINA);
/**
* 私有构造函数,防止实例化
*/
private ExcelAnnotationUtil() {
}
// ========== 单行映射方法 ==========
/**
* 将Excel行通过注解映射到对象(不设置行号)
*/
public static <T> T mapRowToObject(Row row, Class<T> clazz) {
return mapRowToObject(row, clazz, -1, false);
}
/**
* 将Excel行通过注解映射到对象(设置行号)
*/
public static <T> T mapRowToObject(Row row, Class<T> clazz, int rowNum) {
return mapRowToObject(row, clazz, rowNum, true);
}
/**
* 将Excel行通过注解映射到对象
*/
public static <T> T mapRowToObject(Row row, Class<T> clazz, int rowNum, boolean setRowNum) {
try {
T obj = clazz.getDeclaredConstructor().newInstance();
// 设置行号(可选)
if (setRowNum && rowNum >= 0) {
setRowNumber(obj, rowNum);
}
// 获取缓存的字段映射信息
List<ExcelFieldCache.FieldMappingInfo> mappings = ExcelFieldCache.getFieldMappings(clazz);
// 映射字段
for (ExcelFieldCache.FieldMappingInfo mapping : mappings) {
mapFieldWithCachedInfo(obj, mapping, row);
}
return obj;
} catch (Exception e) {
log.error("Excel行映射到对象失败,行号: {}", rowNum, e);
throw new BusinessException("Excel数据映射失败");
}
}
// ========== 批量映射方法 ==========
/**
* 批量映射Excel行到对象列表(不设置行号)
*
* @param rows Excel行列表
* @param clazz 对象类型
* @param isValidNull 是否忽略空行(空行:所有单元格都为空)
*/
public static <T> List<T> mapRowsToObjects(List<Row> rows, Class<T> clazz, boolean isValidNull) {
return mapRowsToObjects(rows, clazz, isValidNull, -1, false);
}
/**
* 批量映射Excel行到对象列表(设置行号)
*
* @param rows Excel行列表
* @param clazz 对象类型
* @param isValidNull 是否忽略空行(空行:所有单元格都为空)
* @param startRowNum 起始行号(从0开始)
*/
public static <T> List<T> mapRowsToObjects(List<Row> rows, Class<T> clazz, boolean isValidNull, int startRowNum) {
return mapRowsToObjects(rows, clazz, isValidNull, startRowNum, true);
}
/**
* 批量映射Excel行到对象列表(缓存)
*/
public static <T> List<T> mapRowsToObjects(List<Row> rows, Class<T> clazz, boolean isValidNull, int startRowNum, boolean setRowNum) {
if (CollectionUtils.isEmpty(rows)) {
return Collections.emptyList();
}
List<T> result = new ArrayList<>(rows.size());
try {
// 预获取所有反射信息(只执行一次)
List<ExcelFieldCache.FieldMappingInfo> mappings = ExcelFieldCache.getFieldMappings(clazz);
// 批量处理所有行
for (int i = 0; i < rows.size(); i++) {
Row row = rows.get(i);
if (isValidNull && (row == null || isEmptyRow(row))) {
continue;
}
T obj = clazz.getDeclaredConstructor().newInstance();
// 设置行号(可选)
if (setRowNum && startRowNum >= 0) {
setRowNumber(obj, startRowNum + i);
}
// 映射字段(使用缓存的反射信息)
for (ExcelFieldCache.FieldMappingInfo mapping : mappings) {
mapFieldWithCachedInfo(obj, mapping, row);
}
result.add(obj);
}
return result;
} catch (Exception e) {
log.error("批量映射Excel行失败", e);
throw new BusinessException("Excel批量映射失败");
}
}
/**
* 映射字段(使用缓存的反射信息)
*/
private static <T> void mapFieldWithCachedInfo(T obj, ExcelFieldCache.FieldMappingInfo mapping, Row row) {
try {
ExcelColumn annotation = mapping.getAnnotation();
Cell cell = row.getCell(annotation.index());
String cellValue = getCellValue(cell, annotation);
// 必填字段校验
if (annotation.required() && (cellValue == null || cellValue.trim().isEmpty())) {
throw new BusinessException("必填字段为空: " + annotation.name() + "(列" + (annotation.index() + 1) + ")");
}
// 使用默认值
if (cellValue == null || cellValue.trim().isEmpty()) {
cellValue = annotation.defaultValue();
}
// 调用setter方法设置值
mapping.getSetter().invoke(obj, cellValue);
} catch (Exception e) {
log.error("映射字段失败: {}(列{})", mapping.getAnnotation().name(), mapping.getAnnotation().index() + 1, e);
throw new BusinessException("字段映射失败");
}
}
/**
* 设置行号
*/
private static <T> void setRowNumber(T obj, int rowNum) {
try {
Method setRowNum = obj.getClass().getMethod("setRowNum", int.class);
setRowNum.invoke(obj, rowNum);
} catch (Exception e) {
log.debug("设置行号失败,对象可能没有rowNum字段");
}
}
/**
* 获取单元格值
*/
private static String getCellValue(Cell cell, ExcelColumn annotation) {
if (cell == null) {
return null;
}
try {
return DATA_FORMATTER.formatCellValue(cell).trim();
} catch (Exception e) {
log.warn("解析单元格值失败,列: {}", annotation.name(), e);
return null;
}
}
/**
* 检查是否为空行
*/
public static boolean isEmptyRow(Row row) {
if (row == null) {
return true;
}
for (Cell cell : row) {
if (cell != null) {
String value = DATA_FORMATTER.formatCellValue(cell).trim();
if (!value.isEmpty()) {
return false;
}
}
}
return true;
}
}
826

被折叠的 条评论
为什么被折叠?



