Java+EasyExcel实现单个接口导出多个Excel的示例详解
一、核心问题与解决方案
在日常开发中,导出Excel是家常便饭,通常一个接口对应一个文件。但偶尔会遇到一些特殊场景——用户希望一次点击,就能同时拿到“用户列表”和“订单列表”这两份独立的数据报表。这该怎么实现呢?
免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈
这里有一个关键的技术前提需要明确:HTTP协议规定,单次响应只能返回一个字节流。这意味着,一个接口无法直接返回两个独立的文件。那么,如何绕过这个限制,满足用户“一次下载,多份文件”的需求呢?
答案其实很经典:打包。将多个Excel文件打包成一个ZIP压缩包,接口返回这个ZIP流。用户下载后解压,就能得到所有独立的文件。这个方案既完全遵守HTTP规范,又完美解决了业务需求,是业界通用且稳妥的做法。
二、前置准备:引入依赖
工欲善其事,必先利其器。实现这个功能,我们需要三个核心依赖:处理Excel读写的EasyExcel、负责文件打包的Apache Commons Compress,以及构建Web接口的Spring Boot Web Starter。以下是Ma ven配置,使用Gradle的朋友可以自行转换。版本号建议与示例保持一致,以避免潜在的兼容性问题。
com.alibaba easyexcel 4.0.3 org.apache.commons commons-compress 1.27.1 org.springframework.boot spring-boot-starter-web
三、步骤1:定义Excel对应的实体类
数据是Excel的灵魂,而实体类就是数据的蓝图。假设我们要导出用户和订单两份数据,就需要为它们分别创建实体类。通过EasyExcel的@ExcelProperty注解,可以轻松定义最终在Excel表头中显示的名称。这里使用Lombok的@Data注解来简化代码,省去手写getter/setter的麻烦。
3.1 用户实体类(UserData.ja va)
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.ja va)
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文件,都可以直接调用,代码复用率极高。
import com.alibaba.excel.EasyExcel;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import ja vax.servlet.ServletOutputStream;
import ja vax.servlet.http.HttpServletResponse;
import ja va.io.ByteArrayOutputStream;
import ja va.net.URLEncoder;
import ja va.util.List;
/**
* EasyExcel 多文件导出 ZIP 工具类(通用可复用)
*/
public class ExcelZipExportUtil {
/**
* 将单个 Excel 文件写入 ZIP 输出流
* @param zipOut ZIP 输出流
* @param excelFileName 单个 Excel 的文件名(如:用户列表.xlsx)
* @param data Excel 中的数据列表
* @param clazz Excel 对应的实体类(用于解析表头)
*/
public static void writeExcelToZip(ZipArchiveOutputStream zipOut, String excelFileName,
List data, Class 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的ZIP包。
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 ja vax.servlet.http.HttpServletResponse;
import ja va.util.ArrayList;
import ja va.util.List;
/**
* Excel 导出接口控制器
*/
@RestController
@RequestMapping("/export")
public class ExcelExportController {
/**
* 单个接口导出两个 Excel 文件(打包成 ZIP 下载)
* 访问地址:https://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 userList = new ArrayList<>();
// 3.1 完善数据逻辑省略
// 4. 构造第二个 Excel 的数据(订单列表,实际项目中替换为数据库查询)
List 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()->zipOut.finish()-> 关闭其他流的顺序操作。顺序一旦颠倒,生成的ZIP包很可能无法正常解压。 - 中文文件名乱码:通过
URLEncoder.encode(fileName, "UTF-8")对文件名进行编码,这是确保在Chrome、Firefox、Edge等所有主流浏览器中都能正确显示中文名的标准做法。 - Excel写入方式:示例中采用了
ByteArrayOutputStream在内存中生成Excel内容,避免了不必要的磁盘I/O操作,性能更优。不推荐先写入临时磁盘文件再打包的方式。 - 异常处理:示例中的
printStackTrace()仅用于演示。在生产环境中,务必使用全局异常处理器或更优雅的方式,向前端返回友好的错误信息,例如“导出失败,请稍后重试”,以提升用户体验。
七、扩展:导出更多Excel文件
这个方案的扩展性极佳。如果需要导出三个、四个甚至更多Excel文件,完全无需修改工具类,只需在接口中继续调用ExcelZipExportUtil.writeExcelToZip()方法即可。
例如,新增一个“商品列表”的导出:
// 新增商品列表数据(假设已有 GoodsData 实体类) ListgoodsList = new ArrayList<>(); // ... 构造商品数据 // 新增一个 Excel 写入 ZIP 流 ExcelZipExportUtil.writeExcelToZip(zipOut, "商品列表.xlsx", goodsList, GoodsData.class);
八、测试效果验证
代码完成后,可以通过以下几个步骤验证功能是否正常:
- 启动Spring Boot项目,访问接口地址:
https://localhost:8080/export/twoExcel(端口号请根据实际配置调整); - 浏览器会自动弹出下载对话框,文件名为“用户订单数据.zip”;
- 下载完成后解压ZIP包,应该能得到“用户列表.xlsx”和“订单列表.xlsx”两个文件;
- 打开Excel文件,核对表头和数据是否与代码中模拟的测试数据一致。
九、总结
回顾整个方案,其实可以清晰地归纳为三个步骤:
- 准备:引入必要的依赖,定义好数据实体类。
- 封装:构建通用的ZIP打包工具类,处理响应头设置和文件写入逻辑。
- 调用:在业务接口中组织数据,调用工具类生成并返回ZIP流。
该方案设计通用,可直接复用于任何Spring Boot项目,能够支持任意数量的Excel文件导出。同时,它已经规避了流关闭顺序、中文乱码等常见“坑点”。如果你在项目中遇到类似的多文件导出需求,不妨直接采用这套经过验证的代码方案。
相关攻略
告别手动排序:用RANK函数搞定Excel跨工作表排名 在日常工作和学习中,Excel的数据排名功能大家肯定不陌生。单张表格内的排名操作,对许多朋友来说已是轻车熟路。然而,当数据分散在多个工作表中,需要跨表进行综合排名时,是否感觉有点无从下手?别担心,今天我们就来彻底解决这个问题,掌握跨表排名的核心
Excel提效 No 047:一句话搞定数据排名与百分位 从此告别手动排序填排名,也无需再写复杂的RANK和PERCENTRANK公式。一句话,就能驱动整个数据排名与百分位的分析流程。 目录 你是否也遇到过这些问题 处理效果 1 前置准备 2 超简单AI自动化解决方案 第1步:准备好你的原始数据
目录 你是否也遇到过这些问题 | 处理效果 | 1 前置准备 | 2 超简单AI自动化解决方案 | 第1步:准备好你的原始数据 | 第2步:针对指定的文件下达指令 | 第3步:验收 | 还能解决这些同类问题 | 指令为什么这么有用? | 更多场景直接抄作业 | 1 按产品类别统计销售额 | 2
Sublime Text无法预览 xlsx文件,仅能通过配置和插件高效处理CSV:需手动绑定CSV语法、匹配编码、禁用大文件高亮,并谨慎使用AlignTab或CSVy对齐;超50MB、含换行符或需统计分析时应换用VS Code、csvkit或Python。 开门见山地说,Sublime Text 本
如何解决读取Excel表格的问题?使用Composer引入PhpSpreadsheet! 别再折腾PHPExcel了,手写fgetcsv去解析 xlsx文件更是条死胡同——它本质上根本不是CSV格式。眼下最稳妥、最一劳永逸的方案,就是通过Composer安装PhpSpreadsheet,并确保基础环
热门专题
热门推荐
红米Note 11 Pro系统升级,为何坚持要求连接Wi-Fi? 当红米Note 11 Pro收到MIUI或澎湃OS的系统更新推送时,官方总会明确提示:整个过程请在Wi-Fi网络环境下完成。这项要求并非随意设定,而是基于清晰的技术与体验考量。一次完整的系统升级包,其大小通常在2GB至4GB之间。如果
小米13 Ultra的NFC功能深度解析:它如何重新定义“全场景智能交互”? 在旗舰手机领域,NFC功能看似已成为标配,但体验却千差万别。小米13 Ultra所搭载的全功能NFC方案,在“全能”与“好用”两个维度上树立了新的标杆。它不仅无缝集成了公交卡模拟、门禁卡复制、数字车钥匙等核心生活服务,更全
嵌入式消毒柜电源插座安装指南:隐蔽式布局提升安全与美观 在规划嵌入式消毒柜的安装方案时,电源插座的布局方式直接影响到最终的整体效果与安全性。正确的做法是避免插座外露,采用隐蔽式安装。根据国家《住宅厨房设计规范》及主流厨电品牌的安装标准,推荐将插座预留在消毒柜后方或侧方的墙体内部,安装高度宜控制在距地
是的,魔音(Beats)耳机充电状态一目了然,指示灯明确显示 当你为Beats头戴式耳机充电时,如何判断它是否已经充满?答案就藏在机身自带的五段式LED电量指示灯里。在充电过程中,这排指示灯会持续闪烁,实时反馈充电进度。一旦所有五个指示灯全部转为稳定常亮、不再闪烁,即代表电池已完全充满。整个充电周期
博朗剃须刀型号全解析:从编码规则到选购技巧的终极指南 面对博朗剃须刀复杂的字母数字组合感到困惑?实际上,其型号命名体系逻辑严谨,是用户选购的核心依据。简单来说,型号首位的数字(1、3、5、7、9)直接代表产品系列,数字越大,通常意味着技术越先进、功能越全面、定位越高端。例如,顶级的9系旗舰机型普遍搭





