EasyExcel 百万级别数据高效率导入/导出

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:业务设计
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

前言

在日常的开发中,用的比较多的方式就是 Apache 下的 POI 框架了,但在目前数据量大的时代下,这种方式显得已经不适合了

编写该篇文章主要是公司中实际上用到了这种场景,将它作为文章分享出来,如果这种方式的导入/导出能帮助到大家也挺开心的,或者大家也可以发表自己在这方面的看法,提出来,做为更好的一种方式去呈现

节点分以下几步走,先是介绍原始 Apache POI 以及由阿里巴巴开源的一框 POI 框架做对比,再是提出解决这方面问题上的思路,然后进行案例演示,最后记录一下在使用中遇到的错误点.

POI 框架特性对比

poi 依赖的基础接口:WorkBook,有几种实现子类需要进行区分,如下:

  1. HSSFWorkbook:Excel 2003(包含) 之前版本使用的子类对象,处理的文件格式都是 .xls 的,其是 poi 中最常用的方式,处理的行数在 6W+,一般处理的数据不超过这个大小就不会出现内存溢出的,这个量内存也是足够支撑的.
  2. XSSFWorkbook:Excel 2003-2007 使用的子类对象,目前还是有大量公司使用的这个,文件格式为 .xlsx,出现这个格式就是为了突破 HSSFWorkBook 6W 数据的局限,是为了针对Excel2007版本的 1048576行,16384 列,最多可以导出 104w 条数据,虽然在数据上增加了,但是内存的瓶颈也就来了,OOM 离之不远了.
  3. SXSSFWorkbook:该实现类是 POI3.8 之后的版本才有的,它可以操作 Excel2007 以后的所有版本 Excel,扩展名是 .xlsx,这种方式提供了一种低内存占用机制,存储百万数据丝毫不是问题,一般不会出现内存溢出(它使用硬盘来换内存,也就是说当内存数据到达一定时会采用硬盘来进行存储,内存里存储的只会是最新的数据),缺点:因为它使用到了硬盘,当数据到达硬盘以后,也就无法完成数据的克隆或者公式计算,sheet.clone() 已经无法被支持了

在使用过程中,如果不涉及到 Excel 公式和样式并且数据量较大的情况下,推荐使用 SXSSFWorkbook;数据量不超过 6W~7W 也涉及到了公式的计算,推荐使用 XSSFWorkbook

大数据量解决思路

使用传统的 poi 导入导出方式,当数据量过大时,明显会出现 OOM 异常,因此推荐大家使用阿里巴巴开源的 easyExcel 框架作为导入导出的媒介

GitHub - alibaba/easyexcel: 快速、简单避免OOM的处理Excel工具

导入

  1. 导入数据也分批次的导入,导入也需要集合来存储 Excel 行列的值,当这个批次被导入完成后,集合进行清空,防止重复处理且减少内存的占用
  2. 在下面的案例中,会通过 MyBatis、JDBC 做批量的数据库插入操作,很明显你就会 JDBC 的效率比 MyBatis 高,因为 MyBatis 底层也是通过 JDBC 对数据库进行操作的,MyBatis 在处理结果集的时候都是一条一条数据进行循环遍历处理的,所以在这个基础上效率就下降了.

导出

  1. 不要一次性将数据量全量查询出来,如果几百万数据一下查出来,内存是吃不下的,所以应该对数据进行分批查出,比如:20W一次、10W一次
  2. 将数据查询出来以后,进行 Excel 写入时,也应该做批次的写入操作,一行一行的写入会造成大量 IO 请求,性能随之就会下降
  3. 几百万数据同时放在一个 sheet 里面,不然打开一个 sheet 都会停顿很长时间,操作时也会明显变慢,例如:一个 sheet 存储一百万数据
  4. 导出到 excel 时,肯定需要依赖集合进行数据的临时存储,在每个批次的数据处理完以后,记得对集合进行清空,以便让 GC 知道这个可以提前进行回收了,内存也不会一直被占用.

案例演示

准备工作

前期准备好 SQL 脚本和存储函数用于批量生成假数据

