最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java實現(xiàn)Excel數(shù)據(jù)導(dǎo)出的實戰(zhàn)指南

 更新時間:2026年06月05日 09:17:46   作者:霸道流氓氣質(zhì)  
Excel 導(dǎo)出是后臺管理系統(tǒng)中的標(biāo)配功能,用于將系統(tǒng)數(shù)據(jù)導(dǎo)出為 Excel 文件供用戶下載、分析或歸檔,本文提供了一個完整的Excel數(shù)據(jù)導(dǎo)出實戰(zhàn)指南,主要針對后臺管理系統(tǒng)中的員工信息導(dǎo)出場景,有需要的小伙伴可以了解下

一、概述

Excel 導(dǎo)出是后臺管理系統(tǒng)中的標(biāo)配功能,用于將系統(tǒng)數(shù)據(jù)導(dǎo)出為 Excel 文件供用戶下載、分析或歸檔。一個完善的導(dǎo)出功能需要考慮:查詢條件過濾、數(shù)據(jù)補充(遠(yuǎn)程服務(wù))、Excel 生成、文件上傳、大數(shù)據(jù)量性能等環(huán)節(jié)。

本文以一個"員工信息導(dǎo)出"場景為例,系統(tǒng)介紹 Excel 導(dǎo)出的完整實現(xiàn)流程。

二、整體流程

  1. 前端提交導(dǎo)出請求(攜帶篩選條件)
  2. Controller 接收請求,委托 Service
  3. Service 查詢數(shù)據(jù)(不分頁,按條件查全部)
  4. 補充遠(yuǎn)程數(shù)據(jù)(Feign 調(diào)用)
  5. 枚舉翻譯、格式化
  6. 使用 POI 生成 Excel 文件
  7. 寫入本地臨時文件
  8. 上傳到 OSS
  9. 刪除臨時文件
  10. 返回 OSS 下載 URL

三、技術(shù)選型

3.1 Excel 生成方式對比

方式文件格式最大行數(shù)內(nèi)存占用適用場景
XSSFWorkbookpoi-ooxml.xlsx104萬行高(全部在內(nèi)存)數(shù)據(jù)量 < 5萬
SXSSFWorkbookpoi-ooxml.xlsx104萬行低(流式寫入)數(shù)據(jù)量 5萬~100萬
HSSFWorkbookpoi.xls6.5萬行兼容舊版 Excel
EasyExcelalibaba.xlsx104萬行極低大數(shù)據(jù)量首選

3.2 文件交付方式對比

方式實現(xiàn)優(yōu)點缺點適用場景
OSS 上傳返回 URL生成文件 → 上傳 → 返回鏈接不占用接口連接時間依賴 OSS 服務(wù)生產(chǎn)環(huán)境推薦
直接流式下載HttpServletResponse 輸出流無需 OSS大文件可能超時小數(shù)據(jù)量/內(nèi)網(wǎng)
異步導(dǎo)出 + 通知MQ 異步生成 → 通知用戶下載不阻塞用戶實現(xiàn)復(fù)雜超大數(shù)據(jù)量

四、完整實現(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 實現(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 實現(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. 查詢數(shù)據(jù)(不分頁)
        List<EmployeeExportDto> dataList = employeeMapper.listEmployeeForExport(paramsDto);

        // 2. 數(shù)據(jù)補充(Feign 遠(yuǎn)程調(diào)用)
        if (dataList != null && !dataList.isEmpty()) {
            enrichData(dataList);
        }

        // 3. 生成 Excel 并上傳 OSS
        return generateAndUploadExcel(dataList);
    }

    /**
     * 補充遠(yuǎn)程數(shù)據(jù) + 枚舉翻譯.
     */
    private void enrichData(List<EmployeeExportDto> dataList) {
        dataList.forEach(dto -> {
            // 補充部門名稱(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("查詢部門信息失敗, 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. 寫入臨時文件
            file = writeToTempFile(workbook);

            // 5. 上傳 OSS
            url = aliOssTemplate.uploadFile(file);

        } catch (Exception e) {
            log.error("導(dǎo)出Excel失敗", e);
            throw new JshCheckException("導(dǎo)出失敗,請稍后重試");
        } finally {
            // 6. 關(guān)閉資源
            closeWorkbook(workbook);
            // 7. 刪除臨時文件
            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 = {"工號", "姓名", "部門", "手機號", "狀態(tài)", "操作人", "創(chuàng)建時間"};
        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);  // 工號
        sheet.setColumnWidth(1, 4000);  // 姓名
        sheet.setColumnWidth(2, 6000);  // 部門
        sheet.setColumnWidth(3, 5000);  // 手機號
        sheet.setColumnWidth(4, 3000);  // 狀態(tài)
        sheet.setColumnWidth(5, 6000);  // 操作人
        sheet.setColumnWidth(6, 6000);  // 創(chuàng)建時間
    }

    /**
     * 寫入臨時文件.
     */
    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);
            }
        }
    }

    /**
     * 刪除臨時文件.
     */
    private void deleteTempFile(File file) {
        if (file != null && file.exists()) {
            try {
                file.delete();
            } catch (Exception e) {
                log.warn("刪除臨時文件失敗: {}", file.getAbsolutePath());
            }
        }
    }

    private String nullToEmpty(String value) {
        return value != null ? value : "";
    }
}

4.4 Mapper(不分頁查詢)

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 &gt;= #{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ù)名稱}-{日期}{隨機數(shù)}.xlsx

