杰瑞科技汇

Java Excel大数据导出,如何优化避免内存溢出?

下面我将从核心思想、常用技术选型、代码实现、性能优化等多个方面,为你提供一个全面且深入的指南。

Java Excel大数据导出,如何优化避免内存溢出?-图1
(图片来源网络,侵删)

核心思想:避免 OOM 的关键

处理大数据导出,最核心、最关键的思想是:不要一次性将所有数据加载到内存中

传统的做法(如使用 Apache POI 的 XSSFWorkbookSXSSFWorkbook 之前的旧版本)会创建一个包含所有数据的 Excel 对象在内存中,当数据量达到几十万行时,内存占用会飙升,直接导致 JVM 崩溃。

正确的做法是采用流式处理分批处理,将数据分批写入到 Excel 文件中,而不是全部驻留在内存。


技术选型

Java 操作 Excel 主要有以下几类库,各有优劣,适用于不同场景。

Java Excel大数据导出,如何优化避免内存溢出?-图2
(图片来源网络,侵删)
类型 优点 缺点 适用场景
Apache POI 功能强大 功能最全面,支持 .xls.xlsx 格式,API 丰富。 - .xlsx 格式XSSFWorkbook 在内存中操作,大数据导出时极易 OOM
- .xls 格式HSSFWorkbook 有行数限制(65535行)。
小数据量导出、复杂 Excel 模板操作、.xls 格式兼容。
EasyExcel 推荐 - 基于 POI,但做了深度优化。
- 内存占用极低,解决了 POI 大数据导出的 OOM 问题。
- API 简洁,上手快,性能好。
相对 POI 功能稍少,但对于 99% 的导出场景已经足够。 大数据量导出、数据导入、日常 Excel 处理。
SXSSFWorkbook (POI) 流式处理 - 官方提供的大数据解决方案
- 通过 windowSize 控制内存中的行数,超出部分写入临时文件。
- API 相对复杂,不如 EasyExcel 简洁。
- 依赖临时文件,需要清理。
对 POI 有强依赖,且不想引入第三方新库的场景。
JXL 旧库 轻量级,API 简单。 已停止更新,不支持 .xlsx 格式,功能有限。 遗留系统维护,仅处理 .xls 格式。

对于大数据导出,强烈推荐使用 EasyExcel,它在 POI 的基础上做了完美的封装,既解决了内存问题,又提供了非常友好的 API,如果项目已经深度使用 POI 且不想引入新依赖,则可以使用 SXSSFWorkbook


代码实现(以 EasyExcel 为例)

EasyExcel 的核心是 ExcelWriterWriteSheet,它通过回调函数逐行写入数据,完美避免了内存问题。

添加 Maven 依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.2</version> <!-- 请使用最新版本 -->
</dependency>

准备数据模型

创建一个与 Excel 表头对应的 Java 实体类。@ExcelProperty 注解用于指定列名。

import com.alibaba.excel.annotation.ExcelProperty;
// 假设我们要导出用户信息
public class UserData {
    // value 对应 Excel 表头
    @ExcelProperty("用户ID")
    private Long id;
    @ExcelProperty("用户名")
    private String username;
    @ExcelProperty("邮箱")
    private String email;
    @ExcelProperty("创建时间")
    private Date createTime;
    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Date getCreateTime() { return createTime; }
    public void setCreateTime(Date createTime) { this.createTime = createTime; }
}

模拟数据查询

这是最关键的一步。不要一次性查询出所有数据,而是分批查询。

Java Excel大数据导出,如何优化避免内存溢出?-图3
(图片来源网络,侵删)
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class UserDataService {
    // 模拟从数据库分批查询数据
    // page: 页码, pageSize: 每页大小
    public List<UserData> queryDataByPage(int page, int pageSize) {
        // 这里应该是你的 DAO 层调用, userMapper.selectPage(page, pageSize);
        // 为了演示,我们模拟生成一些数据
        List<UserData> list = new ArrayList<>(pageSize);
        for (int i = 0; i < pageSize; i++) {
            long index = (page - 1L) * pageSize + i;
            UserData user = new UserData();
            user.setId(index + 1);
            user.setUsername("用户_" + index);
            user.setEmail("user_" + index + "@example.com");
            user.setCreateTime(new Date());
            list.add(user);
        }
        return list;
    }
}

核心导出逻辑