# 表结构脚本
CREATE TABLE `act_result_log` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `onlineseqid` varchar(32) DEFAULT NULL,
  `businessid` varchar(32) DEFAULT NULL,
  `becifno` varchar(32) DEFAULT NULL,
  `ivisresult` varchar(32) DEFAULT NULL,
  `createdby` varchar(32) DEFAULT NULL,
  `createddate` date DEFAULT NULL,
  `updateby` varchar(32) DEFAULT NULL,
  `updateddate` date DEFAULT NULL,
  `risklevel` varchar(32) DEFAULT NULL,
  `is_deleted` tinyint(4) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `account_id_IDX` (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1000002 DEFAULT CHARSET=utf8mb4;

# 存储函数,生成五百万数据,等待时间会比较长
CREATE PROCEDURE demo.batch_generate ( )
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 5000000 DO
INSERT INTO `demo`.`act_result_log` (`onlineseqid`, `businessid`, `becifno`, `ivisresult`, `createdby`,`createddate`, `updateby`, `updateddate`,`risklevel`)
VALUES(CONCAT('0' + i), CONCAT('1'+ i), concat('2'+i), concat('3'+i),'1',now(),'1',now(),'1');
SET i = i + 1;
END WHILE;
END

EasyExcel maven 版本依赖

<easyexcel.version>3.0.5</easyexcel.version>

<!--EasyExcel相关依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>${easyexcel.version}</version>
</dependency>

若在你项目还用到了低版本的 EasyExcel,需要对旧版本的 poi 依赖进行排除

<easyexcel.high.version>3.0.5</easyexcel.high.version>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>${easyexcel.high.version}</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
        </exclusion>
    </exclusions>
</dependency>

示例代码

基础依赖的实体类、mapper、service、控制器、公共类

1、数据库实体

@Data
@Accessors(chain = true)
@TableName("act_result_log")
public class ActResultLogDO implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("onlineseqid")
    private String onlineseqid;

    @TableField("businessid")
    private String businessid;

    @TableField("becifno")
    private String becifno;

    @TableField("ivisresult")
    private String ivisresult;

    @TableField("createdby")
    private String createdby;

    @TableField("createddate")
    private LocalDate createddate;

    @TableField("updateby")
    private String updateby;

    @TableField("updateddate")
    private LocalDate updateddate;

    @TableField("risklevel")
    private String risklevel;
}

2、实体 Vo

@Data
public class ActResultLogVO implements Serializable {
    private static final long serialVersionUID = 1L;

    @ExcelProperty(value = "onlineseqid",index = 0)
    private String onlineseqid;

    @ExcelProperty(value = "businessid",index = 1)
    private String businessid;

    @ExcelProperty(value = "becifno",index = 2)
    private String becifno;

    @ExcelProperty(value = "ivisresult",index = 3)
    private String ivisresult;

    @ExcelProperty(value = "createdby",index = 4)
    private String createdby;

    @ExcelProperty(value = "createddate",index = 5)
    private LocalDate createddate;

    @ExcelProperty(value = "updateby",index = 6)
    private String updateby;

    @ExcelProperty(value = "updateddate",index = 7)
    private LocalDate updateddate;

    @ExcelProperty(value = "risklevel",index = 8)
    private String risklevel;
}

3、控制器

@RequestMapping("/excel")
@RestController
public class ExcelController {

    @Resource
    private EasyExcelUtil easyExcelUtil;

    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) {
        easyExcelUtil.dataExport300w(response);
    }
}

4、常量类

public class ExcelConstants {
    public static final Integer PER_SHEET_ROW_COUNT = 100*10000;
    public static final Integer PER_WRITE_ROW_COUNT = 20*10000;
    public static final Integer GENERAL_ONCE_SAVE_TO_DB_ROWS_JDBC = 10*10000;
    public static final Integer GENERAL_ONCE_SAVE_TO_DB_ROWS_MYBATIS = 5*10000;
}

导入

导入分两种方式进行,第一种是通过 MyBatis 编写 SQL 语句运行,不使用 MP 是因为它底层的批量新增是一条一条写入的,并不是实际意义上的批次;第二种是通过原生 JDBC 进行批量操作,使用手动提交事务的方式.

1、Service 接口

public interface IActResultLogService extends IService<ActResultLogDO> {
    /**
     * 通过分页参数查询一百w数据
     * @return
     */
    List<ActResultLogDO> findByPage100w(Integer pageNum,Integer pageSize);

    /**
     * 从 Excel 导入数据,批次为 10w,通过 JDBC
     * @param dataList
     * @return
     */
    Map<String, Object> import2DBFromExcel10wByJDBC(List<Map<Integer, String>> dataList);

    /**
     * 从 Excel 导入数据,批次为 10W,通过 MyBatis
     * @param actResultLogList
     */
    void import2DBFromExcel10wByMybatis(List<ActResultLogDO> actResultLogList);
}

