[笔记] SpringBoot3 使用 EasyExcel 封装工具类实现复杂 Excel 数据处理:使用Java构建高效的数据导入解决方案

在当今数据驱动的世界中,有效地管理和处理大量数据已成为企业成功的关键因素之一。特别是对于那些依赖于Excel文件进行数据交换和报告的企业来说,能够快速准确地将Excel数据转换为可操作的信息变得尤为重要。然而,传统的数据导入方法往往面临着性能瓶颈、格式不兼容以及缺乏灵活性等问题。本文介绍了一种基于Java的解决方案,它利用了阿里巴巴开源的EasyExcel库来实现高效且灵活的Excel数据导入功能。我们将探讨如何通过自定义监听器和工具类来处理复杂的Excel表结构,并展示如何轻松应对各种数据验证需求。

期间有些数据转换之类的方法,参考 :
[笔记] SpringBoot3 使用 EasyExcel 封装工具类实现,自定义表头导出,并实现数据格式化转换与添加下拉框操作. - 掘金

编写的逻辑是在狮子大佬的开源框架 Ruoyi-Plus 基础上丰富的内容

Ruoyi-Plus 地址 : Ruoyi-Vue-Plus


一. 特殊 Excel 表格例子


二. 使用示例

1. bo 示例(部分字段)

bo 和 vo 注解一样

2. 自定义监听器

3. 使用方法


三. 实现

1. 工具类

java 复制代码
/**
 * Excel相关处理
 *
 * @author 鲁子狄
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ExcelUtil {

    /**
     * 同步导入(适用于小数据量)
     *
     * @param is 输入流
     * @return 转换后集合
     */
    public static <T> List<T> importExcel(InputStream is, Class<T> clazz) {
        return EasyExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync();
    }


    /**
     * 使用校验监听器 异步导入 同步返回
     *
     * @param is         输入流
     * @param clazz      对象类型
     * @param isValidate 是否 Validator 检验 默认为是
     * @return 转换后集合
     */
    public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {
        DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);
        EasyExcel.read(is, clazz, listener).sheet().doRead();
        return listener.getExcelResult();
    }

    /**
     * 使用自定义监听器 异步导入 自定义返回
     *
     * @param is       输入流
     * @param clazz    对象类型
     * @param listener 自定义监听器
     * @return 转换后集合
     */
    public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
        EasyExcel.read(is, clazz, listener).sheet().doRead();
        return listener.getExcelResult();
    }

    /**
     * 使用自定义监听器 异步导入 自定义返回
     *
     * @param is          输入流
     * @param clazz       对象类型
     * @param targetCells 获取表头对应的单元格
     * @param headRowNum  表头行
     * @param listener    自定义监听器
     * @return 转换后集合
     */
    public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, Set<CellPosition> targetCells, Integer headRowNum, ExcelListener<T> listener) {
        byte[] inputStreamBytes;
        try {
            inputStreamBytes = IOUtils.toByteArray(is);
        } catch (IOException e) {
            throw new RuntimeException("读取输入流异常", e);
        }

        if (targetCells != null) {
            // 同步读取预表头数据
            PreHeaderListener preHeaderListener = new PreHeaderListener(targetCells);
            // 注册监听器前先设置读取模式为无模型读取
            try (InputStream preHeaderInputStream = new ByteArrayInputStream(inputStreamBytes)) {
                EasyExcel.read(preHeaderInputStream)
                    .sheet()
                    .headRowNumber(0)
                    .registerReadListener(preHeaderListener)
                    .doReadSync();
            } catch (IOException e) {
                throw new RuntimeException("读取预表头数据异常", e);
            }
            // 获取预表头数据
            Map<String, String> preHeaderData = preHeaderListener.getPreHeaderData();
            listener.setPreHeaderData(preHeaderData);
        }

        // 使用新的输入流重新读取文件,这次是为了读取实际数据
        try (InputStream inputStream = new ByteArrayInputStream(inputStreamBytes)) {
            EasyExcel.read(inputStream)
                .sheet()
                .head(clazz)
                .headRowNumber(headRowNum)
                .registerReadListener(listener)
                .doRead();
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常", e);
        }

        return listener.getExcelResult();
    }
}

