怎样用EasyExcel导出更多代码?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 在处理大量数据导出时遇到Java OutOfMemoryError(OOM)。最初使用公司内部工具直接查询全量数据写入Excel,导致OOM。改用阿里EasyExcel后,虽偶发OOM,但问题依旧存在。为解决此问题,采用了分页查询并分批次写入Excel的方法,有效避免了OOM。为简化此过程,封装了一个EasyExcelExport抽象类,包含分批次导出和不分批次导出的方法。使用时需实现getData()和convertSourceData2ExportEntity()方法。通过示例展示了如何利用这个工具类进行分批导出,避免了内存溢出,并减少了重复代码。

前段时间在做一个导出的功能,本以为是平平无奇的一个功能。就用公司内部的一个导出工具类三下五除二就写完了,做法是直接查全量数据,然后直接往Excel里写。一开始没多少数据也没什么问题,但是当数据量逐渐多了起来后,达到一万多条,导出的时候就会报OOM。然后我就换成了阿里开源的EasyExcel,但是导出的时候也不太稳定,偶尔也会OOM。所以应该是数据量太大了,在写入的时候把内存占满了。然后我就放弃了查全量数据一次性写入Excel的做法,采用分页查询,分批次写入Excel的方式,果然不会出现OOM了。
虽然这种方式不会出现OOM,但是每次导出都写一遍重复的代码着实有点麻烦,所以结合自己平时的使用场景,封装了一个EasyExcel的导出工具类,这样只要在分页查询的基础上写少量的代码,就可以实现分批次写入Excel,简化代码的编写并且解决OOM的问题。
实现
java复制代码@Slf4j
public abstract class EasyExcelExport {

/**
 * EasyExcel导出Excel表格,每个sheet默认最大10万条数据
 *
 * @param fileName  excel文件前缀名
 * @param sheetName 表页名
 */
public void easyExcelBatchExport(String fileName, String sheetName, HttpServletResponse response) {
    this.easyExcelBatchExport(fileName, sheetName, 100000, response);
}

/**
 * 分批次导出excel数据
 *
 * @param fileName  excel文件前缀名
 * @param sheetSize 每个sheet的数据量,默认10万,excel有限制不能大于1048576
 * @param sheetName 表页名
 */
public void easyExcelBatchExport(String fileName, String sheetName, Integer sheetSize, HttpServletResponse response) {
    fileName = fileName + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
    int currentSheet = 1;   // 当前处于第几个sheet
    int totalLine = 0;      // 总共写入的条数
    int currentBatch = 1;   // 当前写入excel的批次(第几页)
    int lineNum = 1;        // 行号,当前写入的是第几条数据

    long startTime = System.currentTimeMillis();
    try {
        response.setCharacterEncoding("utf-8");
        // 告诉浏览器用什么软件可以打开此文件
        response.setHeader("content-Type", "application/vnd.ms-excel");
        // 下载文件的默认名称
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));

        ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]).build();
        WriteSheet sheet = EasyExcel.writerSheet(sheetName).build();

        while (true) {
            // 获取数据,然后currentBatch+1,下次调用就会获取新的数据
            List<S> sourceDataList = getData(currentBatch);
            currentBatch++;

            List<T> exportEntityList = new ArrayList<>();
            if (CollUtil.isNotEmpty(sourceDataList)) {
                totalLine += sourceDataList.size();
                log.info("EasyExcel开始写入第{}批数据,当前批次数据大小为{}", currentBatch - 1, sourceDataList.size());
                for (S sourceData : sourceDataList) {
                    exportEntityList.add(convertSourceData2ExportEntity(sourceData, lineNum));
                    lineNum++;

                    // 当前sheet数据已经到达最大值,将当前数据全写入当前sheet,下一条数据就会写入新sheet
                    if (lineNum > sheetSize) {
                        excelWriter.write(exportEntityList, sheet);
                        exportEntityList.clear();
                        lineNum = 1;
                        currentSheet++;
                        sheet = EasyExcel.writerSheet(sheetName + currentSheet).build();
                    }
                }

                // 写入excel
                excelWriter.write(exportEntityList, sheet);
            } else {
                // 未获取到数据,结束
                break;
            }
        }
        excelWriter.finish();
    } catch (Exception e) {
        log.error("EasyExcel导出异常", e);
    }

    log.info("EasyExcel导出数据结束,总数据量为{},耗时{}ms", totalLine, (System.currentTimeMillis() - startTime));
}