2、Service 接口实现类

@Slf4j
@Service
public class ActResultLogServiceImpl extends ServiceImpl<ActResultLogMapper, ActResultLogDO> implements IActResultLogService {

    @Override
    public List<ActResultLogDO> findByPage100w(Integer pageNum, Integer pageSize) {
        PageInfo<ActResultLogDO> pageInfo = PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(() -> {
            QueryWrapper<ActResultLogDO> wrapper = new QueryWrapper<>();
            // TODO 此处可以进行添加条件过滤
            baseMapper.selectList(wrapper);
        });
        return pageInfo.getList();
    }


    // Service中具体业务逻辑

    /**
     * 测试用Excel导入超过10w条数据,经过测试发现,使用 Mybatis 批量插入速度非常慢,所以这里可以使用 数据分批+JDBC分批插入+事务来继续插入速度会非常快
     */
    @Override
    public Map<String, Object> import2DBFromExcel10wByJDBC(List<Map<Integer, String>> dataList) {
        Map<String, Object> result = new HashMap<>();
        // 结果集中数据为0时,结束方法.进行下一次调用
        if (dataList.size() == 0) {
            result.put("empty", "0000");
            return result;
        }
        // JDBC分批插入+事务操作完成对10w数据的插入
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            long startTime = System.currentTimeMillis();
            log.info("{} 条,开始导入到数据库时间:{}", dataList.size(), startTime + "ms");
            conn = JDBCDruidUtils.getConnection();
            // 控制事务:默认不提交
            conn.setAutoCommit(false);
            String sql = "insert into ACT_RESULT_LOG (onlineseqid,businessid,becifno,ivisresult,createdby,createddate,updateby,updateddate,risklevel) values";
            sql += "(?,?,?,?,?,?,?,?,?)";
            ps = conn.prepareStatement(sql);
            // 循环结果集:这里循环不支持"烂布袋"表达式
            for (int i = 0; i < dataList.size(); i++) {
                Map<Integer, String> item = dataList.get(i);
                ps.setString(1, item.get(0));
                ps.setString(2, item.get(1));
                ps.setString(3, item.get(2));
                ps.setString(4, item.get(3));
                ps.setString(5, item.get(4));
                ps.setTimestamp(6, new Timestamp(System.currentTimeMillis()));
                ps.setString(7, item.get(6));
                ps.setTimestamp(8, new Timestamp(System.currentTimeMillis()));
                ps.setString(9, item.get(8));
                // 将一组参数添加到此 PreparedStatement 对象的批处理命令中。
                ps.addBatch();
            }
            // 执行批处理
            ps.executeBatch();
            // 手动提交事务
            conn.commit();
            long endTime = System.currentTimeMillis();
            log.info("{} 条,结束导入到数据库时间:{}", dataList.size(), endTime + "ms");
            log.info("{} 条,导入用时:{}", dataList.size(), (endTime - startTime) + "ms");
            result.put("success", "1111");
        } catch (Exception e) {
            result.put("exception", "0000");
            e.printStackTrace();
        } finally {
            // 关连接
            JDBCDruidUtils.close(conn, ps);
        }
        return result;
    }


    // 采用 mapper 编写 SQL 语句进行测试,效率明显比原生 JDBC 要低
    @Override
    public void import2DBFromExcel10wByMybatis(List<ActResultLogDO> actResultLogList) {
        baseMapper.importToDb(actResultLogList);
    }
}
MyBatis

1、Mapper.xml 映射文件

<insert id="importToDb" parameterType="ActResultLogDO">
         insert into ACT_RESULT_LOG (onlineseqid,businessid,becifno,ivisresult,createdby,createddate,updateby,updateddate,risklevel)
        values
        <foreach collection="list" item="item" index="index" separator=",">
            (
            #{item.onlineseqid},#{item.businessid},#{item.becifno},#{item.ivisresult},#{item.createdby},#{item.createddate},
            #{item.updateby},#{item.updateddate},#{item.risklevel}
            )
        </foreach>
</insert>

Excel.read() 方法需要一个监听器,当(事件)数据到达时,通过你自定义的这个监听器进行数据的流转处理.

2、EasyExcel 监听器

public class EasyExcelGeneralDataMybatisListener extends AnalysisEventListener<ActResultLogVO> {
    private IActResultLogService actResultLogService;