2. 默认基础导入(特殊 Excel 建立在这个基础上)

特殊 Excel 建立在这个基础上,这块的代码也要有

2.1 Excel 导入监听

java 复制代码
public interface ExcelListener<T> extends ReadListener<T> {

    ExcelResult<T> getExcelResult();

    /**
     * 设置预表头数据
     *
     * @param preHeaderData 预表头数据
     */
    void setPreHeaderData(Map<String, String> preHeaderData);
}

2.2 默认监听

java 复制代码
@Slf4j
@NoArgsConstructor
public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {

    /**
     * 是否Validator检验,默认为是
     */
    private Boolean isValidate = Boolean.TRUE;

    /**
     * excel 表头数据
     */
    private Map<Integer, String> headMap;

    /**
     * 导入回执
     */
    private ExcelResult<T> excelResult;

    public DefaultExcelListener(boolean isValidate) {
        this.excelResult = new DefaultExcelResult<>();
        this.isValidate = isValidate;
    }

    /**
     * 处理异常
     *
     * @param exception ExcelDataConvertException
     * @param context   Excel 上下文
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        String errMsg = null;
        if (exception instanceof ExcelDataConvertException excelDataConvertException) {
            // 如果是某一个单元格的转换异常 能获取到具体行号
            Integer rowIndex = excelDataConvertException.getRowIndex();
            Integer columnIndex = excelDataConvertException.getColumnIndex();
            errMsg = StrUtil.format("第{}行-第{}列-表头{}: 解析异常<br/>",
                rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
            if (log.isDebugEnabled()) {
                log.error(errMsg);
            }
        }
        if (exception instanceof ConstraintViolationException constraintViolationException) {
            Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
            String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
            errMsg = StrUtil.format("第{}行数据校验异常: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
            if (log.isDebugEnabled()) {
                log.error(errMsg);
            }
        }
        excelResult.getErrorList().add(errMsg);
        throw new ExcelAnalysisException(errMsg);
    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        this.headMap = headMap;
        log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        if (isValidate) {
            ValidatorUtils.validate(data);
        }
        excelResult.getList().add(data);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.debug("所有数据解析完成!");
    }

    @Override
    public ExcelResult<T> getExcelResult() {
        return excelResult;
    }

    @Override
    public void setPreHeaderData(Map<String, String> preHeaderData) {

    }

2.3 Excel 返回对象

java 复制代码
public interface ExcelResult<T> {

    /**
     * 对象列表
     */
    List<T> getList();

    /**
     * 错误列表
     */
    List<String> getErrorList();

    /**
     * 导入回执
     */
    String getAnalysis();
}

2.3 默认excel返回对象实现

java 复制代码
public class DefaultExcelResult<T> implements ExcelResult<T> {

    /**
     * 数据对象list
     */
    @Setter
    private List<T> list;

    /**
     * 错误信息列表
     */
    @Setter
    private List<String> errorList;

    public DefaultExcelResult() {
        this.list = new ArrayList<>();
        this.errorList = new ArrayList<>();
    }

    public DefaultExcelResult(List<T> list, List<String> errorList) {
        this.list = list;
        this.errorList = errorList;
    }

    public DefaultExcelResult(ExcelResult<T> excelResult) {
        this.list = excelResult.getList();
        this.errorList = excelResult.getErrorList();
    }

    @Override
    public List<T> getList() {
        return list;
    }

    @Override
    public List<String> getErrorList() {
        return errorList;
    }