/**
 * 不分批次导出excel。一次性获取所有数据写入excel,确定数据量不大时可以使用该方法,数据量过大时使用分批次导出,否则会OOM
 *
 * @param fileName  excel文件前缀名
 * @param sheetName 表页名
 */
public void easyExcelExport(String fileName, String sheetName, HttpServletResponse response) {
    fileName = fileName + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
    int totalLine = 0;      // 总共写入的条数
    int lineNum = 1;        // 行号,当前写入的是第几条数据

    long startTime = System.currentTimeMillis();
    try {
        response.setCharacterEncoding("utf-8");
        // 告诉浏览器用什么软件可以打开此文件
        response.setHeader("content-Type", "application/vnd.ms-excel");
        // 下载文件的默认名称
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));

        List<S> sourceDataList = getData(1);
        List<T> exportEntityList = new ArrayList<>();
        if (CollUtil.isNotEmpty(sourceDataList)) {
            totalLine += sourceDataList.size();
            log.info("EasyExcel开始写入数据,数据大小为{}", sourceDataList.size());
            for (S sourceData : sourceDataList) {
                exportEntityList.add(convertSourceData2ExportEntity(sourceData, lineNum));
                lineNum++;
            }
        }
        response.setCharacterEncoding("utf-8");
        // 告诉浏览器用什么软件可以打开此文件
        response.setHeader("content-Type", "application/vnd.ms-excel");
        // 下载文件的默认名称
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));
        EasyExcel.write(response.getOutputStream(), (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]).sheet(sheetName).doWrite(exportEntityList);
    } catch (Exception e) {
        log.error("EasyExcel导出异常", e);
    }

    log.info("EasyExcel导出数据结束,总数据量为{},耗时{}ms", totalLine, (System.currentTimeMillis() - startTime));
}

/**
 * 将原数据对象转换为需要导出的目标对象
 *
 * @param sourceData 原对象
 * @param lineNum    行号
 */
public abstract T convertSourceData2ExportEntity(S sourceData, Integer lineNum);

/**
 * 获取原始数据,通过currentBatch参数分页获取数据。
 *
 * @param currentBatch 获取第几批(页)数据,通过该参数分页查询,每次调用自动递增。不分批次导出时可以忽略该参数
 */
public abstract List<S> getData(int currentBatch);

}

首先,这是EasyExcelExport是一个抽象类,指定了泛型 T 和 S,T是target目标类,也就是导出时对应的类,S是source原对象所对应的类。
EasyExcelExport里还有两个抽象方法,getData() 和 convertSourceData2ExportEntity() 。这两个方法是需要在平时使用时自己去实现的,getData是数据查询的方法,可以在这里面去实现分页查询的逻辑,currentBatch参数是用来控制分页查询页码的,从1开始,会自动递增。如果确定数据量不大不需要分批次导出的话,那么getData()里只需要进行普通的查询即可,忽略currentBatch参数不用分页查询。还有一个方法是convertSourceData2ExportEntity(),这个是用来将对象S转为对象T的方法,因为从数据库查询或者是从其他地方获取到的对象类型可能是S,而导出时需要的对象类型是T,所以通过该方法进行对象转换。
最核心的是 easyExcelBatchExport() 方法,里面有一个while循环,while循环里首先会去调用getData()方法获取数据,然后将currentBatch加1便于下次获取数据,接下来有个for循环去进行对象的转换并添加到exportEntityList集合中,这个集合中装的是最终写到Excel里的对象。当转换完成后就将当前批次的数据写入Excel中,然后进行下一次循环,当getData()方法未获取到数据时,就结束循环。
同时支持指定每个sheet页的最大行数。在对对象进行转换时有一个判断,当前sheet页的数据是否到达指定值,到达后,直接写入excel,然后新建一个sheet页,这样新的数据就会写入新的sheet页。

