EasyExcel 分Sheet实现大数据量导出

EasyExcel 分 Sheet 实现大数据量导出

        【场景】平台用户导出数据量达 w 级别的数据时界面白屏或按钮无响应。

        【解决方案】做异步导出,用户触发点击时创建导出消息并开启单独线程处理导出,处理成功以后将文件下载路径更新到消息中心提醒用户下载。

        【注意的问题】As we all knew, everything has its limits,Excel 也是(03 版本 Excel 单张 Sheet 容量是 65536 行 ,07 版本单张 Sheet 容量是 1048575 行),超过了容量则 api 报错。invalid row number (1048576) outside allowable range (0..1048575)

        【解决办法】分 Sheet 导出,如果导出的数据量超过单一张 Sheet 的容量就创建一个新 Sheet 进行存放

       

实现步骤如下:

引入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.10</version>
</dependency>

具体方式(参考代码)

/**
 * 异步导出学生数据
 * @param param 查询参数
 * @param loginUserId 当前登录人ID
 * @param classId 班级ID
 * @return
 */
public Result exportStudentInfoBigData(StudentInfoExportQueryParam  param, Integer loginUserId, Integer classId) {
    try {
        String title = "学生信息报表"; // 表格顶部标题
        String fileName = new String((title + String.format("%tF", new Date()) + "-导出").getBytes(), StandardCharsets.UTF_8);
        // 创建任务
        String messageTitle = title + "导出正在处理中(可能时间比较长,请及时关注消息处理进度)...";
        MessageCenter downloadMsg = new MessageCenter(loginUserId, Constants.COMMON_NUMBER_1, messageTitle, null);
        messageCenterMapper.insert(downloadMsg);

        // 输出到临时目录
        String savePath = "";
        if (Arrays.asList("prod".split(",")).contains(appConfiguration.getActive())) { // 环境配置临时文件位置
            savePath = "/mnt/export/";
        } else {
            savePath = "D:/export/";
        }
        String fileStr = fileName + ".xlsx";
        String saveFileName = savePath + fileStr;
        // 创建临时文件
        File savefile = new File(savePath);
        if (!savefile.exists()) {
            savefile.mkdirs();
        }
        FileOutputStream fos = new FileOutputStream(saveFileName);
        // 异步方式
        String bodyFormat = "您于" + DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD_HH_MM_SS) + "提交导出的【%s】已完成, 下载地址: <a style='color: blue' href='%s' download='%s'>点击下载</a>";
        // 校验是否已经存在导出中的数据
        Map<String, Object> paraMap = new HashMap<String, Object>();
        paraMap.put("userId", loginUserId);
        paraMap.put("messageType", 1);
        paraMap.put("messageTitle", title);
        paraMap.put("status", Constants.COMMON_NUMBER_0); // 未读
        List<MessageCenter> downloadMsgList = messageCenterMapper.findListByParam(paraMap);
        if (!CollectionUtils.isEmpty(downloadMsgList)) {
            throw new CustomException("您已有正在处理中的下载任务,请到消息中查看状态完成后下载文件");
        }

        param.setUserClassId(classId);
        param.setLoginUserId(loginUserId);
        // 计算总数分片导出
        // 获取最大ID
        param.setQueryType(1);
        Integer maxId = studentInfoMapper.getMaxOrMinId(param);
        // 获取最小ID
        param.setQueryType(2);
        Integer minId = studentInfoMapper.getMaxOrMinId(param);
        // 查询此时最小ID~最大ID之间的总数
        param.setMaxId(maxId);
        param.setMinId(minId);
        // 根据当前条件查询到的总数据量
        Long totalCount = studentInfoMapper.selectCountStudentInfoExcel(param);
        log.info("当前导出总数量:{}", totalCount);
        ExcelWriter excelWriter = null;
        if(totalCount > Constants.EXCEL_SHEET_CAPACITY) { // 如果总数超出单个sheet的大小 1048575
            excelWriter = EasyExcel.write(fos, StudentInfoExportExcel.class).build();
            // 计算Sheet个数: 如果超过单个sheet大小,需要分sheet,否则报错: invalid row number (1048576) outside allowable range (0..1048575)
            Integer pageSize = Constants.EXPORT_MAX_SHEET_PAGE_SIZE; // 设置的单个Sheet的数据量,必须小于1048576,在此设置Constants.EXPORT_MAX_SHEET_PAGE_SIZE=100,0000
            long sheetCount = (totalCount + Constants.EXPORT_MAX_SHEET_PAGE_SIZE - 1) / pageSize;
            for (Integer pageNum = 1; pageNum <= sheetCount; ++pageNum) {
                WriteSheet writeSheet = EasyExcel.writerSheet(pageNum, fileName + "_Sheet表_" + pageNum).head(StudentInfoExportExcel.class).build();
                PageHelper.startPage(pageNum, pageSize, false); // 分页不统计
                List<StudentInfoExportExcel> excelList = studentInfoMapper.findAndExportStudentInfoList(param);
                excelWriter.write(excelList, writeSheet);
                excelList.clear(); // 导出当前列表以后清空,释放服务器内存
            }
        }else { // 不超出则一次性全部导出
            excelWriter = EasyExcel.write(fos, StudentInfoExportExcel.class).build();
            WriteSheet writeSheet = EasyExcel.writerSheet(fileName).head(StudentInfoExportExcel.class).build();
            List<StudentInfoExportExcel> excelList = studentInfoMapper.findAndExportStudentInfoList(param);
            excelWriter.write(excelList , writeSheet);
            excelList.clear(); // 导出当前列表以后清空,释放服务器内存
        }
        // 关闭
        excelWriter.finish();
        byte[] fileByte = FileUtils.readFileToBinary(saveFileName); // 将临时文件转为二进制数组,然后上传到云存储服务器
        if (Objects.nonNull(fileByte) && fileByte.length > 0) {
            InputStream is = new ByteArrayInputStream(fileByte);
            String url = cloudStorageService.uploadFile(new MockMultipartFile(fileName, fileStr, "", is));
            log.info("导出出来完成,数据量:{}, 路径:{}", totalCount, url);
            messageTitle = title + "导出处理完成";
            downloadMsg.setMessageTitle(messageTitle);
            downloadMsg.setMessageBody(String.format(bodyFormat, fileName, url, fileName));
        } else {
            messageTitle = title + "导出处理失败";
            downloadMsg.setMessageTitle(messageTitle);
            downloadMsg.setMessageBody(title + "导出处理失败,请重新点击导出");
        }
        downloadMsg.setStatus(0);
        messageCenterMapper.updateById(downloadMsg);
        savefile.delete();
        // 关闭流
        fos.close();
        return Result.success("导出已经处理正在处理中,请到消息中心查看处理结果并下载文件");
    } catch (Exception e) {
        log.info("学生信息信息导出异常:{}", e);
        return Result.error("学生信息导出系统异常");
    }
}

        【总结】

   1. 创建代办消息,提示告诉用户要耐心等待处理完成;与此通过线程池启动线程处理导出。

   2. 处理过程:

