SpringBoot導出Excel的最佳實踐
引言:為什么我們需要封裝 Excel 導出?
在企業(yè)級后臺管理系統(tǒng)的開發(fā)中,“導出 Excel” 絕對是一個高頻且讓人又愛又恨的需求。
初入職場時,我們可能都會手寫原生的 Apache POI 代碼:創(chuàng)建 Workbook,創(chuàng)建 Sheet,寫死大標題,然后用兩層 for 循環(huán)把數據一行一行塞進單元格。
這種做法在前期看似簡單,但隨著業(yè)務線的發(fā)展,你會遇到以下幾個致命痛點:
- 重復勞動: 每次新增一個報表,都要復制粘貼幾百行高度相似的 POI 代碼,僅僅是改了改表頭和字段。
- 字典翻譯極其痛苦: 數據庫存的是
type = 1,導出時要變成“收入”;存的是時間戳,導出時要格式化。如果在 SQL 里連表翻譯,會拖慢查詢效率;如果在 Java 循環(huán)里寫if-else,代碼會變得又長又臭。 - 內存溢出(OOM)風險: 如果直接使用
XSSFWorkbook導出幾十萬條數據,內存瞬間打滿。即使用了基于磁盤流的SXSSFWorkbook,手動管理的邏輯依然繁雜。
為了解放生產力,我們必須對 Excel 導出進行通用化封裝。本文將為你提供業(yè)界最主流的兩套解決方案:無依賴侵入的 Java 8 函數式封裝,以及阿里開源神器 EasyExcel 的接入指南。
方案一:基于 Java 8 Function 的極致輕量級封裝
如果你不想引入龐大的第三方框架,或者項目要求嚴格控制依賴,那么原生的 poi-ooxml 配合 Java 8 的函數式接口(Function)是絕佳的選擇。
痛點突圍:如何避免使用反射?
很多傳統(tǒng)封裝喜歡用 Java 反射來讀取對象屬性,但這無法優(yōu)雅地解決“字典翻譯”和“復雜格式化”的問題。利用 Function<T, Object>,我們可以把“如何提取數據”和“如何轉換數據”的動作,作為參數傳給工具類。
核心工具類代碼ExcelExportUtil
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.List;
import java.util.function.Function;
/**
* 通用 Excel 導出工具類
*/
public class ExcelExportUtil {
/**
* 核心導出方法
* @param response HttpServletResponse
* @param fileName 文件名
* @param title 表格大標題(傳空則不生成)
* @param headers 表頭集合
* @param dataList 數據集合 List<T>
* @param extractors 數據提取規(guī)則集合,支持動態(tài)翻譯和格式化
*/
public static <T> void export(HttpServletResponse response,
String fileName,
String title,
List<String> headers,
List<T> dataList,
List<Function<T, Object>> extractors) throws Exception {
if (headers.size() != extractors.size()) {
throw new IllegalArgumentException("表頭列數與數據提取規(guī)則數量不一致!");
}
// 使用 SXSSFWorkbook 防 OOM,內存僅保留 100 行
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
Sheet sheet = workbook.createSheet("Sheet1");
int rowIndex = 0;
// 1. 生成大標題 (可選)
if (title != null && !title.isEmpty()) {
Row titleRow = sheet.createRow(rowIndex++);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue(title);
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, headers.size() - 1));
}
// 2. 生成表頭
Row headerRow = sheet.createRow(rowIndex++);
for (int i = 0; i < headers.size(); i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers.get(i));
sheet.setColumnWidth(i, 15 * 256); // 簡單統(tǒng)一列寬
}
// 3. 填充數據(核心亮點:使用 Function 動態(tài)取值并轉換)
for (T data : dataList) {
Row row = sheet.createRow(rowIndex++);
for (int i = 0; i < extractors.size(); i++) {
Cell cell = row.createCell(i);
// 執(zhí)行外部傳入的 Lambda 表達式
Object value = extractors.get(i).apply(data);
if (value != null) {
if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else {
cell.setCellValue(value.toString());
}
} else {
cell.setCellValue("");
}
}
}
// 4. 寫出響應流
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
try {
workbook.write(response.getOutputStream());
} finally {
workbook.dispose(); // 清理磁盤臨時文件
workbook.close();
}
}
}實戰(zhàn)調用:爽到飛起的 Controller
有了這個工具類,Controller 層的代碼變得極其清爽,甚至可以在一行代碼內完成復雜的字典翻譯和日期格式化。
@PostMapping("/exportFinanceRecord")
public void exportFinanceRecord(HttpServletResponse response, @RequestBody FinanceQuery param) throws Exception {
List<FinanceRecord> recordList = financeRecordService.list(param);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 定義表頭
List<String> headers = Arrays.asList("記錄ID", "收支類型", "金額", "創(chuàng)建時間");
// 定義數據提取與轉換規(guī)則(利用 Lambda 表達式)
List<Function<FinanceRecord, Object>> extractors = Arrays.asList(
FinanceRecord::getId,
record -> record.getType() == 1 ? "收入" : "支出", // 完美解決字典翻譯
FinanceRecord::getAmount,
record -> record.getCreateTime() != null ? sdf.format(record.getCreateTime()) : "" // 日期安全格式化
);
// 一行代碼調用導出
ExcelExportUtil.export(response, "財務收支記錄表", "財務明細大表", headers, recordList, extractors);
}
點評: 這種方案沒有引入任何第三方黑科技,可讀性極強,且沒有任何反射帶來的性能損耗,非常適合中小型項目。
方案二:擁抱阿里開源生態(tài),EasyExcel 的降維打擊
如果你的系統(tǒng)是一個復雜的后臺管理平臺,未來面臨著幾十上百個報表的導入導出,且動輒幾十萬級的數據量。別猶豫了,直接擁抱 Alibaba EasyExcel。
相比于原生的 POI,EasyExcel 就是一輛造好的跑車。它基于注解驅動,徹底重寫了 POI 對 07 版 Excel 的解析引擎,極大地降低了內存占用。
1. 定義專屬導出 VO
不要直接把數據庫的 Entity 拿去導出,專門建一個 View Object (VO),利用注解定義一切。
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
import java.util.Date;
@Data
public class FinanceRecordExportVO {
@ExcelProperty("記錄ID")
@ColumnWidth(12)
private Integer id;
// 翻譯邏輯可以在 Service 層查詢后處理,或者實現 EasyExcel 的 Converter
@ExcelProperty("收支類型")
@ColumnWidth(12)
private String typeStr;
@ExcelProperty("金額")
@ColumnWidth(15)
private Double amount;
@ExcelProperty("創(chuàng)建時間")
@ColumnWidth(22)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") // 自帶強大的格式化注解
private Date createTime;
}
2. 極致優(yōu)雅的 Controller
@PostMapping("/exportFinanceRecord")
public void exportFinanceRecord(HttpServletResponse response, @RequestBody FinanceQuery param) throws Exception {
// 1. 查詢數據并轉換為 VO 集合 (推薦使用 MapStruct 等工具)
List<FinanceRecordExportVO> voList = getAndConvertData(param);
// 2. 設置響應頭
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("財務收支記錄", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 3. 一行代碼完成寫入!
EasyExcel.write(response.getOutputStream(), FinanceRecordExportVO.class)
.sheet("數據明細")
.doWrite(voList);
}
點評: 從列寬、時間格式化到樣式控制,一切皆可通過注解完成。對于復雜的分頁分批寫入、復雜表頭,EasyExcel 都有著不可替代的優(yōu)勢。
避坑指南:引入 EasyExcel 的依賴沖突問題
如果你在項目中原本已經引入了 poi 或 poi-ooxml,當你滿心歡喜地加入 easyexcel 依賴后,運行項目時你大概率會遇到 NoSuchMethodError 或 ClassNotFoundException。
原因分析:
EasyExcel 的底層強依賴了特定版本的 POI。如果你原來的 pom.xml 中指定了較老的 POI 版本,就會覆蓋掉 EasyExcel 真正需要的版本,導致運行時崩潰。
最佳解決姿勢(直接“卸磨殺驢”):
既然 EasyExcel 已經包含了 POI,最清爽的做法就是直接把原有獨立的 POI 依賴全部刪除或注釋掉,全權交給 EasyExcel 管理。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version> </dependency>注:即使你移除了單獨的 POI 依賴,你依然可以在項目中使用原生的 POI API(如 SXSSFWorkbook),因為依賴已經被 EasyExcel 傳遞帶入,且版本絕對安全。
總結
- 場景選擇: 如果只是零星幾個簡單的表格導出,追求代碼零外部侵入,方案一(Java 8 Function) 絕對能讓你眼前一亮。
- 架構演進: 如果是長期維護、報表繁多、數據量大的企業(yè)級后臺,強烈建議使用方案二(EasyExcel)。它不僅僅是一個導出工具,更是處理海量數據的標準組件。
希望這篇文章能幫你告別痛苦的 Excel 導出代碼編寫,把時間留給更有價值的業(yè)務邏輯!
以上就是SpringBoot導出Excel的最佳實踐的詳細內容,更多關于SpringBoot導出Excel的資料請關注腳本之家其它相關文章!
相關文章
如何在Intellij中安裝LeetCode刷題插件方便Java刷題
這篇文章主要介紹了如何在Intellij中安裝LeetCode刷題插件方便Java刷題,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08