5.2 實現(xiàn)方式

SimpleDateFormat dateFmt = new SimpleDateFormat("yyyyMMdd");
String dateStr = dateFmt.format(new Date());
int randomNum = new Random().nextInt(9000) + 1000; // 4位隨機數(shù)
String fileName = "員工信息導(dǎo)出-" + dateStr + randomNum + ".xlsx";
// 結(jié)果示例:員工信息導(dǎo)出-202605291234.xlsx

5.3 為什么加隨機數(shù)

  • 避免同一秒內(nèi)多次導(dǎo)出文件名沖突
  • OSS 上傳時如果文件名相同會覆蓋

六、大數(shù)據(jù)量導(dǎo)出優(yōu)化

6.1 使用 SXSSFWorkbook(流式寫入)

// XSSFWorkbook:所有數(shù)據(jù)在內(nèi)存中,10萬行可能占用 1GB+
// SXSSFWorkbook:只保留最近 N 行在內(nèi)存中,其余寫入臨時文件

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());
}

// 寫入文件
workbook.write(outputStream);
workbook.dispose(); // 清理臨時文件(SXSSFWorkbook 特有)

6.2 分頁查詢 + 流式寫入

// 避免一次性加載全部數(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ù)量推薦方案說明
< 1萬XSSFWorkbook + 同步簡單直接
1萬~10萬SXSSFWorkbook + 同步流式寫入控制內(nèi)存
10萬~50萬SXSSFWorkbook + 分頁查詢避免一次性加載
> 50萬異步導(dǎo)出 + MQ + 通知避免接口超時

七、Feign 數(shù)據(jù)補充優(yōu)化

7.1 逐條調(diào)用(簡單但慢)

// 每條數(shù)據(jù)調(diào)用一次 Feign,N 條數(shù)據(jù) = N 次網(wǎng)絡(luò)請求
dataList.forEach(dto -> {
    DeptInfoDto dept = departmentFeign.getDeptByCode(dto.getDeptCode());
    dto.setDeptName(dept.getDeptName());
});

7.2 批量查詢 + Map 映射(推薦)

// 收集所有部門編碼,一次性查詢
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 無批量接口時的折中

// 加 try-catch,單條失敗不影響整體導(dǎo)出
dataList.forEach(dto -> {
    try {
        // Feign 調(diào)用
    } catch (Exception e) {
        log.warn("補充數(shù)據(jù)失敗, id={}", dto.getId());
        // 該字段留空,不影響導(dǎo)出
    }
});

八、異常處理

異常場景處理方式
查詢數(shù)據(jù)為空生成只有表頭的空 Excel(或提示"無數(shù)據(jù)")
Feign 調(diào)用失敗該字段留空,不影響導(dǎo)出
Excel 生成異常拋出業(yè)務(wù)異常"導(dǎo)出失敗,請稍后重試"
OSS 上傳失敗拋出業(yè)務(wù)異常"文件上傳失敗"
臨時文件刪除失敗僅記錄 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 自動關(guān)閉
try (FileOutputStream fos = new FileOutputStream(file)) {
    workbook.write(fos);
}

十、導(dǎo)出與列表查詢的關(guān)系

維度列表查詢導(dǎo)出
分頁有(pageNum/pageSize)無(查全部)
篩選條件相同相同
返回格式JSONExcel 文件 URL
Feign 補充當(dāng)前頁數(shù)據(jù)全部數(shù)據(jù)
SQL帶分頁參數(shù)不帶分頁參數(shù)

建議:Mapper 中定義兩個方法,一個帶分頁(列表用),一個不帶分頁(導(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>

十一、最佳實踐清單

  1. 導(dǎo)出 SQL 不帶分頁:與列表查詢共用篩選條件,但不限制條數(shù)
  2. Feign 補充加 try-catch:單條失敗不影響整體導(dǎo)出
  3. 優(yōu)先批量查詢:有批量接口時一次性獲取,避免 N+1
  4. 臨時文件及時清理:finally 中刪除,避免磁盤堆積
  5. Workbook 及時關(guān)閉:避免內(nèi)存泄漏
  6. 文件名加隨機數(shù):避免并發(fā)導(dǎo)出時文件名沖突
  7. 大數(shù)據(jù)量用 SXSSFWorkbook:流式寫入控制內(nèi)存
  8. 設(shè)置合理列寬:提升用戶體驗
  9. 日期格式化:統(tǒng)一使用 yyyy-MM-dd HH:mm:ss
  10. 空數(shù)據(jù)處理:null 轉(zhuǎn)空字符串,避免 Excel 中顯示 “null”

到此這篇關(guān)于Java實現(xiàn)Excel數(shù)據(jù)導(dǎo)出的實戰(zhàn)指南的文章就介紹到這了,更多相關(guān)Java Excel數(shù)據(jù)導(dǎo)出內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

六枝特区| 潞西市| 绍兴县| 衡山县| 旌德县| 拉孜县| 克什克腾旗| 湘阴县| 图木舒克市| 肇庆市| 德安县| 海丰县| 同仁县| 宽甸| 抚顺市| 义马市| 南川市| 隆化县| 托克托县| 土默特右旗| 浦城县| 招远市| 毕节市| 天柱县| 渭南市| 莎车县| 吉木乃县| 黑河市| 巴青县| 巩义市| 建宁县| 安溪县| 邯郸市| 彭山县| 湘潭市| 江西省| 兴城市| 宜兰县| 泸州市| 横峰县| 大港区|