    /**
     * 用于存储读取的数据
     */
    private List<ActResultLogVO> dataList = new ArrayList<>();

    public EasyExcelGeneralDataMybatisListener() {

    }

    public EasyExcelGeneralDataMybatisListener(IActResultLogService actResultLogService) {
        this.actResultLogService = actResultLogService;
    }

    @Override
    public void invoke(ActResultLogVO data, AnalysisContext context) {
        // 数据add进入集合
        dataList.add(data);
        // size是否为100000条:这里其实就是分批.当数据等于10w的时候执行一次插入
        if (dataList.size() >= ExcelConstants.GENERAL_ONCE_SAVE_TO_DB_ROWS_JDBC) {
            // 存入数据库:数据小于 1w 条使用 Mybatis 批量插入即可
            saveData();
            // 清理集合便于GC回收
            dataList.clear();
        }
    }

    /**
     * 保存数据到 DB
     */
    private void saveData() {
        if (dataList.size() > 0) {
            actResultLogService.import2DBFromExcel10wByMybatis(StarBeanUtils.copyList(dataList, ActResultLogDO.class));
            dataList.clear();
        }
    }

    /**
     * Excel 中所有数据解析完毕会调用此方法
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData();
        dataList.clear();
    }
}

3、单元测试类

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StarApplication.class)
public class EasyExcelTest {
    @Resource
    private IActResultLogService actResultLogService;

    // EasyExcel的读取Excel数据的API
    @Test
    public void import2DBFromExcel10wTest() {
        String fileName = "/Users/vnjohn/Downloads/export-excel.xlsx";
        // 记录开始读取Excel时间,也是导入程序开始时间
        long startReadTime = System.currentTimeMillis();
        log.info("------开始读取Excel的Sheet时间(包括导入数据过程):" + startReadTime + " ms------");
        // 读取所有Sheet的数据.每次读完一个Sheet就会调用这个方法
        EasyExcel.read(fileName, ActResultLogVO.class, new EasyExcelGeneralDataMybatisListener(actResultLogService))
                .registerConverter(new EasyExcelLocalDateConvert()).doReadAll();
//        EasyExcel.read(fileName, new EasyExcelGeneralDataJDBCListener(actResultLogService)).doReadAll();
        long endReadTime = System.currentTimeMillis();
        log.info("------结束读取Excel的Sheet时间(包括导入数据过程):" + endReadTime + " ms------");
        log.info("------导入总花费时长:{}", ((endReadTime - startReadTime) / 1000) + "s------");
    }
}

通过 Mybatis 导入,需要映射到具体的实体,JDK1.8 以后时间类型大部分都是使用 LocalDate 这种类型了,在 EasyExcel 中没有提供这种 Excel 时间列值到时间类型的转换,所以我们需要自定义一个转换器用来适配

4、时间转换类

public class EasyExcelLocalDateConvert implements Converter<LocalDate> {

    @Override
    public Class<LocalDate> supportJavaTypeKey() {
        return LocalDate.class;
    }

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

    @Override
    public LocalDate convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
                                       GlobalConfiguration globalConfiguration) throws Exception {
        return LocalDate.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }

    @Override
    public WriteCellData<LocalDate> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return new WriteCellData<>(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    }
}

最后,执行 @Test 方法,结果如下:

------开始读取ExcelSheet时间(包括导入数据过程):1657257416753ms
------结束读取ExcelSheet时间(包括导入数据过程):1657257794753 ms------
------导入总花费时长:378s------
JDBC

1、数据库配置 properties 文件

driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://127.0.0.1/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username=root
password=123456

使用 JDBC 读取 Excel 每一行数据返回的数据类型是 Map<Integer,String>,所以它对应的监听器的处理过程如下:

public class EasyExcelGeneralDataJDBCListener extends AnalysisEventListener<Map<Integer, String>> {
    private IActResultLogService actResultLogService;

    /**
     * 用于存储读取的数据
     */
    private List<Map<Integer, String>> dataList = new ArrayList<>();

    public EasyExcelGeneralDataJDBCListener() {
    }

