杰瑞科技汇

Java导出大量Excel数据如何优化性能?

下面我将从 问题分析最佳实践方案代码示例其他方案对比 四个方面,为你详细讲解如何高效地导出大量数据。

Java导出大量Excel数据如何优化性能?-图1
(图片来源网络,侵删)

问题分析:为什么会 OOM?

  1. Apache POI 的 SXSSF vs XSSF

    • XSSF: 用于处理 .xlsx 格式,它会将整个 Excel 文档(包括所有行和单元格)都保存在内存中,如果你的数据有几十万行,内存消耗会非常巨大,直接导致 OutOfMemoryError
    • SXSSF (Streaming Usermodel API): 这是 解决大数据量导出的核心,它采用了 流式处理 的思想,它只保留一部分行(保存在内存中)和临时文件(保存在磁盘上)在内存中,当行数超过预设的 window size 时,最早写入的行就会被“刷”到临时文件中,从而释放内存,所有内存中的数据和临时文件会被合并成一个完整的 .xlsx 文件。
  2. .xls vs .xlsx:

    • HSSF: 用于处理旧的 .xls 格式,它和 XSSF 类似,也是将整个文档加载到内存,不适合大数据量。
    • 导出大量数据,必须使用 .xlsx 格式,并配合 SXSSF API。

最佳实践方案:Apache POI SXSSF

这是目前最主流、最推荐的方案,下面是详细步骤和代码示例。

添加 Maven 依赖

确保你的 pom.xml 文件中包含 POI 的相关依赖。

Java导出大量Excel数据如何优化性能?-图2
(图片来源网络,侵删)
<dependencies>
    <!-- Apache POI Core -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>5.2.3</version> <!-- 建议使用较新版本 -->
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.3</version>
    </dependency>
    <!-- POI 对 Excel 2003 格式的支持 (可选) -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-scratchpad</artifactId>
        <version>5.2.3</version>
    </dependency>
</dependencies>

核心代码实现

