[笔记] 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) {
    }
}
相关推荐
demonlg011210 分钟前
Go 语言标准库中strings和strconv详细功能介绍与示例
开发语言·后端·云原生·golang
Asthenia041234 分钟前
复盘博客:从面试谈 ZSet 的理解与分析
后端
郭涤生43 分钟前
Chapter 3: Programming Paradigms_《clean architecture》notes
java·开发语言·c++·笔记
Hanson Huang1 小时前
23种设计模式-模板方法(Template Method)设计模式
java·设计模式·模板方法模式·行为型设计模式
woai33641 小时前
设计模式-单例模式
java·开发语言·单例模式
始终奔跑在路上1 小时前
Spring Initializr搭建spring boot项目
java·开发语言·spring boot·spring·软件开发
寒冰碧海1 小时前
JDK 17 + Spring Boot 3 全栈升级实战指南--从语法革新到云原生,解锁企业级开发新范式
java·spring boot·云原生
程序猿chen1 小时前
第二重·纵横篇:Kubernetes御剑术与云原生护体罡气
java·git·后端·程序人生·云原生·容器·kubernetes
青云交2 小时前
Java 大视界 -- Java 大数据在智慧港口集装箱调度与物流效率提升中的应用创新(159)
java·大数据·物联网·智慧港口·大数据分析·集装箱调度·物流效率
2501_906800762 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·数学建模·web