相关文章
|
9月前
|
数据库连接 数据处理 数据库
怎么才算精通Excel呢?是能够编译出多层复杂的公式?还是用VBA代码实现Excel缺失的功能?当Excel能够实现数据报表看板、当Excel能够制作有趣的漫画,有没有启发你的代码编写创意?你都见过哪些真正“精通Excel”的操作?
怎么才算精通Excel呢?是能够编译出多层复杂的公式?还是用VBA代码实现Excel缺失的功能?当Excel能够实现数据报表看板、当Excel能够制作有趣的漫画,有没有启发你的代码编写创意?你都见过哪些真正“精通Excel”的操作?
|
18天前
|
easyexcel
EasyExcel导出工具类
EasyExcel导出工具类
19 0
|
6月前
|
小程序 Shell PHP
laravel5.8(二十三)导出PDF
有需求需要使用PHP导出pdf。下面记录一下我使用的两种方式 一:laravel-tcpdf 导出PDF文件Laravel框架为我们集成了一个插件tcpdf。 下载地址: github.com/elibyy/tcpd… 然后使用composer进行安装就可以了。 具体安装过程,请移步《laravel5.8(十)引入第三方类库》 使用的时候记得use 一下 命名空间。 但是这里有一个问题,使用这个插件导出文件无法使用中文,且我还没有找到解决办法,因此,这个laravel的tcpdf插件我就没有使用。 二:tcpdf tcpdf官方网站: tcpdf.org/ 我下载了完整版的TCPDF 下载地址
55 0
|
10月前
|
存储 Java 数据挖掘
探索EasyPoi库:简化Excel操作的神器
在企业应用和数据处理中,Excel表格是常用的数据交换和存储方式。然而,处理和操作Excel表格可能是一项繁琐的任务。EasyPoi库作为一款优秀的Excel操作工具,可以帮助我们更轻松地进行Excel文件的读写、导入导出等操作。本文将深入探讨EasyPoi库的基本概念、特点,以及如何在实际应用中使用它进行Excel操作。
192 0
|
消息中间件 JavaScript 小程序
ngBoot 我随手封装了一个万能的导出excel工具,传什么都能导出 下
ngBoot 我随手封装了一个万能的导出excel工具,传什么都能导出 下
|
JavaScript 小程序 Java
ngBoot 我随手封装了一个万能的导出excel工具,传什么都能导出 上
ngBoot 我随手封装了一个万能的导出excel工具,传什么都能导出 上
|
数据可视化 Python
VBA最常用的基础代码、基础功能写法总结
VBA最常用的基础代码、基础功能写法总结
113 0
|
JSON JavaScript 前端开发
给VSCode定义代码片段,让coding速度快到飞起~
代码片段可以理解为模板,当我们输入指定时,按下【tab】或者【enter】即可出现对应的模板。 只要代码片段写的好,升职加薪少不了~ 代码片段的好处与坏处 coder对代码片段的评价褒贬不一
139 0
|
JSON JavaScript 前端开发
python接口自动化(十七)--Json 数据处理---一次爬坑记(详解)
有些 post 的请求参数是 json 格式的,这个前面发送post 请求里面提到过,需要导入 json模块处理。现在企业公司一般常见的接口因为json数据容易处理,所以绝大多数返回数据也是 json 格式的,我们在做判断时候,往往只需要提取其中 几个关键的参数就行,这时候我们就需要 json 来解析返回的数据了。首先来说一下笔者为何要单独写这么一篇,原因是:python 里面 bool 值是 True 和 False,json 里面 bool 值是 true和 false,并且区分大小写,这就尴尬了,明明都是 bool 值。
211 1
python接口自动化(十七)--Json 数据处理---一次爬坑记(详解)