在日常开发中,我们经常会遇到 Excel 导出的需求,大多是单个接口导出单个 Excel 文件。但偶尔也会有特殊场景——需要一个接口同时导出两个(或多个)独立的 Excel 文件,比如同时导出“用户列表”和“订单列表”,方便用户一次性获取完整数据。
今天就基于 Spring Boot + EasyExcel(目前主流稳定版本),分享一种简单、通用、可直接落地的实现方案,全程附完整代码,新手也能快速上手~
一、核心问题与解决方案
首先要明确一个关键前提:HTTP 协议单次响应只能返回一个字节流,无法直接返回两个独立的 Excel 文件(相当于一次请求只能下载一个文件)。
那怎么实现“一个接口导出多个 Excel”?答案很简单——将多个 Excel 文件打包成 ZIP 压缩包,接口返回 ZIP 流,用户下载后解压,就能得到多个 Excel 文件。这是最通用、最合规的解决方案,既不违背 HTTP 协议,也能满足用户一次性获取多份文件的需求。
二、前置准备:引入依赖
首先在项目中引入 EasyExcel 核心依赖、ZIP 压缩依赖(用于打包多文件),以及 Spring Web 依赖(接口开发必备)。以下是 Maven 配置,Gradle 可对应转换,版本可根据自己项目需求微调(建议保持和示例一致,避免兼容问题)。
<!-- EasyExcel 核心依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.3</version>
</dependency>
<!-- ZIP 压缩依赖(处理多文件打包) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.27.1</version>
</dependency>
<!-- Spring Web 依赖(已有则忽略,接口开发必备) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>三、步骤1:定义Excel对应的实体类
假设我们需要导出两个 Excel 文件:用户列表 和 订单列表,分别创建对应的实体类,通过 EasyExcel 的 @ExcelProperty 注解指定 Excel 表头名称(注解参数就是最终 Excel 中显示的表头)。
实体类用 Lombok 的 @Data 注解简化 getter/setter 代码,无需手动编写,节省开发时间。
3.1 用户实体类(UserData.java)
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 用户列表 Excel 对应的实体类
*/
@Data
public class UserData {
// Excel 表头:用户ID
@ExcelProperty("用户ID")
private Long userId;
// Excel 表头:用户名称
@ExcelProperty("用户名称")
private String userName;
// Excel 表头:手机号
@ExcelProperty("手机号")
private String phone;
}3.2 订单实体类(OrderData.java)
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 订单列表 Excel 对应的实体类
*/
@Data
public class OrderData {
// Excel 表头:订单ID
@ExcelProperty("订单ID")
private String orderId;
// Excel 表头:用户ID(关联用户表)
@ExcelProperty("用户ID")
private Long userId;
// Excel 表头:订单金额
@ExcelProperty("订单金额")
private Double amount;
// Excel 表头:创建时间
@ExcelProperty("创建时间")
private String createTime;
}四、步骤2:封装通用工具类(核心)
为了避免接口层代码冗余,我们封装一个工具类 ExcelZipExportUtil,专门处理“将多个 Excel 写入 ZIP 流”和“初始化 HTTP 响应头”的逻辑。这个工具类是通用的,后续不管导出多少个 Excel,都能直接复用,无需重复编码。
import com.alibaba.excel.EasyExcel;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.net.URLEncoder;
import java.util.List;
/**
* EasyExcel 多文件导出 ZIP 工具类(通用可复用)
*/
public class ExcelZipExportUtil {
/**
* 将单个 Excel 文件写入 ZIP 输出流
* @param zipOut ZIP 输出流
* @param excelFileName 单个 Excel 的文件名(如:用户列表.xlsx)
* @param data Excel 中的数据列表
* @param clazz Excel 对应的实体类(用于解析表头)
*/
public static <T> void writeExcelToZip(ZipArchiveOutputStream zipOut, String excelFileName,
List<T> data, Class<T> clazz) throws Exception {
// 1. 临时存储 Excel 内容(内存级,不写入磁盘,性能更高)
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 2. 使用 EasyExcel 写入数据(sheet1 是工作表名称,可自定义)
EasyExcel.write(bos, clazz)
// 不要自动关闭,交给 Servlet 自己处理
.autoCloseStream(false)
// 基于 column 长度,自动适配。最大 255 宽度
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// 避免 Long 类型丢失精度
.registerConverter(new LongStringConverter())
// 工作表名称
.sheet("sheet1")
.doWrite(data);
// 3. 将 Excel 作为 ZIP 的一个条目写入
zipOut.putArchiveEntry(new ZipArchiveEntry(excelFileName));
zipOut.write(bos.toByteArray());
zipOut.closeArchiveEntry(); // 关闭当前 ZIP 条目(必须,否则后续条目无法写入)
// 4. 关闭临时流
bos.close();
}
/**
* 初始化 HTTP 响应头(设置 ZIP 下载、解决中文文件名乱码)
* @param response 响应对象
* @param zipFileName 最终下载的 ZIP 压缩包名称(如:用户订单数据.zip)
*/
public static void initZipResponse(HttpServletResponse response, String zipFileName) throws Exception {
// 设置响应类型为 ZIP
response.setContentType("application/zip");
// 设置下载头,URLEncoder.encode 解决中文文件名乱码(兼容所有浏览器)
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(zipFileName, "UTF-8"));
// 禁止缓存(避免浏览器缓存旧文件)
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
}
}五、步骤3:接口层实现(最终落地)
创建 Controller 接口,模拟构造两个 Excel 的测试数据(实际项目中,这里可以替换成从数据库查询数据),然后调用上面封装的工具类,将两个 Excel 打包成 ZIP 流,通过 HttpServletResponse 返回,实现“一次请求下载两个 Excel”。
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
/**
* Excel 导出接口控制器
*/
@RestController
@RequestMapping("/export")
public class ExcelExportController {
/**
* 单个接口导出两个 Excel 文件(打包成 ZIP 下载)
* 访问地址:http://localhost:8080/export/twoExcel
*/
@GetMapping("/twoExcel")
public void exportTwoExcel(HttpServletResponse response) {
try {
// 1. 初始化响应头,设置 ZIP 压缩包名称(用户下载时显示的文件名)
ExcelZipExportUtil.initZipResponse(response, "用户订单数据.zip");
// 2. 获取 HTTP 响应输出流,关联 ZIP 输出流
ServletOutputStream servletOut = response.getOutputStream();
ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(servletOut);
// 3. 构造第一个 Excel 的数据(用户列表,实际项目中替换为数据库查询)
List<UserData> userList = new ArrayList<>();
// 3.1 完善数据逻辑省略
// 4. 构造第二个 Excel 的数据(订单列表,实际项目中替换为数据库查询)
List<OrderData> orderList = new ArrayList<>();
// 4.1 完善数据逻辑省略
// 5. 关键操作:将两个 Excel 分别写入 ZIP 流
ExcelZipExportUtil.writeExcelToZip(zipOut, "用户列表.xlsx", userList, UserData.class);
ExcelZipExportUtil.writeExcelToZip(zipOut, "订单列表.xlsx", orderList, OrderData.class);
// 6. 关闭流(顺序不能错!否则 ZIP 包会损坏,无法解压)
zipOut.finish(); // 完成 ZIP 写入
zipOut.close();
servletOut.flush();
servletOut.close();
} catch (Exception e) {
e.printStackTrace();
// 实际项目中建议自定义异常处理,给前端返回明确的错误提示
response.setStatus(500);
}
}
}六、关键注意事项(避坑重点)
这部分一定要仔细看!很多新手实现后,出现 ZIP 包损坏、中文乱码、数据缺失等问题,大多是因为忽略了这些细节。
- 流的关闭顺序:必须先执行 zipOut.closeArchiveEntry()(关闭当前 ZIP 条目),再执行 zipOut.finish(),最后关闭 ZIP 流和响应流。顺序颠倒会导致 ZIP 包损坏,无法解压。
- 中文文件名乱码:通过 URLEncoder.encode(zipFileName, "UTF-8") 对 ZIP 文件名和 Excel 文件名编码,兼容 Chrome、Firefox、Edge 等所有主流浏览器,避免中文乱码。
- Excel 写入方式:示例中使用 ByteArrayOutputStream 临时存储 Excel 内容,属于内存级写入,不占用磁盘空间,性能更高。不建议直接写入磁盘文件再打包,会增加磁盘 IO 开销。
- 异常处理:实际项目中,建议替换 try-catch 中的 printStackTrace(),使用全局异常处理器,给前端返回明确的错误信息(如“导出失败,请重试”),提升用户体验。
七、扩展:导出更多Excel文件
如果需要导出 3 个、4 个甚至更多 Excel 文件,无需修改工具类,只需在接口中继续调用 ExcelZipExportUtil.writeExcelToZip() 方法即可。
示例(新增“商品列表”Excel):
// 新增商品列表数据(假设已有 GoodsData 实体类)
List<GoodsData> goodsList = new ArrayList<>();
// ... 构造商品数据
// 新增一个 Excel 写入 ZIP 流
ExcelZipExportUtil.writeExcelToZip(zipOut, "商品列表.xlsx", goodsList, GoodsData.class);八、测试效果验证
代码写完后,启动 Spring Boot 项目,通过以下步骤测试,确保导出正常:
- 访问接口地址:http://localhost:8080/export/twoExcel(端口号根据自己项目配置调整);
- 浏览器会自动弹出下载提示,下载的文件名为“用户订单数据.zip”;
- 解压 ZIP 压缩包,会得到两个 Excel 文件:用户列表.xlsx 和 订单列表.xlsx;
- 打开 Excel 文件,检查表头和数据是否正常(和接口中构造的测试数据一致)。
九、总结
代码通用可复用,总结下来就是 3 步:
- 引入 EasyExcel 和 ZIP 依赖,做好前置准备;
- 定义 Excel 对应的实体类,封装通用工具类(处理 ZIP 打包和响应头);
- 在接口中构造数据,调用工具类将多个 Excel 写入 ZIP 流,返回给前端。
该方案适用于所有 Spring Boot 项目,支持任意数量的 Excel 导出,避开了新手常踩的坑(流顺序、中文乱码),直接复制代码就能落地使用。如果你的项目中有类似需求,不妨试试这个方案~
评论 (0)