Excel文件导出总结,包含大数据量的分批导出方式

2023-11-01

目前本文章中分配导出的相关代码已更新至本人github的练习项目中
https://github.com/kimikudo/practice-back

更新记录

2023-08-10 谁能想到拖延很久很久终于在去年完成的笔记,居然真的帮助了我一次,遇到了一个类似的需求,根据模板导出数据,数据量大概1W行,300+列,文件大小15M左右. 本来的导出方式就是POI,一般的业务场景也不会有这么多列的情况,所以在尝试导出时候项目可能会直接崩溃.因为是个比较大比较老的项目,很多工具类都是封装好的,也不好重新引入其他工具,所以就在原来的基础上进行了一些修改优化,最终实现了这个需求,具体的我记录到下面POI原生方式-大数据量分批导出下面.

需求背景

前两天项目中有个将数据导出为Excel的功能,一开始用的是Excel4J进行导出,但是由于数据量较大导出很慢,而且会出现内存溢出的问题.所以准备进行优化.首先想到的是分批查询数据进行导出.搜索了一些资料也准备开始做了.但是同事说可以直接使用阿里的EasyExcel,导出速度和内存占用方面确实提升明显.但是分批导出的方式还是顺便自己业余时间实现一下并且做个记录吧!

参考内容

这里要感谢一下我搜到的一篇博客
POI百万级大数据量EXCEL导出
以及该作者另一篇EasyExcel实现的博客
阿里开源(EasyExcel)—导出EXCEL
之前发现Excel4J方式出现内存溢出问题之后,查询资料发现可以使用POI的SXSSFWorkbook来解决,于是就找到了这篇内容,他的解决方案给了我很大的启发,所以我才觉得自己实现并进行总结的.在此感谢这位作者!

导出方式

目前暂时总结了三种数据导出为Excel的方式,分别是Excel4J方式,POI原生方式,和阿里EasyExcel方式,其中Excel4J的效果不太理想,我也暂时不考虑使用该方法进行大数据量导出,只总结了该方式的普通导出用法,大数据量仅使用另外两种方式进行实现.

代码实现

Excel4J

普通导出

  1. 添加Excel4J依赖
    <!-- Excel4j导出 -->
    <!-- https://mvnrepository.com/artifact/com.github.crab2died/Excel4J -->
    <dependency>
        <groupId>com.github.crab2died</groupId>
        <artifactId>Excel4J</artifactId>
        <version>3.0.0</version>
    </dependency>
    
  2. 使用注解标识导出表头
    这里使用一张IMSI数据表进行演示,使用注解方式,在数据实体类的属性上,使用@ExcelField(title = "IMSI", order = 1)注解将属性指定为表头,并可添加列序号,
    @Data
    @ToString
    @ApiModel(value = "IMSI源数据")
    public class ImsiSourceData {
        /**
         * 源数据表主键
         */
        @ApiModelProperty("源数据表主键")
        private Integer id;
        /**
         * 设备编号
         */
        @ApiModelProperty("设备编号")
        @ExcelField(title = "设备编号", order = 2)
        private String deviceid;
        /**
         * imei
         */
        @ApiModelProperty("imei")
        private String imei;
        /**
         * imsi
         */
        @ApiModelProperty("imsi")
        @ExcelField(title = "IMSI", order = 1)
        private String imsi;
        /**
         * 上报时间
         */
        @ApiModelProperty("上报时间")
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        @ExcelField(title = "采集时间", order = 3, writeConverter = DateTimeConverter.class)
        private Long reportTime;
        /**
         * 归属地
         */
        @ApiModelProperty("归属地")
        @ExcelField(title = "归属地", order = 4)
        private String homeLocation;
        /**
         * 运营商
         */
        @ApiModelProperty("运营商")
        @ExcelField(title = "运营商", order = 5)
        private String networkOperator;
        /**
         * 网络制式
         */
        @ApiModelProperty("网络制式")
        @ExcelField(title = "网络制式", order = 6)
        private String terminalNetworkInformation;
    }
    
  3. 数据导出
    通过查询数据库获得数据,再直接调用自带方法即可实现导出;
    其中,exportObjects2Excel()方法的参数,第三个可以指定为输出流,为了处理输出流 ,可能需要对响应头的属性进行设置.
    public void exportByExcel4J(HttpServletResponse response) throws IOException {
        List<ImsiSourceData> list = this.list(
                new QueryWrapper<ImsiSourceData>()
                        .orderByDesc("report_time")
                        .last(" LIMIT 50")
        );
        //Excel4J方式导出
        try {
            ExcelUtils.getInstance().exportObjects2Excel(list,ImsiSourceData.class,"IMSI导出测试.xlsx");
        } catch (Excel4JException e) {
            e.printStackTrace();
        }
    }
    
  4. 效果测试
    我这里直接指定的文件名,即在项目根目录成功导出文件.
    在这里插入图片描述