    public EasyExcelGeneralDataJDBCListener(IActResultLogService actResultLogService) {
        this.actResultLogService = actResultLogService;
    }

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        // 数据add进入集合
        dataList.add(data);
        // size是否为100000条:这里其实就是分批.当数据等于10w的时候执行一次插入
        if (dataList.size() >= ExcelConstants.GENERAL_ONCE_SAVE_TO_DB_ROWS_JDBC) {
            // 存入数据库:数据小于 1w 条使用 Mybatis 批量插入即可
            saveData();
            // 清理集合便于GC回收
            dataList.clear();
        }
    }

    /**
     * 保存数据到 DB
     */
    private void saveData() {
        if (dataList.size() > 0) {
            actResultLogService.import2DBFromExcel10wByJDBC(dataList);
            dataList.clear();
        }
    }

    /**
     * Excel 中所有数据解析完毕会调用此方法
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData();
        dataList.clear();
    }
}

最后,执行 @Test 方法后,结果如下:

------开始读取ExcelSheet时间(包括导入数据过程):1657258268921 ms------
------结束读取ExcelSheet时间(包括导入数据过程):1657258435024 ms------
------导入总花费时长:166s------

比较 MyBatis、原生 JDBC 执行后的结果可以看到,JDBC 的效率比 MyBatis 提升了 2.2 以上

导出

导出的数据量按照 200W 来进行测试,100W 存到一个 sheet 里面,从数据库先查询总数,然后 /100W 得出多少个 sheet,外层循环是 sheet 数量,内层是要读取数据库的次数,比如:一次性查询出 20W 数据,那么一个 sheet 就需要查 5 次,因此查询次数在 sheet数*5 这个范围;注意:在每次从数据库读取完以后,记得清空集合的元素,方便提前被 GC 回收,防止内存的空间占用

1、导出工具类

@Slf4j
@Component
public class EasyExcelUtil {
    @Resource
    private IActResultLogService actResultLogService;

    /**
     * 导出逻辑代码
     *
     * @param response
     */
    public void dataExport300w(HttpServletResponse response) {
        OutputStream outputStream = null;
        try {
            long startTime = System.currentTimeMillis();
            log.info("导出开始时间:{}", startTime);
            outputStream = response.getOutputStream();
            WriteWorkbook writeWorkbook = new WriteWorkbook();
            writeWorkbook.setOutputStream(outputStream);
            writeWorkbook.setExcelType(ExcelTypeEnum.XLSX);
            ExcelWriter writer = new ExcelWriter(writeWorkbook);
            String fileName = new String(("export-excel").getBytes(), StandardCharsets.UTF_8);
            // TODO WriteTable 标题这块可以作为公共的封装起来:通过反射获取变量上注解等
            WriteTable table = new WriteTable();
            List<List<String>> titles = new ArrayList<List<String>>();
            titles.add(Collections.singletonList("onlineseqid"));
            titles.add(Collections.singletonList("businessid"));
            titles.add(Collections.singletonList("becifno"));
            titles.add(Collections.singletonList("ivisresult"));
            titles.add(Collections.singletonList("createdby"));
            titles.add(Collections.singletonList("createddate"));
            titles.add(Collections.singletonList("updateby"));
            titles.add(Collections.singletonList("updateddate"));
            titles.add(Collections.singletonList("risklevel"));
            table.setHead(titles);
            // 记录总数:实际中需要根据查询条件(过滤数据)进行统计即可,
            // TODO 此处写入限定的条数进行自测
//            Integer totalCount = actResultLogService.count();
            Integer totalCount = 200 * 10000;
            // 每一个Sheet存放100w条数据
            Integer sheetDataRows = ExcelConstants.PER_SHEET_ROW_COUNT;
            // 每次写入的数据量20w
            Integer writeDataRows = ExcelConstants.PER_WRITE_ROW_COUNT;
            // 计算需要的Sheet数量
            int sheetNum = totalCount % sheetDataRows == 0 ? (totalCount / sheetDataRows) : (totalCount / sheetDataRows + 1);
            // 计算一般情况下每一个Sheet需要写入的次数(一般情况不包含最后一个sheet,因为最后一个sheet不确定会写入多少条数据)
            int oneSheetWriteCount = totalCount > sheetDataRows ? sheetDataRows / writeDataRows : totalCount % writeDataRows > 0 ? totalCount / writeDataRows + 1 : totalCount / writeDataRows;
            // 计算最后一个sheet需要写入的次数
            int lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount : (totalCount % sheetDataRows % writeDataRows == 0 ? (totalCount / sheetDataRows / writeDataRows) : (totalCount / sheetDataRows / writeDataRows + 1));

            // 开始分批查询分次写入
            // 注意这次的循环就需要进行嵌套循环了,外层循环是Sheet数目,内层循环是写入次数
            List<List<String>> dataList = new ArrayList<>();
            for (int i = 0; i < sheetNum; i++) {
                //创建Sheet
                WriteSheet sheet = new WriteSheet();
                sheet.setSheetNo(i);
                sheet.setSheetName(fileName + i);
                // 循环写入次数: j的自增条件是当不是最后一个Sheet的时候写入次数为正常的每个Sheet写入的次数,如果是最后一个就需要使用计算的次数lastSheetWriteCount
                for (int j = 0; j < (i != sheetNum - 1 || i==0 ? oneSheetWriteCount : lastSheetWriteCount); j++) {
                    // 集合复用,便于GC清理
                    dataList.clear();
                    // 分页查询一次20w
                    List<ActResultLogDO> resultList = actResultLogService.findByPage100w(j + 1 + oneSheetWriteCount * i, writeDataRows);
                    if (!CollectionUtils.isEmpty(resultList)) {
                        resultList.forEach(item -> {
                            dataList.add(Arrays.asList(item.getOnlineseqid(), item.getBusinessid(), item.getBecifno(), item.getIvisresult(), item.getCreatedby(), LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), item.getUpdateby(), LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), item.getRisklevel()));
                        });
                    }
                    // 写数据
                    writer.write(dataList, sheet, table);
                }
            }

            // 下载EXCEL 以下代码可以作为公共的进行封装.
            setExcelRespProp(response, fileName);
            writer.finish();
            outputStream.flush();
            // 导出时间结束
            long endTime = System.currentTimeMillis();
            log.info("导出结束时间:{}", endTime + "ms");
            log.info("导出所用时间:{}", (endTime - startTime) / 1000 + "秒");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    /**
     * 公共响应参数
     */
    public static void setResponseParam(HttpServletResponse response, String fileName) throws UnsupportedEncodingException {
		// 下载EXCEL 以下代码可以作为公共的进行封装.
		response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes("gb2312"), StandardCharsets.ISO_8859_1) + ".xlsx");
		response.setContentType("multipart/form-data");
		response.setCharacterEncoding("utf-8");
	}
	
	/**
     * 通过反射方式将头部作为公共部分进行设置
     */
	public void setTitles(Class clazz) {
		List<List<String>> titles = new ArrayList<List<String>>();
		for (Field declaredField : clazz.getDeclaredFields()) {
			ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class);
			if (null != annotation) {
				titles.add(Arrays.asList(annotation.value()));
			}
		}
	}
    
    /**
     * 设置excel下载响应头属性
     */
    public static void setExcelRespProp(HttpServletResponse response, String rawFileName){
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = null;
        try {
            fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
        } catch (UnsupportedEncodingException e) {
            log.error("设置excel下载响应头属性,失败 {}",e.getMessage());
        }
        response.setHeader("Content-disposition", "attachment;filename=utf-8''" + fileName + ".xlsx");
    }
}