(1)根据当前用户点击导出时的查询条件,获取当前目标导出数据的最小 ID 和最大 ID;

       (2)再统计最小 ID~ 最大 ID 范围内所有的数据总数 totalCount(用于下一步计算要多少个 Sheet 来装)

       (3)判断 totalCount 没有超过 Sheet 容量就一次性导出,如果超过则分 Sheet

       (4)根据数据总数 totalCount 计算 Sheet 张数,每张 Sheet 存储 100w 条数据;

Integer pageSize = Constants.EXPORT_MAX_SHEET_PAGE_SIZE; // 设置的单个 Sheet 的数据量,必须小于 1048576,在此设置(Constants.EXPORT_MAX_SHEET_PAGE_SIZE=100,0000)

                                       long sheetCount = (totalCount + Constants.EXPORT_MAX_SHEET_PAGE_SIZE - 1) / pageSize;

        (5)循环 Sheet 数量进行分页查询和加载到 Excel 中,加载每一页后要清空 List,否则服务器内存容易会被消耗完而导致服务器崩溃重启。

       (6)将生成 Excel 存储到 Linux / 本地服务器空间比较大的文件路径,将其读取并上传到云存储,上传完成后得到下载 url,删除 Linux / 本地服务器文件释放磁盘空间。

        (7)更新消息中心信息,把 url 放入消息体,提示用户当前的导出处理完成,可以点击链接下载。

           最近在工作中遇到用户反馈说导出功能不好用,就单独抽出时间来优化平台中导出的功能,之前把导出优化的任务分给同事来搞,写的代码看起来就重写,导出的数据都不对,最后还是要我出马。

 积硅步以致千里

                      --------------------------------------------------------------------->>[Every problem has its solutions]

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值