POI原生方式

普通导出

这里只是简单的记录一下POI原生的导出方式,与本次博客内容关系不大,本次主要是记录分批导出的设计方案.

  1. 添加POI依赖
    <!-- POI依赖 -->
    <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>4.1.2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>4.1.2</version>
    </dependency>
    
  2. 创建Excel对象,创建标题行
    //HSSFWorkbook是处理.xls文件的,XSSFWorkbook处理.xlsx文件
    HSSFWorkbook workbook = new HSSFWorkbook();
    HSSFSheet sheet = workbook.createSheet("sheet1");
    //标题行
    HSSFRow headRow = sheet.createRow(0);
    headRow.createCell(0).setCellValue("ID");
    headRow.createCell(1).setCellValue("IMSI");
    headRow.createCell(2).setCellValue("采集时间");
    headRow.createCell(3).setCellValue("运营商");
    
  3. 遍历数据,创建内容行,并写入文件/输出流
    //写数据
    List<ImsiSourceData> list = this.listData();
    int i = 1;
    for (ImsiSourceData imsiSourceData : list) {
        HSSFRow row = sheet.createRow(i);
        row.createCell(0).setCellValue(imsiSourceData.getId());
        row.createCell(1).setCellValue(imsiSourceData.getImsi());
        //时间戳字段的处理
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        row.createCell(2).setCellValue(format.format(new Date(imsiSourceData.getReportTime())));
        row.createCell(3).setCellValue(imsiSourceData.getNetworkOperator());
        i++;
    }
    FileOutputStream os = new FileOutputStream("POI导出_" + System.currentTimeMillis() + ".xls");
    workbook.write(os);
    os.flush();
    os.close();
    
  4. 生成的文件效果
    在这里插入图片描述

大数据量分批导出

由于POI的工作簿对象会存在内存溢出的问题,所以在进行大数据量导出是,考虑使用SXSSFWorkbook对象,这是一个基于XSSF的工作簿对象,适用于大对象的创建和大数据量的导出,同时支持.xlsx后缀名.
POI的分批导出参考的是这篇博客: POI百万级大数据量EXCEL导出 自己参照这篇博客的方式尝试实现了一遍,该说不说原博主写的还不错!这里记录一下自己的实现过程.

2023-08更新 实际应用记录

遇到一个需求需要读取模板,然后向模板中写入相应数据,数据量大概在1W行300列 文件大小15M左右,正常使用XSSFWorkbook一样会挂,所以想到是否可以使用SXSSFWorkbook来实现写入操作. 由于数据量不算特别大,而且是多数据源的数据源根据人员拼接在一行里面,所以没必要才去之前记录的分次分批写入的操作,直接使用此工作簿对象进行写入即可.

  • 加载Workbook
    指定文件路径,读取为inputStream,再根据流创建XSSFWorkbook对象,最后使用XSSFWorkbook创建SXSSFWorkbook,并指定窗口大小,不指定也可以,会有默认的
/**
     * 打开exel模板文件-SXSSFWorkbook
     *
     * @param tempFileName
     * @return
     */
    public static SXSSFWorkbook loadExcelTemplateSXSSF(String tempFileName, Integer windowSize) {

        // 打开模板文件
        InputStream in = ExcelUtil.class.getClassLoader()
                .getResourceAsStream(tempFileName);
        // 新建HSSFWorkbook
        XSSFWorkbook workbook = null;
        SXSSFWorkbook sxssfWorkbook = null;
        try {
            workbook = new XSSFWorkbook(in);
            if (windowSize != null) {
                sxssfWorkbook = new SXSSFWorkbook(workbook, windowSize);
            } else {
                sxssfWorkbook = new SXSSFWorkbook(workbook);
            }

        } catch (IOException e) {
            System.err.println("创建SXSSFWorkbook失败");
            e.printStackTrace();
        }

        try {
            in.close();
        } catch (IOException e) {
            System.err.println("关闭模板文件失败");
            e.printStackTrace();
        }

        return sxssfWorkbook;
    }
  • 写数据
    之后正常使用此workbook获取sheet进行写数据即可,就可以实现窗口分批写入数据了,超简单!
SXSSFSheet sheet = sxssfWorkbook.getSheetAt(0);
  • !!!需要读取文件进行修改操作时
    由于此次是读取模板进行写数据,模板难免会有需要修改一下表头的情况,当出现这种应用场景,如果使用SXSSFSheet 时一般会提示xxx行已写入磁盘不可修改之类的异常,其实这种时候只要将写入数据和修改模板的操作分开,前后进行就行了.写入大数据量数据时使用SXSSFSheet,修改模板时使用XSSFSheet即可,结合使用更好用哦!