使用 EasyExcel.write() 进行流式写入。

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
public class BigDataExportDemo {
    public static void main(String[] args) {
        // 在 Web 环境中,通常是通过 HttpServletResponse 来输出
        // 这里为了演示,我们直接写入到文件
        String fileName = "big_data_export.xlsx";
        // 1. 创建 ExcelWriter 对象
        // fileName 是最终生成的文件名
        ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build();
        // 2. 创建 WriteSheet 对象,可以理解为 Excel 的一个 Sheet
        WriteSheet writeSheet = EasyExcel.writerSheet("用户数据").build();
        // 3. 模拟总数据量和分批大小
        long totalDataCount = 1_000_000L; // 100万条数据
        int pageSize = 5000; // 每批查询5000条
        // 4. 循环分批查询并写入
        UserDataService userService = new UserDataService();
        for (int page = 1; page <= (totalDataCount / pageSize + 1); page++) {
            System.out.println("正在处理第 " + page + " 页数据...");
            // 4.1 分批查询数据
            List<UserData> data = userService.queryDataByPage(page, pageSize);
            // 4.2 将当前批次数据写入到 Sheet 中
            // EasyExcel 会自动处理,不会将所有数据都加载到内存
            excelWriter.write(data, writeSheet);
            // 如果查询出的数据为空,说明已经处理完毕,可以提前退出
            if (data.isEmpty()) {
                break;
            }
        }
        // 5. 关闭流,非常重要!
        excelWriter.finish();
        System.out.println("导出完成!");
    }
    // Web 环境下的导出方法示例
    public void exportInWeb(HttpServletResponse response) throws IOException {
        String fileName = "用户数据_" + System.currentTimeMillis() + ".xlsx";
        // 设置响应头,告诉浏览器这是一个文件下载
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // URLEncoder.encode 处理中文文件名,防止乱码
        String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + encodedFileName + ";filename*=utf-8''" + encodedFileName);
        // 1. 创建 ExcelWriter,直接输出到 response 的 OutputStream
        ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), UserData.class).build();
        WriteSheet writeSheet = EasyExcel.writerSheet("用户数据").build();
        // ... (分批查询和写入的逻辑与上面 main 方法相同) ...
        long totalDataCount = 1_000_000L;
        int pageSize = 5000;
        UserDataService userService = new UserDataService();
        for (int page = 1; page <= (totalDataCount / pageSize + 1); page++) {
            List<UserData> data = userService.queryDataByPage(page, pageSize);
            excelWriter.write(data, writeSheet);
            if (data.isEmpty()) {
                break;
            }
        }
        // 2. 关闭流
        excelWriter.finish();
    }
}

性能优化技巧

除了流式写入,你还可以从以下几个方面进行优化:

  1. SQL 优化

    • 只查询需要的列:避免使用 SELECT *,明确指定列名,减少数据传输量。
    • 添加索引:确保查询字段(如分页的 idcreate_time)有索引,加快数据库查询速度。
    • 使用游标:如果你的数据库(如 MySQL)支持,可以使用 JDBC 的 ResultSet 游标,让数据库驱动逐行返回数据,而不是一次性加载到内存,这能进一步降低 JVM 内存压力。
  2. JVM 参数调优

    • 增加堆内存:如果导出过程非常耗时且数据量极大,可以适当增加 JVM 的最大堆内存(-Xmx)。
    • 使用 G1 垃圾回收器:对于大内存应用,G1 回收器通常比 CMS 有更好的表现,可以减少 Full GC 的停顿时间。-XX:+UseG1GC
  3. 服务器配置

    • 增加线程池:如果你的数据查询是 I/O 密集型(比如查询远程数据库),可以考虑使用多线程并行查询,然后将结果写入同一个 Excel 文件(注意线程安全和写入顺序)。
    • 使用临时文件SXSSFWorkbook 和 EasyExcel 底层都会使用临时文件,确保应用的临时目录(java.io.tmpdir)有足够的磁盘空间,并在导出完成后清理这些临时文件。
  4. 用户体验优化 (Web 场景)

    • 异步导出:对于耗时很长的导出任务,绝对不能在前端直接同步调用,应该采用异步任务模式。
      • 前端点击“导出”按钮后,先向服务器发送一个请求,服务器生成一个任务 ID 并立即返回。
      • 前端轮询任务状态,或者通过 WebSocket 接收任务进度更新。
      • 任务完成后,前端再通过一个链接去下载生成的文件。
    • 进度反馈:在异步任务中,可以记录已导出的行数,并通过 WebSocket 或轮询接口将进度反馈给前端,让用户了解导出进度。

对比:EasyExcel vs. POI SXSSFWorkbook

特性 EasyExcel POI SXSSFWorkbook
易用性 极高,API 简洁,像写普通 Java 代码。 一般,需要理解 SXSSFSheetSXSSFRow 等概念。
内存控制 非常优秀,通过监听器逐行写入,内存占用恒定。 优秀,通过 windowSize 控制内存中可见的行数。
底层原理 基于 POI SXSSF,但做了更高层封装。 原生的 POI 流式 API。
功能扩展 方便,可以轻松集成自定义的转换器、监听器。 需要直接操作 POI 底层,较为复杂。
推荐度 强烈推荐,是当前大数据导出的首选方案。 可选,适用于不想引入新依赖的 POI 老项目。

对于任何新的 Java 项目,需要进行大数据 Excel 导出时,请毫不犹豫地选择 EasyExcel,它为你解决了所有关于内存和性能的难题,让你可以专注于业务逻辑本身。

分享:
扫描分享到社交APP
上一篇
下一篇