Java實(shí)現(xiàn)Excel數(shù)據(jù)導(dǎo)出的實(shí)戰(zhàn)指南
一、概述
Excel 導(dǎo)出是后臺(tái)管理系統(tǒng)中的標(biāo)配功能,用于將系統(tǒng)數(shù)據(jù)導(dǎo)出為 Excel 文件供用戶(hù)下載、分析或歸檔。一個(gè)完善的導(dǎo)出功能需要考慮:查詢(xún)條件過(guò)濾、數(shù)據(jù)補(bǔ)充(遠(yuǎn)程服務(wù))、Excel 生成、文件上傳、大數(shù)據(jù)量性能等環(huán)節(jié)。
本文以一個(gè)"員工信息導(dǎo)出"場(chǎng)景為例,系統(tǒng)介紹 Excel 導(dǎo)出的完整實(shí)現(xiàn)流程。
二、整體流程
- 前端提交導(dǎo)出請(qǐng)求(攜帶篩選條件)
- Controller 接收請(qǐng)求,委托 Service
- Service 查詢(xún)數(shù)據(jù)(不分頁(yè),按條件查全部)
- 補(bǔ)充遠(yuǎn)程數(shù)據(jù)(Feign 調(diào)用)
- 枚舉翻譯、格式化
- 使用 POI 生成 Excel 文件
- 寫(xiě)入本地臨時(shí)文件
- 上傳到 OSS
- 刪除臨時(shí)文件
- 返回 OSS 下載 URL
三、技術(shù)選型
3.1 Excel 生成方式對(duì)比
| 方式 | 類(lèi) | 文件格式 | 最大行數(shù) | 內(nèi)存占用 | 適用場(chǎng)景 |
|---|---|---|---|---|---|
| XSSFWorkbook | poi-ooxml | .xlsx | 104萬(wàn)行 | 高(全部在內(nèi)存) | 數(shù)據(jù)量 < 5萬(wàn) |
| SXSSFWorkbook | poi-ooxml | .xlsx | 104萬(wàn)行 | 低(流式寫(xiě)入) | 數(shù)據(jù)量 5萬(wàn)~100萬(wàn) |
| HSSFWorkbook | poi | .xls | 6.5萬(wàn)行 | 中 | 兼容舊版 Excel |
| EasyExcel | alibaba | .xlsx | 104萬(wàn)行 | 極低 | 大數(shù)據(jù)量首選 |
3.2 文件交付方式對(duì)比
| 方式 | 實(shí)現(xiàn) | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|---|
| OSS 上傳返回 URL | 生成文件 → 上傳 → 返回鏈接 | 不占用接口連接時(shí)間 | 依賴(lài) OSS 服務(wù) | 生產(chǎn)環(huán)境推薦 |
| 直接流式下載 | HttpServletResponse 輸出流 | 無(wú)需 OSS | 大文件可能超時(shí) | 小數(shù)據(jù)量/內(nèi)網(wǎng) |
| 異步導(dǎo)出 + 通知 | MQ 異步生成 → 通知用戶(hù)下載 | 不阻塞用戶(hù) | 實(shí)現(xiàn)復(fù)雜 | 超大數(shù)據(jù)量 |
四、完整實(shí)現(xiàn)
4.1 API 接口定義
@Tag(name = "員工管理")
@RestController
@RequestMapping("/api/page/employee")
public interface EmployeePageApi {
@Operation(summary = "員工信息導(dǎo)出")
@PostMapping("/export-employee")
RestControllerResult<String> exportEmployee(
@RequestBody EmployeeQueryParamsDto paramsDto);
}
4.2 Controller 實(shí)現(xiàn)
@Slf4j
@RestController
public class EmployeePageApiController implements EmployeePageApi {
@Resource
private EmployeeService employeeService;
@Override
public RestControllerResult<String> exportEmployee(
@RequestBody EmployeeQueryParamsDto paramsDto) {
RestControllerResult<String> result = new RestControllerResult<>();
result.setSuccess(Boolean.TRUE);
result.setData(employeeService.exportEmployee(paramsDto));
return result;
}
}
4.3 Service 實(shí)現(xiàn)
@Slf4j
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Resource
private EmployeeMapper employeeMapper;
@Resource
private DepartmentFeign departmentFeign;
@Resource
private AliOssTemplate aliOssTemplate;
@Override
public String exportEmployee(EmployeeQueryParamsDto paramsDto) {
// 1. 查詢(xún)數(shù)據(jù)(不分頁(yè))
List<EmployeeExportDto> dataList = employeeMapper.listEmployeeForExport(paramsDto);
// 2. 數(shù)據(jù)補(bǔ)充(Feign 遠(yuǎn)程調(diào)用)
if (dataList != null && !dataList.isEmpty()) {
enrichData(dataList);
}
// 3. 生成 Excel 并上傳 OSS
return generateAndUploadExcel(dataList);
}
/**
* 補(bǔ)充遠(yuǎn)程數(shù)據(jù) + 枚舉翻譯.
*/
private void enrichData(List<EmployeeExportDto> dataList) {
dataList.forEach(dto -> {
// 補(bǔ)充部門(mén)名稱(chēng)(Feign 調(diào)用)
if (StringUtils.isNotBlank(dto.getDeptCode())) {
try {
RestControllerResult<DeptInfoDto> deptResult =
departmentFeign.getDeptByCode(dto.getDeptCode());
if (deptResult != null && Boolean.TRUE.equals(deptResult.getSuccess())
&& deptResult.getData() != null) {
dto.setDeptName(deptResult.getData().getDeptName());
}
} catch (Exception e) {
log.warn("查詢(xún)部門(mén)信息失敗, deptCode={}", dto.getDeptCode());
}
}
// 枚舉翻譯:狀態(tài)
if (dto.getStatus() != null) {
dto.setStatusName(EmployeeStatusEnum.getNameByCode(dto.getStatus()));
}
// 格式化:操作人
if (StringUtils.isNotBlank(dto.getStaffNo())
&& StringUtils.isNotBlank(dto.getOperatorName())) {
dto.setOperatorDisplay(dto.getStaffNo() + " " + dto.getOperatorName());
}
});
}
/**
* 生成 Excel 文件并上傳 OSS.
*/
private String generateAndUploadExcel(List<EmployeeExportDto> dataList) {
String url = "";
XSSFWorkbook workbook = null;
File file = null;
try {
workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("員工信息");
// 1. 創(chuàng)建表頭
createHeader(workbook, sheet);
// 2. 填充數(shù)據(jù)
fillData(sheet, dataList);
// 3. 設(shè)置列寬(可選)
setColumnWidth(sheet);
// 4. 寫(xiě)入臨時(shí)文件
file = writeToTempFile(workbook);
// 5. 上傳 OSS
url = aliOssTemplate.uploadFile(file);
} catch (Exception e) {
log.error("導(dǎo)出Excel失敗", e);
throw new JshCheckException("導(dǎo)出失敗,請(qǐng)稍后重試");
} finally {
// 6. 關(guān)閉資源
closeWorkbook(workbook);
// 7. 刪除臨時(shí)文件
deleteTempFile(file);
}
return url;
}
/**
* 創(chuàng)建表頭.
*/
private void createHeader(XSSFWorkbook workbook, Sheet sheet) {
// 表頭樣式
Font fontStyle = workbook.createFont();
fontStyle.setBold(true);
fontStyle.setFontHeightInPoints((short) 11);
CellStyle headerStyle = workbook.createCellStyle();
headerStyle.setFont(fontStyle);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
// 表頭內(nèi)容
String[] headers = {"工號(hào)", "姓名", "部門(mén)", "手機(jī)號(hào)", "狀態(tài)", "操作人", "創(chuàng)建時(shí)間"};
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
}
/**
* 填充數(shù)據(jù)行.
*/
private void fillData(Sheet sheet, List<EmployeeExportDto> dataList) {
if (dataList == null || dataList.isEmpty()) {
return;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < dataList.size(); i++) {
EmployeeExportDto dto = dataList.get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(nullToEmpty(dto.getStaffNo()));
row.createCell(1).setCellValue(nullToEmpty(dto.getStaffName()));
row.createCell(2).setCellValue(nullToEmpty(dto.getDeptName()));
row.createCell(3).setCellValue(nullToEmpty(dto.getPhone()));
row.createCell(4).setCellValue(nullToEmpty(dto.getStatusName()));
row.createCell(5).setCellValue(nullToEmpty(dto.getOperatorDisplay()));
row.createCell(6).setCellValue(
dto.getCreateTime() != null ? sdf.format(dto.getCreateTime()) : "");
}
}
/**
* 設(shè)置列寬.
*/
private void setColumnWidth(Sheet sheet) {
sheet.setColumnWidth(0, 4000); // 工號(hào)
sheet.setColumnWidth(1, 4000); // 姓名
sheet.setColumnWidth(2, 6000); // 部門(mén)
sheet.setColumnWidth(3, 5000); // 手機(jī)號(hào)
sheet.setColumnWidth(4, 3000); // 狀態(tài)
sheet.setColumnWidth(5, 6000); // 操作人
sheet.setColumnWidth(6, 6000); // 創(chuàng)建時(shí)間
}
/**
* 寫(xiě)入臨時(shí)文件.
*/
private File writeToTempFile(XSSFWorkbook workbook) throws IOException {
SimpleDateFormat dateFmt = new SimpleDateFormat("yyyyMMdd");
String dateStr = dateFmt.format(new Date());
int randomNum = new Random().nextInt(9000) + 1000;
String fileName = "員工信息導(dǎo)出-" + dateStr + randomNum + ".xlsx";
String tempPath = System.getProperty("java.io.tmpdir") + File.separator + fileName;
File file = new File(tempPath);
try (FileOutputStream fos = new FileOutputStream(file)) {
workbook.write(fos);
}
return file;
}
/**
* 關(guān)閉 Workbook.
*/
private void closeWorkbook(XSSFWorkbook workbook) {
if (workbook != null) {
try {
workbook.close();
} catch (IOException e) {
log.warn("關(guān)閉Workbook失敗", e);
}
}
}
/**
* 刪除臨時(shí)文件.
*/
private void deleteTempFile(File file) {
if (file != null && file.exists()) {
try {
file.delete();
} catch (Exception e) {
log.warn("刪除臨時(shí)文件失敗: {}", file.getAbsolutePath());
}
}
}
private String nullToEmpty(String value) {
return value != null ? value : "";
}
}
4.4 Mapper(不分頁(yè)查詢(xún))
public interface EmployeeMapper {
List<EmployeeExportDto> listEmployeeForExport(
@Param("param") EmployeeQueryParamsDto param);
}
<select id="listEmployeeForExport"
resultType="com.example.dto.EmployeeExportDto">
SELECT
e.staff_no AS staffNo,
e.staff_name AS staffName,
e.dept_code AS deptCode,
e.phone AS phone,
e.status AS status,
e.create_time AS createTime,
e.create_user_id AS createUserId
FROM employee e
<where>
<if test="param.staffName != null and param.staffName != ''">
AND e.staff_name LIKE CONCAT('%', #{param.staffName}, '%')
</if>
<if test="param.deptCode != null and param.deptCode != ''">
AND e.dept_code = #{param.deptCode}
</if>
<if test="param.status != null">
AND e.status = #{param.status}
</if>
<if test="param.createTimeStart != null and param.createTimeStart != ''">
AND e.create_time >= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null and param.createTimeEnd != ''">
AND e.create_time <= #{param.createTimeEnd}
</if>
</where>
ORDER BY e.id DESC
</select>
五、文件命名規(guī)范
5.1 命名規(guī)則
{業(yè)務(wù)名稱(chēng)}-{日期}{隨機(jī)數(shù)}.xlsx
5.2 實(shí)現(xiàn)方式
SimpleDateFormat dateFmt = new SimpleDateFormat("yyyyMMdd");
String dateStr = dateFmt.format(new Date());
int randomNum = new Random().nextInt(9000) + 1000; // 4位隨機(jī)數(shù)
String fileName = "員工信息導(dǎo)出-" + dateStr + randomNum + ".xlsx";
// 結(jié)果示例:?jiǎn)T工信息導(dǎo)出-202605291234.xlsx
5.3 為什么加隨機(jī)數(shù)
- 避免同一秒內(nèi)多次導(dǎo)出文件名沖突
- OSS 上傳時(shí)如果文件名相同會(huì)覆蓋
六、大數(shù)據(jù)量導(dǎo)出優(yōu)化
6.1 使用 SXSSFWorkbook(流式寫(xiě)入)
// XSSFWorkbook:所有數(shù)據(jù)在內(nèi)存中,10萬(wàn)行可能占用 1GB+
// SXSSFWorkbook:只保留最近 N 行在內(nèi)存中,其余寫(xiě)入臨時(shí)文件
SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 內(nèi)存中保留100行
Sheet sheet = workbook.createSheet("數(shù)據(jù)");
// 使用方式與 XSSFWorkbook 完全一致
for (int i = 0; i < dataList.size(); i++) {
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(dataList.get(i).getName());
}
// 寫(xiě)入文件
workbook.write(outputStream);
workbook.dispose(); // 清理臨時(shí)文件(SXSSFWorkbook 特有)
6.2 分頁(yè)查詢(xún) + 流式寫(xiě)入
// 避免一次性加載全部數(shù)據(jù)到內(nèi)存
int pageSize = 5000;
int pageNum = 1;
int rowIndex = 1;
while (true) {
List<EmployeeExportDto> pageData = employeeMapper.listEmployeePager(
paramsDto, pageSize, pageNum);
if (pageData == null || pageData.isEmpty()) {
break;
}
for (EmployeeExportDto dto : pageData) {
Row row = sheet.createRow(rowIndex++);
fillRow(row, dto);
}
pageNum++;
}
6.3 數(shù)據(jù)量與方案選擇
| 數(shù)據(jù)量 | 推薦方案 | 說(shuō)明 |
|---|---|---|
| < 1萬(wàn) | XSSFWorkbook + 同步 | 簡(jiǎn)單直接 |
| 1萬(wàn)~10萬(wàn) | SXSSFWorkbook + 同步 | 流式寫(xiě)入控制內(nèi)存 |
| 10萬(wàn)~50萬(wàn) | SXSSFWorkbook + 分頁(yè)查詢(xún) | 避免一次性加載 |
| > 50萬(wàn) | 異步導(dǎo)出 + MQ + 通知 | 避免接口超時(shí) |
七、Feign 數(shù)據(jù)補(bǔ)充優(yōu)化
7.1 逐條調(diào)用(簡(jiǎn)單但慢)
// 每條數(shù)據(jù)調(diào)用一次 Feign,N 條數(shù)據(jù) = N 次網(wǎng)絡(luò)請(qǐng)求
dataList.forEach(dto -> {
DeptInfoDto dept = departmentFeign.getDeptByCode(dto.getDeptCode());
dto.setDeptName(dept.getDeptName());
});
7.2 批量查詢(xún) + Map 映射(推薦)
// 收集所有部門(mén)編碼,一次性查詢(xún)
List<String> deptCodes = dataList.stream()
.map(EmployeeExportDto::getDeptCode)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
// 一次 Feign 調(diào)用
Map<String, String> deptMap = departmentFeign.batchGetDeptNames(deptCodes);
// 遍歷賦值
dataList.forEach(dto -> {
if (deptMap.containsKey(dto.getDeptCode())) {
dto.setDeptName(deptMap.get(dto.getDeptCode()));
}
});
7.3 無(wú)批量接口時(shí)的折中
// 加 try-catch,單條失敗不影響整體導(dǎo)出
dataList.forEach(dto -> {
try {
// Feign 調(diào)用
} catch (Exception e) {
log.warn("補(bǔ)充數(shù)據(jù)失敗, id={}", dto.getId());
// 該字段留空,不影響導(dǎo)出
}
});
八、異常處理
| 異常場(chǎng)景 | 處理方式 |
|---|---|
| 查詢(xún)數(shù)據(jù)為空 | 生成只有表頭的空 Excel(或提示"無(wú)數(shù)據(jù)") |
| Feign 調(diào)用失敗 | 該字段留空,不影響導(dǎo)出 |
| Excel 生成異常 | 拋出業(yè)務(wù)異常"導(dǎo)出失敗,請(qǐng)稍后重試" |
| OSS 上傳失敗 | 拋出業(yè)務(wù)異常"文件上傳失敗" |
| 臨時(shí)文件刪除失敗 | 僅記錄 warn 日志,不影響返回 |
九、資源管理
9.1 try-finally 模式
XSSFWorkbook workbook = null;
File file = null;
try {
workbook = new XSSFWorkbook();
// ... 生成 Excel ...
file = writeToTempFile(workbook);
url = aliOssTemplate.uploadFile(file);
} catch (Exception e) {
throw new JshCheckException("導(dǎo)出失敗");
} finally {
// 確保資源釋放
if (workbook != null) {
try { workbook.close(); } catch (Exception ignored) {}
}
if (file != null && file.exists()) {
file.delete();
}
}
9.2 try-with-resources(FileOutputStream)
// FileOutputStream 用 try-with-resources 自動(dòng)關(guān)閉
try (FileOutputStream fos = new FileOutputStream(file)) {
workbook.write(fos);
}
十、導(dǎo)出與列表查詢(xún)的關(guān)系
| 維度 | 列表查詢(xún) | 導(dǎo)出 |
|---|---|---|
| 分頁(yè) | 有(pageNum/pageSize) | 無(wú)(查全部) |
| 篩選條件 | 相同 | 相同 |
| 返回格式 | JSON | Excel 文件 URL |
| Feign 補(bǔ)充 | 當(dāng)前頁(yè)數(shù)據(jù) | 全部數(shù)據(jù) |
| SQL | 帶分頁(yè)參數(shù) | 不帶分頁(yè)參數(shù) |
建議:Mapper 中定義兩個(gè)方法,一個(gè)帶分頁(yè)(列表用),一個(gè)不帶分頁(yè)(導(dǎo)出用),SQL 條件部分可以用 <sql> 片段復(fù)用。
<sql id="queryCondition">
<if test="param.staffName != null and param.staffName != ''">
AND e.staff_name LIKE CONCAT('%', #{param.staffName}, '%')
</if>
<!-- 其他條件 -->
</sql>
<select id="listEmployeePager">
SELECT ... FROM employee e
<where><include refid="queryCondition"/></where>
ORDER BY e.id DESC
</select>
<select id="listEmployeeForExport">
SELECT ... FROM employee e
<where><include refid="queryCondition"/></where>
ORDER BY e.id DESC
</select>十一、最佳實(shí)踐清單
- 導(dǎo)出 SQL 不帶分頁(yè):與列表查詢(xún)共用篩選條件,但不限制條數(shù)
- Feign 補(bǔ)充加 try-catch:?jiǎn)螚l失敗不影響整體導(dǎo)出
- 優(yōu)先批量查詢(xún):有批量接口時(shí)一次性獲取,避免 N+1
- 臨時(shí)文件及時(shí)清理:finally 中刪除,避免磁盤(pán)堆積
- Workbook 及時(shí)關(guān)閉:避免內(nèi)存泄漏
- 文件名加隨機(jī)數(shù):避免并發(fā)導(dǎo)出時(shí)文件名沖突
- 大數(shù)據(jù)量用 SXSSFWorkbook:流式寫(xiě)入控制內(nèi)存
- 設(shè)置合理列寬:提升用戶(hù)體驗(yàn)
- 日期格式化:統(tǒng)一使用 yyyy-MM-dd HH:mm:ss
- 空數(shù)據(jù)處理:null 轉(zhuǎn)空字符串,避免 Excel 中顯示 “null”
到此這篇關(guān)于Java實(shí)現(xiàn)Excel數(shù)據(jù)導(dǎo)出的實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)Java Excel數(shù)據(jù)導(dǎo)出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java中大量數(shù)據(jù)Excel導(dǎo)入導(dǎo)出的完整實(shí)現(xiàn)過(guò)程
- Java使用EasyExcel實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)導(dǎo)出的最佳實(shí)踐指南
- Java實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)導(dǎo)出Excel的詳細(xì)指南
- Java編寫(xiě)通用的導(dǎo)出任何對(duì)象列表數(shù)據(jù)到excel的工具類(lèi)
- 使用Java實(shí)現(xiàn)百萬(wàn)Excel數(shù)據(jù)導(dǎo)出
- Java表數(shù)據(jù)導(dǎo)出到Excel中的實(shí)現(xiàn)
相關(guān)文章
mybatis升級(jí)mybatis-plus時(shí)踩到的一些坑
這篇文章主要給大家介紹了關(guān)于mybatis升級(jí)mybatis-plus時(shí)踩到的一些坑,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
深入學(xué)習(xí)Hibernate持久化對(duì)象的三個(gè)狀態(tài)
Hibernate中的對(duì)象有3中狀態(tài),瞬時(shí)對(duì)象(TransientObjects)、持久化對(duì)象(PersistentObjects)和離線對(duì)象(DetachedObjects也叫做脫管對(duì)象),下面通過(guò)本文給大家分享Hibernate持久化對(duì)象的三個(gè)狀態(tài),一起看看吧2017-09-09
java?Comparable和Comparator的區(qū)別及作用面試精講
這篇文章主要為大家介紹了java?Comparable和Comparator的區(qū)別及作用面試精講,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
一文徹底搞懂Spring?Boot中properties和YAML的區(qū)別
properties和yml都是Spring Boot支持的兩種配置文件,其中yml格式的配置文件可以看做是對(duì)properties配置文件的升級(jí),這篇文章主要介紹了Spring?Boot中properties和YAML區(qū)別的相關(guān)資料,需要的朋友可以參考下2026-04-04
SpringBoot使用Sharding-JDBC實(shí)現(xiàn)數(shù)據(jù)分片和讀寫(xiě)分離的方法
本文主要介紹了SpringBoot使用Sharding-JDBC實(shí)現(xiàn)數(shù)據(jù)分片和讀寫(xiě)分離,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
Druid連接池實(shí)現(xiàn)自定義數(shù)據(jù)庫(kù)密碼加解密功能
在現(xiàn)代應(yīng)用開(kāi)發(fā)中,數(shù)據(jù)安全是至關(guān)重要的,本文將介紹如何在???Druid???連接池中實(shí)現(xiàn)自定義的數(shù)據(jù)庫(kù)密碼加解密功能,有需要的小伙伴可以參考一下2025-05-05
Java音頻處理之音頻流轉(zhuǎn)音頻文件和獲取音頻播放時(shí)長(zhǎng)詳解
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)音頻流轉(zhuǎn)音頻文件和獲取音頻播放時(shí)長(zhǎng)功能,文中的示例代碼簡(jiǎn)潔易懂,有需要的小伙伴可以了解下2025-07-07