Sheet sheet= workbook.getXSSFWorkbook().getSheetAt(0);
依赖版本
  • POI 5.2.2 用于excel写入
  • MyBatisPlus 3.4 用于数据查询
实现思路

在贴代码之前先说一下这个功能的实现思路
数据量大的情况下导出数据,需将数据划分为不同的sheet,再根据不同sheet进行写入,所以在写入前需先确定单个sheet保存多少条数据,单次写入多少条数据,从而可以计算出单个sheet的写入次数.再根据次数进行循环写入.数据查询也是在每次写入时完成,可以直接理解为分页查询,根据当次的写入量查询对应数量的数据来完成写入,从而尽可能不发生内存溢出的问题.
规定数据条数的常量如下

public class ExcelConstant {
    /**
     * 每次写入的记录条数
     */
    public static final Integer WRITE_ROW_COUNT = 100000;

    /**
     * 单个sheet保存的记录数
     */
    public static final Integer SHEET_ROW_COUNT = 500000;

    /**
     * 每个sheet的写入次数: 总数/单次写入
     */
    public static final Integer WRITE_TIMES = SHEET_ROW_COUNT / WRITE_ROW_COUNT;
}
POI工具类

该工具类包含根据数据量来初始化Workbook,创建对应数量的sheet,并设置表头;将Workbook保存到文件或输出流;分批将数据写入Workbook等功能

import javax.servlet.http.HttpServletResponse;

import com.kay.practiceback.practice.excel.poi.constant.ExcelConstant;
import com.kay.practiceback.practice.excel.poi.delegate.WriteDataDelegated;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import java.io.*;
import java.nio.charset.StandardCharsets;

/**
 * POI工具类
 *
 * @author Kay
 * @date 2022-05-19
 */
@Slf4j
public class PoiUtil {
    /**
     * 初始化表格
     *
     * @param totalRow 数据总行数
     * @param titles   列名集合
     * @return workbook
     */
    public static SXSSFWorkbook init(Integer totalRow, String[] titles) {
        //定义窗口行数,即进行读写时,内存中存在100条数据
        SXSSFWorkbook wb = new SXSSFWorkbook(1000);
        //计算需要多少个sheet
        int sheetCount = (totalRow % ExcelConstant.SHEET_ROW_COUNT == 0) ?
                (totalRow / ExcelConstant.SHEET_ROW_COUNT) : (totalRow / ExcelConstant.SHEET_ROW_COUNT + 1);
        log.info("初始化excel,共计 {} 条数据,共计 {} 个sheet", totalRow, sheetCount);

        for (int i = 0; i < sheetCount; i++) {
            //初始化sheet
            SXSSFSheet sheet = wb.createSheet("sheet" + (i + 1));
            //创建表头行,并写入表头
            SXSSFRow headRow = sheet.createRow(0);
            for (int j = 0; j < titles.length; j++) {
                SXSSFCell headCell = headRow.createCell(j);
                headCell.setCellValue(titles[j]);
            }
        }
        log.info("Excel文件初始化完成,共计{}个sheet", sheetCount);
        return wb;
    }