前面已经把控制器类贴出来了,请求地址:前缀/excel/export,我这边采用的测试工具为 safari 浏览器,如果是 windows 可以使用自带的微软浏览器,Google、 Postman 我这边在测试的时候可能是因为数据量太大的原因导致内存爆满,程序就异常退出了.

导出的时间结果如下:

导出结束时间:1657259099773ms
导出所用时间:71

操作时遇到的错误

1、MySQL 允许最大的包文件参数调整

在进行 MyBatis 测试时,一下子堆积 10W 数据批量插入,MySQL 抛出异常提示传输的包文件过大,超出了参数的配置,导致无法正常插入,需要调整 show VARIABLES like ‘%max_allowed_packet%’,Windows 是在 server 根目录下的 my.ini 文件中,默认为 8M,将其调整 64M 即可;Linux/Mac 是在 server 根目录下的 my.cnf 文件里,Mac 无须调整,参数大小足够支撑了.

2、导入 Excel 列数据无法与实体属性映射问题

刚开始 ActResultLogVO 类上只是加了 @Getter/@Setter 注解,不是使用的 @Data 注解,导致无法映射,最终修改为 @Data 注解该问题不再出现,具体原因后续文章进行详细分析.

足以见识到 EasyExcel 支撑的强大数据量,对此底层实现后续可以详细了解和阅读,学习其强大的内功心法加粗样式

参考文献

https://www.freesion.com/article/8852561865/

https://blog.csdn.net/weixin_44848900/article/details/117701981

EasyExcel 案例源码

最后

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!干货连连不断~

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

评论 44
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

vnjohn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值