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

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

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

一、概述

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)流程。

二、整體流程

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

三、技術(shù)選型

3.1 Excel 生成方式對(duì)比

方式類(lèi)文件格式最大行數(shù)內(nèi)存占用適用場(chǎng)景
XSSFWorkbookpoi-ooxml.xlsx104萬(wàn)行高(全部在內(nèi)存)數(shù)據(jù)量 < 5萬(wàn)
SXSSFWorkbookpoi-ooxml.xlsx104萬(wàn)行低(流式寫(xiě)入)數(shù)據(jù)量 5萬(wàn)~100萬(wàn)
HSSFWorkbookpoi.xls6.5萬(wàn)行兼容舊版 Excel
EasyExcelalibaba.xlsx104萬(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 &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ù)名稱(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ú)(查全部)
篩選條件相同相同
返回格式JSONExcel 文件 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í)踐清單

  1. 導(dǎo)出 SQL 不帶分頁(yè):與列表查詢(xún)共用篩選條件,但不限制條數(shù)
  2. Feign 補(bǔ)充加 try-catch:?jiǎn)螚l失敗不影響整體導(dǎo)出
  3. 優(yōu)先批量查詢(xún):有批量接口時(shí)一次性獲取,避免 N+1
  4. 臨時(shí)文件及時(shí)清理:finally 中刪除,避免磁盤(pán)堆積
  5. Workbook 及時(shí)關(guān)閉:避免內(nèi)存泄漏
  6. 文件名加隨機(jī)數(shù):避免并發(fā)導(dǎo)出時(shí)文件名沖突
  7. 大數(shù)據(jù)量用 SXSSFWorkbook:流式寫(xiě)入控制內(nèi)存
  8. 設(shè)置合理列寬:提升用戶(hù)體驗(yàn)
  9. 日期格式化:統(tǒng)一使用 yyyy-MM-dd HH:mm:ss
  10. 空數(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)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

灵丘县| 长宁县| 岑巩县| 聂荣县| 遵义县| 阳西县| 泸溪县| 黑山县| 江津市| 库伦旗| 古丈县| 沙河市| 沂源县| 德江县| 浦东新区| 冕宁县| 满洲里市| 平邑县| 马龙县| 麻阳| 休宁县| 崇阳县| 武邑县| 吉林省| 仙居县| 桐乡市| 商城县| 石首市| 自治县| 雷波县| 融水| 呼图壁县| 娱乐| 合作市| 望谟县| 万州区| 湖南省| 都昌县| 牙克石市| 页游| 嵩明县|