杰瑞科技汇

Java Web如何实现Excel导出?

核心思想

无论使用哪种技术,Java Web 导出 Excel 的核心流程都是一样的:

Java Web如何实现Excel导出?-图1
(图片来源网络,侵删)
  1. 后端准备数据:在 Java 后端,你需要将需要导出的数据查询出来,并组织成一个结构化的形式(一个 List 或一个自定义的 List
  2. 生成 Excel 文件:使用某个库(如 Apache POI、EasyExcel)来创建一个 Excel 文件(通常是 .xlsx 格式),并将第一步准备好的数据填充到 Excel 的指定单元格中。
  3. 设置响应头:在 Servlet 中,通过 HttpServletResponse 设置响应头,告诉浏览器这是一个需要下载的文件,并指定文件的名称。
  4. 输出流:将第二步生成的 Excel 文件,通过输出流(OutputStream)写入到 HttpServletResponse 的输出流中,浏览器就会收到这个文件并触发下载。

主流技术方案对比

特性 Apache POI EasyExcel (阿里巴巴)
简介 Java 操作 Office 格式文件最经典、最强大的库,功能全面。 阿里巴巴开源,基于 POI 优化,解决了 POI 内存消耗大的问题,主打低内存
优点 功能强大,支持所有 Excel 版本和复杂格式(如公式、图表、宏)。
社区成熟,资料丰富。
内存占用极低,解决了 POI 的 OOM 问题,适合大数据量导出。
API 设计更简洁,使用方便。
读写性能优秀。
缺点 对于大数据量(如 10 万行以上),使用 SXSSFWorkbook 模式依然复杂,且容易 OOM。
API 相对繁琐。
功能相对 POI 较弱,不支持非常复杂的 Excel 特性(如 VBA 宏)。
社区规模小于 POI。
适用场景 - 需要生成复杂格式、模板的 Excel。
- 数据量不大或可控的场景。
- 大数据量导出(推荐首选)。
- 对内存敏感的场景。
- 普通的数据导出需求。
依赖 poi, poi-ooxml easyexcel
  • 新项目、数据量大强烈推荐使用 EasyExcel,它更简单、更高效。
  • 需要处理复杂模板、格式:可以考虑 Apache POI
  • 老项目,已经使用 POI 且无性能问题:可以继续使用 POI。

实战演练

下面我们分别用 EasyExcelApache POI 来实现一个简单的数据导出功能。

场景:导出一个用户列表

数据模型

// User.java
public class User {
    private String name;
    private Integer age;
    private String email;
    // 构造方法、Getter 和 Setter
    public User(String name, Integer age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

使用 EasyExcel (推荐)

EasyExcel 的核心优势在于其注解驱动低内存模型

添加 Maven 依赖

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

创建实体类并添加注解

为了方便 Excel 的列名映射,我们在 User 类上添加 EasyExcel 的注解。

Java Web如何实现Excel导出?-图2
(图片来源网络,侵删)
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
// @ColumnWidth 用于设置列宽,可选
@ColumnWidth(20)
public class User {
    // value = "姓名" 表示 Excel 表头
    @ExcelProperty("姓名")
    private String name;
    @ExcelProperty("年龄")
    private Integer age;
    @ExcelProperty("邮箱")
    private String email;
    // 构造方法、Getter 和 Setter...
    // 省略...
}

编写 Controller 或 Service 层代码

这是导出的核心逻辑。

import com.alibaba.excel.EasyExcel;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/excel")
public class ExcelExportController {
    @GetMapping("/export/easyexcel")
    public void exportEasyExcel(HttpServletResponse response) throws IOException {
        // 1. 准备数据
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            userList.add(new User("用户" + i, 20 + i, "user" + i + "@example.com"));
        }
        // 2. 设置响应头
        // 设置内容类型为 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        // 设置编码,防止中文乱码
        String fileName = URLEncoder.encode("用户列表_EasyExcel", "UTF-8");
        // 设置响应头,告诉浏览器这是一个附件,需要下载
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
        // 3. 写入 Excel
        // EasyExcel.write(response.getOutputStream(), User.class).sheet("用户数据").doWrite(userList);
        try {
            EasyExcel.write(response.getOutputStream(), User.class)
                     .sheet("用户列表") // sheet 名称
                     .doWrite(userList); // 写入数据
        } catch (Exception e) {
            // 处理异常,比如流关闭等
            e.printStackTrace();
        }
    }
}

代码解释

  • response.setContentType: 告诉浏览器文件类型是 .xlsx
  • URLEncoder.encode: 非常重要! 用于处理中文文件名,防止乱码。
  • response.setHeader("Content-disposition", "attachment;filename=..."): 这是触发下载的关键。
  • EasyExcel.write(...): 核心API,指定输出流、数据模型类。
  • .sheet("..."): 指定工作表的名称。
  • .doWrite(userList): 执行写入操作。

使用 Apache POI

POI 是传统方案,API 更底层,但功能更强大。

添加 Maven 依赖

<!-- POI 核心库 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.5</version> <!-- 建议使用最新版本 -->
</dependency>
<!-- POI 对应 Office 2007+ 格式的支持 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

编写 Controller 或 Service 层代码

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/excel")
public class ExcelExportController {
    @GetMapping("/export/poi")
    public void exportPoi(HttpServletResponse response) throws IOException {
        // 1. 准备数据
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            userList.add(new User("用户" + i, 20 + i, "user" + i + "@example.com"));
        }
        // 2. 创建 Excel 工作簿
        Workbook workbook = new XSSFWorkbook(); // 使用 XSSFWorkbook 生成 .xlsx 格式
        Sheet sheet = workbook.createSheet("用户列表_POI");
        // 3. 创建表头
        Row headerRow = sheet.createRow(0);
        headerRow.createCell(0).setCellValue("姓名");
        headerRow.createCell(1).setCellValue("年龄");
        headerRow.createCell(2).setCellValue("邮箱");
        // 4. 填充数据
        int rowNum = 1;
        for (User user : userList) {
            Row row = sheet.createRow(rowNum++);
            row.createCell(0).setCellValue(user.getName());
            row.createCell(1).setCellValue(user.getAge());
            row.createCell(2).setCellValue(user.getEmail());
        }
        // 5. 设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        String fileName = URLEncoder.encode("用户列表_POI", "UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
        // 6. 输出到响应流
        try {
            workbook.write(response.getOutputStream());
            workbook.close(); // 关闭工作簿,释放资源
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码解释

Java Web如何实现Excel导出?-图3
(图片来源网络,侵删)
  • XSSFWorkbook: 用于创建 .xlsx 格式的工作簿,如果是 .xls,则使用 HSSFWorkbook
  • workbook.createSheet(): 创建工作表。
  • sheet.createRow(): 创建行。
  • row.createCell(): 创建单元格并赋值。
  • workbook.write(): 将工作簿内容写入输出流。

高级技巧与常见问题

大数据量导出 (EasyExcel)

EasyExcel 的核心优势就在于此,它采用磁盘缓存的方式,避免将所有数据加载到内存中。

// 假设你有一个从数据库分页查询数据的方法
// public List<User> queryUsersByPage(int pageNum, int pageSize);
@GetMapping("/export/big-data")
public void exportBigData(HttpServletResponse response) throws IOException {
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    String fileName = URLEncoder.encode("大数据量列表", "UTF-8");
    response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
    // EasyExcel.write 会自动处理分页和流式写入
    EasyExcel.write(response.getOutputStream(), User.class)
             .sheet("大数据")
             .doWrite(() -> {
                 // 这里返回一个迭代器或 Lambda,EasyExcel 会循环调用它获取数据
                 // 模拟一个大数据源
                 List<User> allData = new ArrayList<>();
                 for (int i = 0; i < 100000; i++) { // 模拟10万条数据
                     allData.add(new User("BigUser" + i, 30, "big" + i + "@test.com"));
                 }
                 return allData; // 实际项目中,这里应该是分页查询数据库的逻辑
             });
}

最佳实践:不要一次性查出所有数据(SELECT * FROM users),而是在 Lambda 中实现分页查询逻辑,每次只查一页数据,返回给 EasyExcel,这样可以极大地降低内存峰值。

使用模板导出

有时,Excel 的格式(如合并单元格、固定标题、Logo、样式)是固定的,这时就需要使用模板。

EasyExcel 模板导出

  1. 准备一个 Excel 模板文件(template.xlsx),用 {{xxx}} 作为占位符。
  2. 将模板文件放在 resources 目录下。
  3. 在代码中读取模板并填充数据。
// 假设模板文件 template.xlsx 中有三个列,分别用 {{name}}, {{age}}, {{email}} 占位
@GetMapping("/export/template")
public void exportTemplate(HttpServletResponse response) throws IOException {
    // 准备数据
    List<User> userList = new ArrayList<>();
    // ... 省略数据填充
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    String fileName = URLEncoder.encode("模板导出", "UTF-8");
    response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
    // 从 classpath 加载模板
    ClassPathResource resource = new ClassPathResource("template.xlsx");
    // 使用模板进行写入
    EasyExcel.write(response.getOutputStream(), User.class)
             .withTemplate(resource.getInputStream())
             .sheet()
             .doWrite(userList);
}

常见问题:中文乱码

  • 文件名乱码:使用 URLEncoder.encode(fileName, "UTF-8") 进行编码。
  • Excel 内容乱码
    • 确保你的项目源文件编码是 UTF-8
    • 确保数据库中的数据编码是正确的。
    • 使用 EasyExcel 时,确保实体类的字段是 String 类型,如果数据库返回的是 Blob 或其他类型,需要先正确转换为 String

文件名含空格或特殊字符

URLEncoder.encode 会将空格编码为 ,这在某些浏览器上可能无法正确识别,可以手动替换掉 。

String fileName = URLEncoder.encode("用户 列表", "UTF-8").replaceAll("\\+", "%20");

特性 EasyExcel (推荐) Apache POI
上手难度 简单 中等
大数据量 优秀,内存占用低 差,容易 OOM
功能丰富度 良好,满足 90% 需求 卓越,支持所有复杂功能
API 风格 现代化,链式调用 传统,面向对象

对于绝大多数 Java Web 开发场景,EasyExcel 是目前更优的选择,它简单、高效,能解决大部分导出需求,尤其是在处理大数据量时优势明显,只有在遇到非常特殊的 Excel 格式要求时,才需要考虑使用功能更全面的 Apache POI。

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