基于Java POI与自定义注解的Excel与实体类映射方案

概要

传统的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;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值