    /**
     * 获取导入回执
     *
     * @return 导入回执
     */
    @Override
    public String getAnalysis() {
        int successCount = list.size();
        int errorCount = errorList.size();
        if (successCount == 0) {
            return "读取失败,未解析到数据";
        } else {
            if (errorCount == 0) {
                return StrUtil.format("恭喜您,全部读取成功!共{}条", successCount);
            } else {
                return "";
            }
        }
    }
}

3. 特殊 Excel

3.1 单元格位置类

java 复制代码
/**
 * 单元格位置类
 * 用于表示Excel中的单元格位置
 *
 * @author 鲁子狄
 * @since 2025/03/19 11:30
 **/
@Data
public class CellPosition {

    /**
     * 行索引
     */
    private final int row;

    /**
     * 列索引
     */
    private final int column;


    /**
     * 构造函数
     * 将列字母转换为列索引
     *
     * @param row          行索引
     * @param columnLetter 列字母
     */
    public CellPosition(int row, String columnLetter) {
        this.row = row - 1;
        column = getColumnIndex(columnLetter);
    }

    /**
     * 将列字母转换为列索引
     *
     * @param columnLetter 列字母
     * @return 列索引
     */
    private static int getColumnIndex(String columnLetter) {
        int columnIndex = 0;
        for (int i = 0; i < columnLetter.length(); i++) {
            columnIndex = columnIndex * 26 + (columnLetter.charAt(i) - 'A' + 1);
        }
        return columnIndex - 1;
    }
}

3.2 预表头监听器

java 复制代码
/**
 * 预表头监听器
 * 用于在读取Excel文件时处理预表头数据
 *
 * @author 鲁子狄
 * @since 2025/03/18 14:55
 **/
@Slf4j
@Getter
public class PreHeaderListener extends AnalysisEventListener<Map<Integer, String>> {

    /**
     * 存储目标单元格的位置
     */
    private final Set<CellPosition> targetCells;
    /**
     * 存储预表头数据
     */
    private final Map<String, String> preHeaderData = new HashMap<>();

    /**
     * 构造函数
     *
     * @param targetCells 目标单元格的位置集合
     */
    public PreHeaderListener(Set<CellPosition> targetCells) {
        this.targetCells = targetCells;
    }

    /**
     * 将列索引转换为列字母
     *
     * @param columnIndex 列索引
     * @return 列字母
     */
    private static String getColumnLetter(int columnIndex) {
        StringBuilder columnLetter = new StringBuilder();
        while (columnIndex >= 0) {
            columnLetter.insert(0, (char) ('A' + columnIndex % 26));
            columnIndex = columnIndex / 26 - 1;
        }
        return columnLetter.toString();
    }

    /**
     * 处理每一行数据
     *
     * @param map     当前行的数据
     * @param context 分析上下文
     */
    @Override
    public void invoke(Map<Integer, String> map, AnalysisContext context) {
        int rowIndex = context.readRowHolder().getRowIndex();
        for (CellPosition position : targetCells) {
            if (position.getRow() == rowIndex) {
                int columnIndex = position.getColumn();
                if (map.containsKey(columnIndex)) {
                    String cellValue = map.get(columnIndex);
                    preHeaderData.put("行" + (rowIndex + 1) + "列" + getColumnLetter(columnIndex), cellValue);
                }
            }
        }
    }

    /**
     * 所有数据解析完成后调用
     *
     * @param analysisContext 分析上下文
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
    }
}
相关推荐
想躺平的咸鱼干5 分钟前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying32 分钟前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·40 分钟前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
martinzh2 小时前
Spring AI 项目介绍
后端
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
前端付豪2 小时前
20、用 Python + API 打造终端天气预报工具(支持城市查询、天气图标、美化输出🧊
后端·python
爱学习的小学渣2 小时前
关系型数据库
后端
武子康2 小时前
大数据-33 HBase 整体架构 HMaster HRegion
大数据·后端·hbase
前端付豪2 小时前
19、用 Python + OpenAI 构建一个命令行 AI 问答助手
后端·python