Java+EasyExcel實(shí)現(xiàn)單個(gè)接口導(dǎo)出多個(gè)Excel的示例詳解
在日常開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到 Excel 導(dǎo)出的需求,大多是單個(gè)接口導(dǎo)出單個(gè) Excel 文件。但偶爾也會(huì)有特殊場(chǎng)景——需要一個(gè)接口同時(shí)導(dǎo)出兩個(gè)(或多個(gè))獨(dú)立的 Excel 文件,比如同時(shí)導(dǎo)出“用戶列表”和“訂單列表”,方便用戶一次性獲取完整數(shù)據(jù)。
今天就基于 Spring Boot + EasyExcel(目前主流穩(wěn)定版本),分享一種簡(jiǎn)單、通用、可直接落地的實(shí)現(xiàn)方案,全程附完整代碼,新手也能快速上手~
一、核心問(wèn)題與解決方案
首先要明確一個(gè)關(guān)鍵前提:HTTP 協(xié)議單次響應(yīng)只能返回一個(gè)字節(jié)流,無(wú)法直接返回兩個(gè)獨(dú)立的 Excel 文件(相當(dāng)于一次請(qǐng)求只能下載一個(gè)文件)。
那怎么實(shí)現(xiàn)“一個(gè)接口導(dǎo)出多個(gè) Excel”?答案很簡(jiǎn)單——將多個(gè) Excel 文件打包成 ZIP 壓縮包,接口返回 ZIP 流,用戶下載后解壓,就能得到多個(gè) Excel 文件。這是最通用、最合規(guī)的解決方案,既不違背 HTTP 協(xié)議,也能滿足用戶一次性獲取多份文件的需求。
二、前置準(zhǔn)備:引入依賴
首先在項(xiàng)目中引入 EasyExcel 核心依賴、ZIP 壓縮依賴(用于打包多文件),以及 Spring Web 依賴(接口開(kāi)發(fā)必備)。以下是 Maven 配置,Gradle 可對(duì)應(yīng)轉(zhuǎn)換,版本可根據(jù)自己項(xiàng)目需求微調(diào)(建議保持和示例一致,避免兼容問(wèn)題)。
<!-- 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 依賴(已有則忽略,接口開(kāi)發(fā)必備) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>三、步驟1:定義Excel對(duì)應(yīng)的實(shí)體類
假設(shè)我們需要導(dǎo)出兩個(gè) Excel 文件:用戶列表 和 訂單列表,分別創(chuàng)建對(duì)應(yīng)的實(shí)體類,通過(guò) EasyExcel 的 @ExcelProperty 注解指定 Excel 表頭名稱(注解參數(shù)就是最終 Excel 中顯示的表頭)。
實(shí)體類用 Lombok 的 @Data 注解簡(jiǎn)化 getter/setter 代碼,無(wú)需手動(dòng)編寫(xiě),節(jié)省開(kāi)發(fā)時(shí)間。
3.1 用戶實(shí)體類(UserData.java)
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 用戶列表 Excel 對(duì)應(yīng)的實(shí)體類
*/
@Data
public class UserData {
// Excel 表頭:用戶ID
@ExcelProperty("用戶ID")
private Long userId;
// Excel 表頭:用戶名稱
@ExcelProperty("用戶名稱")
private String userName;
// Excel 表頭:手機(jī)號(hào)
@ExcelProperty("手機(jī)號(hào)")
private String phone;
}
3.2 訂單實(shí)體類(OrderData.java)
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 訂單列表 Excel 對(duì)應(yīng)的實(shí)體類
*/
@Data
public class OrderData {
// Excel 表頭:訂單ID
@ExcelProperty("訂單ID")
private String orderId;
// Excel 表頭:用戶ID(關(guān)聯(lián)用戶表)
@ExcelProperty("用戶ID")
private Long userId;
// Excel 表頭:訂單金額
@ExcelProperty("訂單金額")
private Double amount;
// Excel 表頭:創(chuàng)建時(shí)間
@ExcelProperty("創(chuàng)建時(shí)間")
private String createTime;
}
四、步驟2:封裝通用工具類(核心)
為了避免接口層代碼冗余,我們封裝一個(gè)工具類 ExcelZipExportUtil,專門(mén)處理“將多個(gè) Excel 寫(xiě)入 ZIP 流”和“初始化 HTTP 響應(yīng)頭”的邏輯。這個(gè)工具類是通用的,后續(xù)不管導(dǎo)出多少個(gè) Excel,都能直接復(fù)用,無(wú)需重復(fù)編碼。
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 多文件導(dǎo)出 ZIP 工具類(通用可復(fù)用)
*/
public class ExcelZipExportUtil {
/**
* 將單個(gè) Excel 文件寫(xiě)入 ZIP 輸出流
* @param zipOut ZIP 輸出流
* @param excelFileName 單個(gè) Excel 的文件名(如:用戶列表.xlsx)
* @param data Excel 中的數(shù)據(jù)列表
* @param clazz Excel 對(duì)應(yīng)的實(shí)體類(用于解析表頭)
*/
public static <T> void writeExcelToZip(ZipArchiveOutputStream zipOut, String excelFileName,
List<T> data, Class<T> clazz) throws Exception {
// 1. 臨時(shí)存儲(chǔ) Excel 內(nèi)容(內(nèi)存級(jí),不寫(xiě)入磁盤(pán),性能更高)
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 2. 使用 EasyExcel 寫(xiě)入數(shù)據(jù)(sheet1 是工作表名稱,可自定義)
EasyExcel.write(bos, clazz)
// 不要自動(dòng)關(guān)閉,交給 Servlet 自己處理
.autoCloseStream(false)
// 基于 column 長(zhǎng)度,自動(dòng)適配。最大 255 寬度
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// 避免 Long 類型丟失精度
.registerConverter(new LongStringConverter())
// 工作表名稱
.sheet("sheet1")
.doWrite(data);
// 3. 將 Excel 作為 ZIP 的一個(gè)條目寫(xiě)入
zipOut.putArchiveEntry(new ZipArchiveEntry(excelFileName));
zipOut.write(bos.toByteArray());
zipOut.closeArchiveEntry(); // 關(guān)閉當(dāng)前 ZIP 條目(必須,否則后續(xù)條目無(wú)法寫(xiě)入)
// 4. 關(guān)閉臨時(shí)流
bos.close();
}
/**
* 初始化 HTTP 響應(yīng)頭(設(shè)置 ZIP 下載、解決中文文件名亂碼)
* @param response 響應(yīng)對(duì)象
* @param zipFileName 最終下載的 ZIP 壓縮包名稱(如:用戶訂單數(shù)據(jù).zip)
*/
public static void initZipResponse(HttpServletResponse response, String zipFileName) throws Exception {
// 設(shè)置響應(yīng)類型為 ZIP
response.setContentType("application/zip");
// 設(shè)置下載頭,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:接口層實(shí)現(xiàn)(最終落地)
創(chuàng)建 Controller 接口,模擬構(gòu)造兩個(gè) Excel 的測(cè)試數(shù)據(jù)(實(shí)際項(xiàng)目中,這里可以替換成從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)),然后調(diào)用上面封裝的工具類,將兩個(gè) Excel 打包成 ZIP 流,通過(guò) HttpServletResponse 返回,實(shí)現(xiàn)“一次請(qǐng)求下載兩個(gè) 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 導(dǎo)出接口控制器
*/
@RestController
@RequestMapping("/export")
public class ExcelExportController {
/**
* 單個(gè)接口導(dǎo)出兩個(gè) Excel 文件(打包成 ZIP 下載)
* 訪問(wèn)地址:http://localhost:8080/export/twoExcel
*/
@GetMapping("/twoExcel")
public void exportTwoExcel(HttpServletResponse response) {
try {
// 1. 初始化響應(yīng)頭,設(shè)置 ZIP 壓縮包名稱(用戶下載時(shí)顯示的文件名)
ExcelZipExportUtil.initZipResponse(response, "用戶訂單數(shù)據(jù).zip");
// 2. 獲取 HTTP 響應(yīng)輸出流,關(guān)聯(lián) ZIP 輸出流
ServletOutputStream servletOut = response.getOutputStream();
ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(servletOut);
// 3. 構(gòu)造第一個(gè) Excel 的數(shù)據(jù)(用戶列表,實(shí)際項(xiàng)目中替換為數(shù)據(jù)庫(kù)查詢)
List<UserData> userList = new ArrayList<>();
// 3.1 完善數(shù)據(jù)邏輯省略
// 4. 構(gòu)造第二個(gè) Excel 的數(shù)據(jù)(訂單列表,實(shí)際項(xiàng)目中替換為數(shù)據(jù)庫(kù)查詢)
List<OrderData> orderList = new ArrayList<>();
// 4.1 完善數(shù)據(jù)邏輯省略
// 5. 關(guān)鍵操作:將兩個(gè) Excel 分別寫(xiě)入 ZIP 流
ExcelZipExportUtil.writeExcelToZip(zipOut, "用戶列表.xlsx", userList, UserData.class);
ExcelZipExportUtil.writeExcelToZip(zipOut, "訂單列表.xlsx", orderList, OrderData.class);
// 6. 關(guān)閉流(順序不能錯(cuò)!否則 ZIP 包會(huì)損壞,無(wú)法解壓)
zipOut.finish(); // 完成 ZIP 寫(xiě)入
zipOut.close();
servletOut.flush();
servletOut.close();
} catch (Exception e) {
e.printStackTrace();
// 實(shí)際項(xiàng)目中建議自定義異常處理,給前端返回明確的錯(cuò)誤提示
response.setStatus(500);
}
}
}
六、關(guān)鍵注意事項(xiàng)(避坑重點(diǎn))
這部分一定要仔細(xì)看!很多新手實(shí)現(xiàn)后,出現(xiàn) ZIP 包損壞、中文亂碼、數(shù)據(jù)缺失等問(wèn)題,大多是因?yàn)楹雎粤诉@些細(xì)節(jié)。
- 流的關(guān)閉順序:必須先執(zhí)行 zipOut.closeArchiveEntry()(關(guān)閉當(dāng)前 ZIP 條目),再執(zhí)行 zipOut.finish(),最后關(guān)閉 ZIP 流和響應(yīng)流。順序顛倒會(huì)導(dǎo)致 ZIP 包損壞,無(wú)法解壓。
- 中文文件名亂碼:通過(guò) URLEncoder.encode(zipFileName, "UTF-8") 對(duì) ZIP 文件名和 Excel 文件名編碼,兼容 Chrome、Firefox、Edge 等所有主流瀏覽器,避免中文亂碼。
- Excel 寫(xiě)入方式:示例中使用 ByteArrayOutputStream 臨時(shí)存儲(chǔ) Excel 內(nèi)容,屬于內(nèi)存級(jí)寫(xiě)入,不占用磁盤(pán)空間,性能更高。不建議直接寫(xiě)入磁盤(pán)文件再打包,會(huì)增加磁盤(pán) IO 開(kāi)銷。
- 異常處理:實(shí)際項(xiàng)目中,建議替換 try-catch 中的 printStackTrace(),使用全局異常處理器,給前端返回明確的錯(cuò)誤信息(如“導(dǎo)出失敗,請(qǐng)重試”),提升用戶體驗(yàn)。
七、擴(kuò)展:導(dǎo)出更多Excel文件
如果需要導(dǎo)出 3 個(gè)、4 個(gè)甚至更多 Excel 文件,無(wú)需修改工具類,只需在接口中繼續(xù)調(diào)用 ExcelZipExportUtil.writeExcelToZip() 方法即可。
示例(新增“商品列表”Excel):
// 新增商品列表數(shù)據(jù)(假設(shè)已有 GoodsData 實(shí)體類) List<GoodsData> goodsList = new ArrayList<>(); // ... 構(gòu)造商品數(shù)據(jù) // 新增一個(gè) Excel 寫(xiě)入 ZIP 流 ExcelZipExportUtil.writeExcelToZip(zipOut, "商品列表.xlsx", goodsList, GoodsData.class);
八、測(cè)試效果驗(yàn)證
代碼寫(xiě)完后,啟動(dòng) Spring Boot 項(xiàng)目,通過(guò)以下步驟測(cè)試,確保導(dǎo)出正常:
- 訪問(wèn)接口地址:
http://localhost:8080/export/twoExcel(端口號(hào)根據(jù)自己項(xiàng)目配置調(diào)整); - 瀏覽器會(huì)自動(dòng)彈出下載提示,下載的文件名為“用戶訂單數(shù)據(jù).zip”;
- 解壓 ZIP 壓縮包,會(huì)得到兩個(gè) Excel 文件:用戶列表.xlsx 和 訂單列表.xlsx;
- 打開(kāi) Excel 文件,檢查表頭和數(shù)據(jù)是否正常(和接口中構(gòu)造的測(cè)試數(shù)據(jù)一致)。
九、總結(jié)
代碼通用可復(fù)用,總結(jié)下來(lái)就是 3 步:
- 引入 EasyExcel 和 ZIP 依賴,做好前置準(zhǔn)備;
- 定義 Excel 對(duì)應(yīng)的實(shí)體類,封裝通用工具類(處理 ZIP 打包和響應(yīng)頭);
- 在接口中構(gòu)造數(shù)據(jù),調(diào)用工具類將多個(gè) Excel 寫(xiě)入 ZIP 流,返回給前端。
該方案適用于所有 Spring Boot 項(xiàng)目,支持任意數(shù)量的 Excel 導(dǎo)出,避開(kāi)了新手常踩的坑(流順序、中文亂碼),直接復(fù)制代碼就能落地使用。如果你的項(xiàng)目中有類似需求,不妨試試這個(gè)方案~
到此這篇關(guān)于Java+EasyExcel實(shí)現(xiàn)單個(gè)接口導(dǎo)出多個(gè)Excel的示例詳解的文章就介紹到這了,更多相關(guān)Java EasyExcel導(dǎo)出多個(gè)Excel內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java使用EasyExcel實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)導(dǎo)出的最佳實(shí)踐指南
- Java使用EasyExcel導(dǎo)出Excel的全流程詳解
- Java如何利用Easyexcel動(dòng)態(tài)導(dǎo)出表頭列
- java使用EasyExcel導(dǎo)出上萬(wàn)數(shù)據(jù)如何避免OOM
- Java?EasyExcel導(dǎo)出合并單元格的示例詳解
- Java使用EasyExcel實(shí)現(xiàn)Excel的導(dǎo)入導(dǎo)出
- Java利用EasyExcel實(shí)現(xiàn)導(dǎo)出導(dǎo)入功能的示例代碼
相關(guān)文章
Springboot應(yīng)用中Mybatis輸出SQL日志的3種方法代碼示例
在前臺(tái)請(qǐng)求數(shù)據(jù)的時(shí)候,sql語(yǔ)句一直都是打印到控制臺(tái)的,有一個(gè)想法就是想讓它打印到日志里,該如何做呢?這篇文章主要給大家介紹了關(guān)于Springboot應(yīng)用中Mybatis輸出SQL日志的3種方法,需要的朋友可以參考下2024-01-01
Java?實(shí)戰(zhàn)項(xiàng)目之學(xué)生信息管理系統(tǒng)的實(shí)現(xiàn)流程
讀萬(wàn)卷書(shū)不如行萬(wàn)里路,只學(xué)書(shū)上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),大家可以在過(guò)程中查缺補(bǔ)漏,提升水平2021-11-11
Java的基礎(chǔ)語(yǔ)法學(xué)習(xí)筆記
這里為大家整理了Java的基礎(chǔ)語(yǔ)法學(xué)習(xí)筆記,包括關(guān)鍵詞、運(yùn)算符與基本的流程控制語(yǔ)句寫(xiě)法等,需要的朋友可以參考下2016-05-05
Spring boot中自定義Json參數(shù)解析器的方法
這篇文章主要介紹了Spring boot中自定義Json參數(shù)解析器的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
JDBC以反射機(jī)制加載類注冊(cè)驅(qū)動(dòng)連接MySQL
這篇文章介紹了JDBC以反射機(jī)制加載類注冊(cè)驅(qū)動(dòng)連接MySQL的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01
Java如何設(shè)置過(guò)期時(shí)間的map的幾種方法
本文主要介紹了Java如何設(shè)置過(guò)期時(shí)間的map的幾種方法,常見(jiàn)的解決方法有:ExpiringMap、LoadingCache及基于HashMap的封裝三種,下面就詳細(xì)的介紹一下,感興趣的可以了解下2022-03-03