    /**
     * 将excel保存到本地路径
     *
     * @param wb   excel对象
     * @param path 保存路径
     */
    public static void saveExcelToPath(SXSSFWorkbook wb, String path) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(path);
            wb.write(fos);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != wb) {
                wb.dispose();
            }
            if (null != fos) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 将excel转为输出流
     *
     * @param wb       excel
     * @param response response
     * @param fileName 文件名
     */
    public static void saveExcelToStream(SXSSFWorkbook wb, HttpServletResponse response, String fileName) throws UnsupportedEncodingException {
        response.setHeader("Content-disposition", "attachment;filename=" + new String((fileName + ".xlsx").getBytes(StandardCharsets.UTF_8), "ISO8859-1"));
        OutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            wb.write(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != wb) {
                try {
                    wb.dispose();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (null != outputStream) {
                try {
                    outputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 导出Excel到指定路径
     *
     * @param totalRow           总记录数
     * @param titles             标题
     * @param exportPath         导出路径
     * @param writeDataDelegated 数据写入委托
     */
    public static void exportToPath(Integer totalRow, String[] titles, String exportPath, WriteDataDelegated writeDataDelegated) {
        log.info("开始导出excel文件...");
        //初始化工作簿
        SXSSFWorkbook workbook = PoiUtil.writeWorkbook(totalRow, titles, writeDataDelegated);
        //写入成功后将excel保存到指定路径
        PoiUtil.saveExcelToPath(workbook, exportPath);
        log.info("Excel文件导出完成");
    }

    /**
     * 导出Excel到指定路径
     *
     * @param totalRow           总记录数
     * @param titles             标题
     * @param response           导出的输出流
     * @param fileName           文件名
     * @param writeDataDelegated 数据写入委托
     */
    public static void exportToStream(Integer totalRow, String[] titles, HttpServletResponse response, String fileName, WriteDataDelegated writeDataDelegated) throws UnsupportedEncodingException {
        log.info("开始导出excel到流...");
        SXSSFWorkbook workbook = PoiUtil.writeWorkbook(totalRow, titles, writeDataDelegated);
        //写入成功后将excel保存到指定路径
        PoiUtil.saveExcelToStream(workbook, response, fileName);
        log.info("Excel输出流导出完成");
    }

	/**
     * 数据写入,创建工作簿并写入数据
     *
     * @param totalRow           总数据条数
     * @param titles             表头
     * @param writeDataDelegated 写数据委托
     * @return workbook
     */
    public static SXSSFWorkbook writeWorkbook(Integer totalRow, String[] titles, WriteDataDelegated writeDataDelegated) {
        log.info("写入数据开始...");
        //初始化工作簿
        SXSSFWorkbook workbook = PoiUtil.init(totalRow, titles);
        //获取sheet数目,遍历写入sheet
        int sheetCount = workbook.getNumberOfSheets();
        for (int i = 0; i < sheetCount; i++) {
            log.info("开始第{}次写入", i + 1);
            SXSSFSheet sheet = workbook.getSheetAt(i);
            //根据单个sheet和单次写入数量计算的写入次数,来进行多次写入
            for (int j = 0; j < ExcelConstant.WRITE_TIMES; j++) {
                int pageNum = i * ExcelConstant.WRITE_TIMES + j + 1;
                int pageSize = ExcelConstant.WRITE_ROW_COUNT;
                int startRow = j * ExcelConstant.WRITE_ROW_COUNT + 1;
                writeDataDelegated.writeData(sheet, startRow, pageNum, pageSize);
            }
        }
        return workbook;
    }
}

数据写入

参考原博客的委托方式,这里也创建了一个委托接口,可在接口的实现中自行查询数据并将数据逐行写入sheet,我这里创建了一个订单表的写数据委托实现,如果需要其他内容的导出,再创建一个接口实现即可
接口

import org.apache.poi.xssf.streaming.SXSSFSheet;

/**
 * Excel数据写入委托类
 *
 * @author Kay
 * @date 2022-07-13
 */
public interface WriteDataDelegated {
    /**
     * 分页查询并将数据写入excel
     *
     * @param sheet    写入的数据表
     * @param startRow 写入开始行
     * @param pageNum  数据查询分页页数
     * @param pageSize 数据查询记录条数
     */
    void writeData(SXSSFSheet sheet, Integer startRow, Integer pageNum, Integer pageSize);
}

实现
这里仅作练习和测试使用,所以数据比较简单只是单表查询,导出的字段也随便填了几个,实际应用中可以根据实际需求进行增减和格式化

import cn.hutool.core.util.NumberUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.kay.practiceback.db.order.entity.OrderRecord;
import com.kay.practiceback.db.order.service.OrderRecordService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 订单记录的Excel数据写入委托
 *
 * @author Kay
 * @date 2022-07-13
 */
@Slf4j
@Component
public class OrderWriteDataDelegated implements WriteDataDelegated {

    @Autowired
    private OrderRecordService orderRecordService;

    @Override
    public void writeData(SXSSFSheet sheet, Integer startRow, Integer pageNum, Integer pageSize) {
        log.info("开始分批查询并写入数据...");
        //根据条件查询待写入的数据
        List<OrderRecord> orderList = orderRecordService.page(new Page<OrderRecord>(pageNum, pageSize),
                new LambdaQueryWrapper<OrderRecord>().orderByDesc(OrderRecord::getCreateTime)).getRecords();
        log.info("本次写入起始行号为:{},共计 {} 条数据", startRow, orderList.size());
        if (orderList.size() > 0) {
            for (int i = startRow; i < orderList.size() + startRow; i++) {
                OrderRecord data = orderList.get(i - startRow);
                SXSSFRow row;
                if (null != sheet.getRow(i)) {
                    row = sheet.getRow(i);
                } else {
                    row = sheet.createRow(i);
                }
                //写入单行数据,根据实际需要写入所需字段,这里仅写入几列进行测试
                row.createCell(0).setCellValue(data.getId().toString());
                row.createCell(1).setCellValue(data.getClientId().toString());
                row.createCell(2).setCellValue(data.getAccount().toString());
                row.createCell(3).setCellValue(NumberUtil.div(data.getDeno().toString(), "100",0).toString());
                row.createCell(4).setCellValue(NumberUtil.decimalFormat("#.##", NumberUtil.div(data.getPrice().toString(), "100")));
                row.createCell(5).setCellValue(data.getState());
                row.createCell(6).setCellValue(data.getCreateTime().toString());
            }
        }
        log.info("本次写入完成,当前sheet总行数为:{}", sheet.getLastRowNum());
    }
}

调用测试

创建服务类,定义文件名,进行到处之前先计算一下要导出的数据总数,之后执行导出即可

@Service
public class PoiExportServiceImpl implements PoiExportService {

    @Autowired
    private OrderRecordService orderRecordService;
   	/**
     * 写数据委托类,使用注解注入所需的实现
     */
    @Autowired
    @Qualifier("orderWriteDataDelegated")
    private OrderWriteDataDelegated orderWriteDataDelegated;

    @Override
    public void exportOrder(String fileName) {
    	//定义表头,去委托中相同
        String[] titles = {"订单号", "渠道编号", "充值账号", "面额", "售价", "状态", "创建时间"};
        String path = "D:\\Kay\\export\\";
        File pathFile = new File(path);
        if (!pathFile.exists()) {
            pathFile.mkdirs();
        }
        //计算总条数,与委托类中的查询相同
        Long totalCount = orderRecordService.count(new LambdaQueryWrapper<OrderRecord>().orderByDesc(OrderRecord::getCreateTime));
        PoiUtil.exportToPath(totalCount.intValue(), titles, path + fileName, orderWriteDataDelegated);
    }
}
测试结果

我的数据表中大概有66W条数据,导出总共用时1分钟左右,文件大小29M左右,功能基本上是实现了的,截图了一sheet1的行数,与预期一致,时间可以格式化的,偷懒没弄!就这样吧!
在这里插入图片描述

一个小意外

代码刚完成开始测试时候,第二次写入总是报错 Attempting to write a row[100001] in the range [0,661376] that is already written to disk.,开始以为是循环时候写入的行号不正确,仔细审阅了代码发现也没什么问题…于是开始逐行debug…最后发现了…是第一次写入时候数据查询的分页没有生效!单次就像给我写入总条数了!..赶紧加上了MyBatisPlus的分页配置,问题解决

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor paginationInnerInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
        return interceptor;
    }
}

EasyExcel

因为这个博客坑了太久了,期间其实EasyExcel也更新了,所以这里就按我现在填坑时候的操作来写吧!

普通导出

普通导出参考官方文档的步骤即可: 写Excel|EasyExcel
这里的版本是3.1.1

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

在实体类的属性上添加@ExcelProperty注解规定需要导出的列,注解可以规定index指定顺序,无需导出的添加@ExcelIgnore即可,对于日期或数值类型的字段可以使用 @DateTimeFormat@NumberFormat进行格式化,对于需要自定义输出格式的可以自行实现Converter<T>接口进行格式化
对于Long型的字段,可以使用EasyExcel自带的LongStringConverter将Long转为String

@ApiModelProperty("订单号")
    @ExcelProperty(value = "订单号", index = 0, converter = LongStringConverter.class)
    private Long id;

    @ExcelProperty(value = "渠道编码", index = 1)
    private Integer clientId;

    @ExcelProperty(value = "支付金额", index = 6, converter = AmountConverter.class)
    private Integer price;

    @ExcelIgnore
    private Integer payType;

    @ExcelProperty(value = "充值金额", index = 5, converter = AmountConverter.class)
    private Integer deno;

    @ExcelProperty(value = "创建时间", index = 8)
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

Converter:

import cn.hutool.core.util.NumberUtil;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;

public class AmountConverter implements Converter<Integer> {
    @Override
    public Class<Integer> supportJavaTypeKey() {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception {
        return new WriteCellData<String>(NumberUtil.div(context.getValue().toString(), "100", 2).toString());
    }
}

之后直接进行写Excel操作即可,其他写Excel方式参考文档就好了~

@Override
    public void simpleWrite() {
        String fileName = "D:\\Kay\\export\\订单导出_" + System.currentTimeMillis() + ".xlsx";
        EasyExcel.write(fileName, OrderRecord.class).sheet("订单记录").doWrite(() -> {
            return orderRecordService.lambdaQuery().orderByDesc(OrderRecord::getCreateTime).last(" LIMIT 100").list();
        });
    }

最后的效果
在这里插入图片描述

大数据量分批导出

EasyExcel的分批导出相较于POI简单了许多,原理与POI是相同的,都是分页查询数据,然后再写入sheet,循环查询并写入直到所有数据导出完成,代码也几行就搞定了!重点关注EasyExcel相关的几行操作即可,如创建excelWriter,创建sheet并写入数据,最后excelWriter.finish()输出文件

	@Override
    public void batchExport() {
        log.info("开始导出数据...");
        String fileName = "D:\\Kay\\export\\订单导出_批量_" + System.currentTimeMillis() + ".xlsx";
        ExcelWriter excelWriter = EasyExcel.write(fileName, OrderRecord.class).build();
        //数据总量
        long totalCount = orderRecordService.count(new LambdaQueryWrapper<OrderRecord>().orderByDesc(OrderRecord::getCreateTime));
        //单个sheet保存的数据量
        int perSheetCount = 100000;
        //sheet总数
        long sheetCount = totalCount % perSheetCount == 0 ? (totalCount / perSheetCount) : (totalCount / perSheetCount + 1);
        log.info("总数据量:{},共计{}个sheet", totalCount, sheetCount);
        for (int i = 0; i < sheetCount; i++) {
            log.info("开始第{}个sheet写入", i);
            //分页查询写入数据
            List<OrderRecord> orderList = orderRecordService.page(new Page<OrderRecord>(i + 1, perSheetCount),
                    new LambdaQueryWrapper<OrderRecord>().orderByDesc(OrderRecord::getCreateTime)).getRecords();
            //创建sheet
            WriteSheet sheet = EasyExcel.writerSheet(i, "订单记录-" + i).build();
            excelWriter.write(orderList, sheet);
        }
        excelWriter.finish();
        log.info("数据导出完成");
    }

结果测试

导出文件
在这里插入图片描述

EasyExcel我导出的列相较于POI多几个,为了测试自定义格式转换的功能,而且sheet数量也不同,最终导出66W数据耗时1分钟16s,文件大小45.5M

结语

至此!这个已经挖了快两年的坑终于被我填上了!当时挖坑是因为我们的项目中的导出功能在数据量很大时会产生内存溢出,后来切换为EasyExcel就解决了当时的问题,但是当时翻阅资料时候看到了最开始提到的那位作者的文章,想着自己一定也要再来实现一次才好!但是拖延症加上好后来跳槽了之后变得异常忙碌,一直搁置了没有填坑,最近难得闲下来了,开始回头重新学些这些!期间EasyExcel也出了好几版,这里用的是目前最新的版本,也算给自己留个存档吧!!!
好了!还有个前端学习的坑要填!继续努力去咯!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Excel文件导出总结,包含大数据量的分批导出方式 的相关文章

  • 如何使用 exec.Command 在 golang 中执行 Mysql 脚本

    您好 我正在尝试执行一个脚本以使用 Golang 将数据填充到数据库中 func executeTestScript cmd exec Command usr local mysql bin mysql h127 0 0 1 P3333 u
  • 在 Java 中从 SOAPMessage 获取原始 XML

    我已经在 J AX WS 中设置了 SOAP WebServiceProvider 但我无法弄清楚如何从 SOAPMessage 或任何 Node 对象获取原始 XML 下面是我现在获得的代码示例 以及我试图获取 XML 的位置 WebSe
  • javax.persistence.RollbackException:提交事务时出错],根本原因是 java.lang.StackOverflowError:null

    我有一个使用 Spring Data REST 框架的 Spring Boot API 从 spring boot starter parent 2 1 0 RELEASE 继承的依赖项 我正在尝试执行 PUT 或 PATCH 请求来更新实
  • 检查 IPv4 地址是否在私有范围内

    在 Python 中 使用 IPy 模块您可以执行以下操作 gt gt gt ip iptype PRIVATE 有没有一个库或简单的方法可以在 Java 中执行相同的操作 似乎不完全是但是InetAddress有一些 isXX 方法 例如
  • 需要正则表达式帮助

    我正在尝试替换两次或多次出现的 br like br br br 标签与两个一起 br br 具有以下模式 Pattern brTagPattern Pattern compile lt s br s s gt s 2 Pattern CA
  • 在 Java 中创建 T 的新实例

    在C 中 我们可以定义一个泛型class A
  • 正则表达式在 Velocity 模板中不起作用

    我在 Test java 中尝试过这个 String regex lt s br s s gt String test1 lt br gt System out println test replaceAll regex 但是当我在速度模板
  • java.lang.Object的hashCode具体使用的算法是什么

    中使用的算法是什么JVM实施java lang Object的隐含的hashCode 方法 OpenJDK or Oracle JDK答案中首选 它依赖于实现 并且在很大程度上 该算法是entirely取决于实施 只要它是一致的 但是 根据
  • MySQL 中非空值的计数和分组

    我需要计算按特定 ID 分组的非空 我的意思是至少包含 1 个字符的字符串 行 例如 我的数据可能如下所示 form id mapping 1 value 1 1 1 value 2 2 2 NULL 3 value 3 我想计算每个表单的
  • 如何使用 swagger-codegen-plugin (maven) 生成客户端代码?

    我需要使用 swagger codegen plugin for maven 在 eclipse 中生成服务器存根代码 你能帮忙怎么做吗 以及需要什么配置 在 pom xml 中 我找到了这个答案 您只需要像下面这样更改 pom xml 即
  • 改变 Java 中凯撒移位的方向

    用户可以通过选择 1 向左或 2 向右移动字母来选择向左或向右移动 左边工作正常 右边不行 现在它显示了完全相同的循环 但我已经改变了所有 and 以不同的方式进行标记 最终我总是得到奇怪的字符 如何让程序将字符向相反方向移动 如果用户输入
  • 未知的表引擎“InnoDB”

    最近 我发现如果我有好的硬件 我可以最大限度地提高 mysql 的性能 由于我一直在使用 InnoDB 所以我在 my ini 中添加了额外的配置 以下是新添加的配置 innodb data file path ibdata1 10M au
  • 在循环中按名称访问变量

    我正在开发一个 Android 项目 并且有很多可绘制对象 这些绘图的名称都类似于icon 0 png icon 1 png icon 100 png 我想将这些可绘制对象的所有资源 ID 添加到整数 ArrayList 中 对于那些不了解
  • Java和手动执行finalize

    如果我打电话finalize 在我的程序代码中的一个对象上 JVM当垃圾收集器处理这个对象时仍然再次运行该方法吗 这是一个大概的例子 MyObject m new MyObject m finalize m null System gc 是
  • 从浏览器访问本地文件?

    您好 我想从浏览器访问系统的本地文件 由于涉及大量安全检查 是否可以通过某种方式实现这一目标 或使用 ActiveX 或 Java Applet 的任何其他工作环境 请帮帮我 要通过浏览器访问本地文件 您可以使用签名的 Java Apple
  • Java String ReplaceAll 方法给出非法重复错误?

    我有一个字符串 当我尝试运行时replaceAll方法 我收到这个奇怪的错误 String str something op str str replaceAll o n it works fine str str replaceAll n
  • android 中的 java.net.URL ..新手问题

    我是java新手 正在尝试android开发 以下代码生成 malformedURLException 有人可以帮助我识别异常吗 任何提示都会非常有帮助 package com example helloandroid import and
  • 在 Freemarker 模板中检查 Spring 安全角色和记录的用户名

    有谁知道 freemarker 标签来检查 freemarker 文件中的 spring 安全角色和用户名 我从网络上的几个资源中发现以下代码将打印登录的用户名 但它没有打印用户名 而是打印 登录为
  • java中的预增量/后增量

    有人可以帮助我理解为什么 int i 1 int j 1 int k 1 int l 1 System out println i i System out println j j System out println k k System
  • 使用自定义比较器在 Java 中创建 SortedMap

    我想创建一个TreeMap在 Java 中具有自定义排序顺序 排序后的键是字符串 需要根据第二个字符进行排序 这些值也是字符串 示例地图 Za FOO Ab Bar 您可以像这样使用自定义比较器 Comparator

随机推荐

  • 教你如何安装SCSI硬盘 安装、启动、设置篇

    SCSI硬盘一向认为是昂贵且高不可攀的存储设备 不仅本身价位高于IDE硬盘很多 而且必须另外选择SCSI接口卡 因此在这样的条件下 一般计算机用户便倾向于使用IDE接口的硬盘 IDE硬盘虽然具有低价的优势 但始终敌不过SCSI硬盘的强悍速度
  • python算法中的深度学习算法之强化学习(详解)

    目录 学习目标 学习内容 强化学习 环境建模 Markov决策过程
  • C++11 -- 入门基础知识

    文章目录 C 11简介 列表初始化 std initializer list 变量类型推导 nullptr 范围for循环 STL中的一些变化 C 11简介 在2003年C 标准委员会曾经提交了一份技术勘误表 简称TC1 使得C 03这个名
  • uniapp+vue3+vite+ts+uviewPlus搭建项目

    创建项目架构 首先使用官方提供的脚手架创建一个项目 这里我创建的 vue3 vite ts 的项目 npx degit dcloudio uni preset vue vite ts project name 如命令行创建失败 请直接访问
  • python selenium启动常用

    加载超时设置 browser set page load timeout 5 二进制转16进制 a 00000000 hex int a 2 from selenium import webdriver import time from s
  • Python学习-----流程控制3.0(习题实操)

    目录 前言 1 求和 2 求阶乘 3 输出100以内的奇数 4 打印九九乘法表 5 斐波那契数列 6 打印空心矩形 7 打印三角形 8 求两个数字之间的最大公约数 9 质数问题 10 进制数转换 11 字符统计 12 字符匹配问题 13 求
  • 终于有人把数据指标体系讲明白了

    来源 数据学堂 数据岗的核心职能 在于产出数据资产 提升信息的价值密度 而指标体系就是一个组织最为重要的数据资产 那么 1 为什么指标体系这么重要 2 什么是指标体系 3 指标体系的衡量标准是什么 4 如何去搭建一套好的指标体系 只要弄清楚
  • C++ primer 第十一章习题

    chapter11 关联容器 文章目录 chapter11 关联容器 练习 11 1 节练习 练习11 1 练习11 2 练习11 3 练习11 4 11 2 1 节练习 练习11 5 练习11 6 练习11 7 练习11 8 11 2 2
  • 脏牛提权(cve-2016-5159)

    漏洞范围 大于2 6 22版本 2007年发行 到2016年10月18日修复 危害 低权限的用户可利用这一漏洞在本地进行提权 原理 linux内核的子系统在处理写入时复制至产生了竞争条件 恶意用户可利用此漏洞来获取高权限 对只读内存映射进行
  • 如何快速选择开源许可证License,看这三个就够了

    开源License很多 如果你不想在License耗费太多精力 那么推荐你重点了解这三种 GPL Apache License及MIT 这三种在开源License中很具代表性 使用广泛 且简洁易理解 同时 这三种license是经过OSI
  • MySQL数据库学习

    目录 从管理员cmd页面打开数据库 创建一个用户 数据库的基本操作 数据完整性 完整性约束管理 表的基本操作 判断关键字 聚合函数 多表连接查询 嵌套查询 联合查询 事务 锁 索引 视图 存储过程 函数 与存储过程类似 光标 触发器 JAV
  • lazarus调用http接口解析json(迎接云计算适应微服务)

    lazarus 跨平台free pascal语言ide工具 社区 http www fpccn com 下载 ftp freepascal dfmk hu pub lazarus 号称一次编码 到处编译 window linux macos
  • 制造业数据治理白皮书(2022版)

    全书基于双方赋能一线制造业企业数字化转型过程的实操践行 经验沉淀和所感所悟 分别从背景及趋势 现状与挑战 实施途径 典型案例等角度揭示了当下制造业数据治理的重要性 关注公众号 互联互通社区 回复 DATA176 获取全部报告内容 精彩推荐
  • ES集群宕机后处理——重新分配shards,负载均衡

    ES集群5台机器 由于同时读写导致其中一台机器宕机 原本每天的索引shard数设定为10 这样5台机器每台分配2个shard 但是一旦集群宕机 重启集群后 5号机器宕机导致它上面的shard会转移到其他1 4号机器上 如果此时往ES里写数据
  • UE4_DatatTable数据保存

    UE4 提供了很多数据持久的工具 很多 1 2 3 网上使用UE4 c 操作DataTable的也很多 不接入别人的链接了 使用C 操作DataTable修改数据也没啥大的问题 坑爹的地方 修改完数据之后 重启编辑器之后 修改的数据恢复之前
  • 线性、非线性分类器&数据的线性、非线性

    线性 非线性分类器 数据的线性 非线性 一 线性分类器 有无数个可划分这两个线性可分类的超平面 在二维空间里面 一个线性分类器是一条线 图14 8展示了五个分类例子 这些线有一个函数形式w1x1 w2 x2 b 线性分类器的分类规则是 如果
  • Unity如何使用手机进行调试(真机)

    文章目录 手机操作 具体步骤 Unity操作 错误处理 没有检测到手机 手机操作 首先打开手机的 USB调试 开关 具体步骤 这里以华为手机举例 手机的系统是EMUI10 具体操作如下 首先打开手机 gt 进入 设置 找到 关于手机 连续点
  • linux内核模块作用,Linux内核模块(二)

    ko kernel object so shared object root rhel6 ls lib modules uname r kernel arch x86 kvm kvm amd ko kvm intel ko kvm ko 通
  • 强化学习奖励和状态设计

    奖励 1 稀疏奖励问题 2 奖励模式化问题 3 奖励不能太过于全局化 4 记住一些常用的奖励设置方式 5 逆向强化学习自动涉及回报函数 6 避免奖励异常问题 贪婪 来回踱步 胆怯 不敢走 主线奖励太小 鲁莽 惩罚不够 7 采用reward
  • Excel文件导出总结,包含大数据量的分批导出方式

    文章目录 更新记录 需求背景 参考内容 导出方式 代码实现 Excel4J 普通导出 POI原生方式 普通导出 大数据量分批导出 2023 08更新 实际应用记录 依赖版本 实现思路 POI工具类 数据写入 调用测试 测试结果 一个小意外