关键点:

  • 使用 SXSSFWorkbook 而不是 XSSFWorkbook
  • 可以设置 window size (SXSSFWorkbook(int rowAccessWindowSize)),这个值决定了内存中最多保留多少行,100 表示内存中只保留最新的 100 行,其余的写入磁盘,通常设置为 100 是一个不错的选择。
  • 数据分批查询:不要一次性从数据库查出所有数据,而应该使用分页查询(如 MySQL 的 LIMIT offset, size),每次只查询一页的数据,然后写入 Excel,再查询下一页,这是最最关键的一步,可以极大地降低数据库和应用的内存压力。
  • 使用 try-with-resources:确保 WorkbookOutputStream 被正确关闭,避免资源泄漏。
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFRow;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class LargeDataExportExample {
    // 模拟从数据库分页查询数据
    public static List<User> queryUsersFromDB(int pageNum, int pageSize) {
        // 这里应该是你的数据库查询逻辑,例如使用 MyBatis 或 JPA
        // 为了演示,我们创建一些假数据
        List<User> users = new ArrayList<>();
        for (int i = 0; i < pageSize; i++) {
            int userId = (pageNum - 1) * pageSize + i + 1;
            users.add(new User(userId, "User-" + userId, "user" + userId + "@example.com", new BigDecimal("10000.00")));
        }
        return users;
    }
    public static void main(String[] args) {
        // 1. 准备数据 - 模拟总共有 1,000,000 条数据
        int totalRows = 1_000_000;
        int pageSize = 1000; // 每页查询 1000 条
        int totalPages = (int) Math.ceil((double) totalRows / pageSize);
        // 2. 创建 SXSSFWorkbook 对象
        // 参数 100 表示内存中最多保留 100 行,超过的会写入磁盘
        try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
            // 3. 创建 Sheet
            Sheet sheet = workbook.createSheet("Users");
            // 4. 创建表头(仅创建一次)
            Row headerRow = sheet.createRow(0);
            headerRow.createCell(0).setCellValue("ID");
            headerRow.createCell(1).setCellValue("Name");
            headerRow.createCell(2).setCellValue("Email");
            headerRow.createCell(3).setCellValue("Salary");
            // 5. 分批查询和写入数据
            for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
                System.out.println("正在处理第 " + pageNum + " / " + totalPages + " 页数据...");
                List<User> users = queryUsersFromDB(pageNum, pageSize);
                // 遍历当前页数据并写入 Excel
                for (int i = 0; i < users.size(); i++) {
                    User user = users.get(i);
                    // 注意:行号是全局的,从 0 开始,表头是第 0 行,所以数据从第 1 行开始
                    int rowNum = (pageNum - 1) * pageSize + i + 1;
                    Row row = sheet.createRow(rowNum);
                    row.createCell(0).setCellValue(user.getId());
                    row.createCell(1).setCellValue(user.getName());
                    row.createCell(2).setCellValue(user.getEmail());
                    row.createCell(3).setCellValue(user.getSalary().doubleValue());
                }
            }
            // 6. 写入文件
            try (FileOutputStream out = new FileOutputStream("D:/temp/large_data_export.xlsx")) {
                workbook.write(out);
                System.out.println("Excel 文件导出成功!");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 用户实体类
    static class User {
        private int id;
        private String name;
        private String email;
        private BigDecimal salary;
        // constructor, getters, and setters
        public User(int id, String name, String email, BigDecimal salary) {
            this.id = id;
            this.name = name;
            this.email = email;
            this.salary = salary;
        }
        public int getId() { return id; }
        public String getName() { return name; }
        public String getEmail() { return email; }
        public BigDecimal getSalary() { return salary; }
    }
}

代码优化点

  • setCellValue 的数据类型:尽量使用 setCellValue(String)setCellValue(double) 等基本类型,而不是直接传入复杂对象,减少不必要的转换开销。
  • 样式处理:SXSSF 对样式的支持有限,如果你需要对大量单元格应用复杂的样式,这可能会影响性能,尽量保持样式简单,或者在创建 CellStyle 后重复使用它。
  • 列宽自动调整autoSizeColumn() 方法在 SXSSF 中是不支持的,因为它需要遍历所有行来计算宽度,这与流式处理的初衷相悖,如果必须设置列宽,最好在创建表头后,根据已知内容或预估宽度手动设置。

其他备选方案

除了 Apache POI,还有其他一些工具也可以处理大数据量导出,各有优劣。

EasyExcel (阿里巴巴)

这是目前非常流行的一个选择,尤其在国内,底层也是基于 POI 的 SXSSF,但做了大量优化,API 更加简洁易用。

优点

Java导出大量Excel数据如何优化性能?-图3
(图片来源网络,侵删)
  • API 简洁:注解驱动,代码量少,学习成本低。
  • 性能好:对内存的控制非常好,同样是流式读写。
  • 功能丰富:支持 Excel 模板、数据转换、自定义格式等。

缺点

  • 生态和社区相比 POI 较小,遇到某些复杂问题可能资料较少。

Maven 依赖:

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

代码示例 (非常简洁):

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
public class EasyExcelExample {
    public static void main(String[] args) {
        // 准备数据
        List<User> dataList = new ArrayList<>();
        for (int i = 0; i < 1_000_000; i++) {
            dataList.add(new User(i + 1, "User-" + (i + 1), "user" + (i + 1) + "@example.com", new BigDecimal("10000.00")));
        }
        // 写入文件
        String fileName = "D:/temp/large_data_export_easyexcel.xlsx";
        EasyExcel.write(fileName, User.class).sheet("Users").doWrite(dataList);
        System.out.println("EasyExcel 导出成功!");
    }
    // 实体类,使用 @ExcelProperty 注解映射列
    public static class User {
        @ExcelProperty("ID")
        private Integer id;
        @ExcelProperty("Name")
        private String name;
        @ExcelProperty("Email")
        private String email;
        @ExcelProperty("Salary")
        private BigDecimal salary;
        // constructor, getters, setters
        public User(Integer id, String name, String email, BigDecimal salary) {
            this.id = id;
            this.name = name;
            this.email = email;
            this.salary = salary;
        }
        // ...省略getter/setter
    }
}

注意:EasyExcel 虽然代码简单,但在处理极致大数据量(如数千万行)时,其内部缓冲机制可能导致内存占用略高于手动控制的 POI SXSSF,但对于绝大多数场景,EasyExcel 已经足够优秀且开发效率高。

JXL (Java Excel API)

一个比较古老的轻量级库,只支持 .xls 格式。

优点

  • 非常轻量,API 简单。
  • 内存占用相对较低。

缺点

  • 不支持 .xlsx 格式,这是致命缺陷。
  • 停止更新维护多年,不推荐用于新项目。

方案对比与选择建议

特性 Apache POI SXSSF EasyExcel (Alibaba) JXL
支持格式 .xlsx, .xls .xlsx, .xls .xls
内存管理 优秀,流式处理,可配置窗口大小 优秀,流式处理,自动优化 一般,仅支持旧格式
API 易用性 较繁琐,需要手动创建 Row/Cell 非常简洁,注解驱动 简单
性能 非常好,是行业标准 非常好,略优于 POI 一般
功能丰富度 极其强大,支持所有 Excel 特性 丰富,支持模板、数据校验等 较少
社区与生态 非常成熟,文档齐全 非常活跃,国内社区好 已过时,社区小
推荐场景 对 Excel 兼容性、功能要求极高的项目 绝大多数新项目,追求开发效率和性能 维护旧项目或只导出 .xls

如何选择?

  1. 新项目,追求快速开发首选 EasyExcel,它的注解式开发能为你节省大量代码,性能也足够好。
  2. 新项目,需要极致性能或复杂操作:使用 Apache POI SXSSF,你可以对内存和性能进行更精细的控制。
  3. 维护旧项目,只支持 .xls:可以考虑 JXL,但更建议升级到 .xlsx 并使用 POI 或 EasyExcel
  4. 需要导出超大数据(亿级):如果单文件太大,可以考虑分多个 sheet,或者考虑 CSV 格式(纯文本,体积小,解析快,但无格式),如果必须单文件,POI SXSSF 依然是最后的保障。

导出大量数据到 Excel 的核心思想是 “流式处理”“分批查询”

  • 工具选型:抛弃旧的 HSSF 和 JXL,使用 SXSSF (POI)EasyExcel
  • 关键代码:使用 SXSSFWorkbook,并设置合理的 rowAccessWindowSize
  • 数据源务必从数据库分页查询数据,一次性加载所有数据是导致 OOM 的主要原因。
  • 资源管理:使用 try-with-resources 确保 WorkbookOutputStream 被关闭。

遵循以上原则,你就可以稳定、高效地完成 Java 大数据量 Excel 导出任务。

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