Java实现Excel数据导出的实战指南

 更新时间:2026年06月05日 09:17:46   作者:霸道流氓气质  
Excel 导出是后台管理系统中的标配功能,用于将系统数据导出为 Excel 文件供用户下载、分析或归档,本文提供了一个完整的Excel数据导出实战指南,主要针对后台管理系统中的员工信息导出场景,有需要的小伙伴可以了解下

一、概述

Excel 导出是后台管理系统中的标配功能,用于将系统数据导出为 Excel 文件供用户下载、分析或归档。一个完善的导出功能需要考虑:查询条件过滤、数据补充(远程服务)、Excel 生成、文件上传、大数据量性能等环节。

本文以一个"员工信息导出"场景为例,系统介绍 Excel 导出的完整实现流程。

二、整体流程

  1. 前端提交导出请求(携带筛选条件)
  2. Controller 接收请求,委托 Service
  3. Service 查询数据(不分页,按条件查全部)
  4. 补充远程数据(Feign 调用)
  5. 枚举翻译、格式化
  6. 使用 POI 生成 Excel 文件
  7. 写入本地临时文件
  8. 上传到 OSS
  9. 删除临时文件
  10. 返回 OSS 下载 URL

三、技术选型

3.1 Excel 生成方式对比

方式文件格式最大行数内存占用适用场景
XSSFWorkbookpoi-ooxml.xlsx104万行高(全部在内存)数据量 < 5万
SXSSFWorkbookpoi-ooxml.xlsx104万行低(流式写入)数据量 5万~100万
HSSFWorkbookpoi.xls6.5万行兼容旧版 Excel
EasyExcelalibaba.xlsx104万行极低大数据量首选

3.2 文件交付方式对比

方式实现优点缺点适用场景
OSS 上传返回 URL生成文件 → 上传 → 返回链接不占用接口连接时间依赖 OSS 服务生产环境推荐
直接流式下载HttpServletResponse 输出流无需 OSS大文件可能超时小数据量/内网
异步导出 + 通知MQ 异步生成 → 通知用户下载不阻塞用户实现复杂超大数据量

四、完整实现

4.1 API 接口定义

@Tag(name = "员工管理")
@RestController
@RequestMapping("/api/page/employee")
public interface EmployeePageApi {

    @Operation(summary = "员工信息导出")
    @PostMapping("/export-employee")
    RestControllerResult<String> exportEmployee(
            @RequestBody EmployeeQueryParamsDto paramsDto);
}

4.2 Controller 实现

@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 实现

@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. 查询数据(不分页)
        List<EmployeeExportDto> dataList = employeeMapper.listEmployeeForExport(paramsDto);

        // 2. 数据补充(Feign 远程调用)
        if (dataList != null && !dataList.isEmpty()) {
            enrichData(dataList);
        }

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

    /**
     * 补充远程数据 + 枚举翻译.
     */
    private void enrichData(List<EmployeeExportDto> dataList) {
        dataList.forEach(dto -> {
            // 补充部门名称(Feign 调用)
            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());
                }
            }

            // 枚举翻译:状态
            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. 创建表头
            createHeader(workbook, sheet);

            // 2. 填充数据
            fillData(sheet, dataList);

            // 3. 设置列宽(可选)
            setColumnWidth(sheet);

            // 4. 写入临时文件
            file = writeToTempFile(workbook);

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

        } catch (Exception e) {
            log.error("导出Excel失败", e);
            throw new JshCheckException("导出失败,请稍后重试");
        } finally {
            // 6. 关闭资源
            closeWorkbook(workbook);
            // 7. 删除临时文件
            deleteTempFile(file);
        }

        return url;
    }

    /**
     * 创建表头.
     */
    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);

        // 表头内容
        String[] headers = {"工号", "姓名", "部门", "手机号", "状态", "操作人", "创建时间"};
        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);
        }
    }

    /**
     * 填充数据行.
     */
    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()) : "");
        }
    }

    /**
     * 设置列宽.
     */
    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);  // 状态
        sheet.setColumnWidth(5, 6000);  // 操作人
        sheet.setColumnWidth(6, 6000);  // 创建时间
    }

    /**
     * 写入临时文件.
     */
    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 = "员工信息导出-" + 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;
    }

    /**
     * 关闭 Workbook.
     */
    private void closeWorkbook(XSSFWorkbook workbook) {
        if (workbook != null) {
            try {
                workbook.close();
            } catch (IOException e) {
                log.warn("关闭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>

五、文件命名规范

5.1 命名规则

{业务名称}-{日期}{随机数}.xlsx

5.2 实现方式

SimpleDateFormat dateFmt = new SimpleDateFormat("yyyyMMdd");
String dateStr = dateFmt.format(new Date());
int randomNum = new Random().nextInt(9000) + 1000; // 4位随机数
String fileName = "员工信息导出-" + dateStr + randomNum + ".xlsx";
// 结果示例:员工信息导出-202605291234.xlsx

5.3 为什么加随机数

  • 避免同一秒内多次导出文件名冲突
  • OSS 上传时如果文件名相同会覆盖

六、大数据量导出优化

6.1 使用 SXSSFWorkbook(流式写入)

// XSSFWorkbook:所有数据在内存中,10万行可能占用 1GB+
// SXSSFWorkbook:只保留最近 N 行在内存中,其余写入临时文件

SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 内存中保留100行
Sheet sheet = workbook.createSheet("数据");

// 使用方式与 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 分页查询 + 流式写入

// 避免一次性加载全部数据到内存
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 数据量与方案选择

数据量推荐方案说明
< 1万XSSFWorkbook + 同步简单直接
1万~10万SXSSFWorkbook + 同步流式写入控制内存
10万~50万SXSSFWorkbook + 分页查询避免一次性加载
> 50万异步导出 + MQ + 通知避免接口超时

七、Feign 数据补充优化

7.1 逐条调用(简单但慢)

// 每条数据调用一次 Feign,N 条数据 = N 次网络请求
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 调用
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,单条失败不影响整体导出
dataList.forEach(dto -> {
    try {
        // Feign 调用
    } catch (Exception e) {
        log.warn("补充数据失败, id={}", dto.getId());
        // 该字段留空,不影响导出
    }
});

八、异常处理

异常场景处理方式
查询数据为空生成只有表头的空 Excel(或提示"无数据")
Feign 调用失败该字段留空,不影响导出
Excel 生成异常抛出业务异常"导出失败,请稍后重试"
OSS 上传失败抛出业务异常"文件上传失败"
临时文件删除失败仅记录 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("导出失败");
} 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 自动关闭
try (FileOutputStream fos = new FileOutputStream(file)) {
    workbook.write(fos);
}

十、导出与列表查询的关系

维度列表查询导出
分页有(pageNum/pageSize)无(查全部)
筛选条件相同相同
返回格式JSONExcel 文件 URL
Feign 补充当前页数据全部数据
SQL带分页参数不带分页参数

建议:Mapper 中定义两个方法,一个带分页(列表用),一个不带分页(导出用),SQL 条件部分可以用 <sql> 片段复用。

<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. 导出 SQL 不带分页:与列表查询共用筛选条件,但不限制条数
  2. Feign 补充加 try-catch:单条失败不影响整体导出
  3. 优先批量查询:有批量接口时一次性获取,避免 N+1
  4. 临时文件及时清理:finally 中删除,避免磁盘堆积
  5. Workbook 及时关闭:避免内存泄漏
  6. 文件名加随机数:避免并发导出时文件名冲突
  7. 大数据量用 SXSSFWorkbook:流式写入控制内存
  8. 设置合理列宽:提升用户体验
  9. 日期格式化:统一使用 yyyy-MM-dd HH:mm:ss
  10. 空数据处理:null 转空字符串,避免 Excel 中显示 “null”

到此这篇关于Java实现Excel数据导出的实战指南的文章就介绍到这了,更多相关Java Excel数据导出内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java中的静态代码块使用解读

    Java中的静态代码块使用解读

    本文将深入探讨静态代码块的工作原理、使用场景以及一些最佳实践,帮助你更好地理解和应用这一特性
    2025-02-02
  • Lucene词向量索引文件构建源码解析

    Lucene词向量索引文件构建源码解析

    这篇文章主要为大家介绍了Lucene词向量索引文件构建源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • IDEA连接Mysql数据库的详细图文教程

    IDEA连接Mysql数据库的详细图文教程

    项目开发时使用Intellij IDEA连接本地数据库,将数据库可视化,还可对数据库表直接进行增删改查操作,方便快捷又清晰,下面这篇文章主要给大家介绍了关于IDEA连接Mysql数据库的详细图文教程,需要的朋友可以参考下
    2023-03-03
  • Java中监听器Listener详解

    Java中监听器Listener详解

    Listener是由Java编写的WEB组件,主要完成对内置对象状态的变化 (创建、销毁)和属性的变化进行监听,做进一步的处理,主要对session和application内置对象监听,这篇文章主要介绍了Java中监听器Listener,需要的朋友可以参考下
    2023-08-08
  • Java中关于子类覆盖父类的抛出异常问题

    Java中关于子类覆盖父类的抛出异常问题

    今天小编就为大家分享一篇关于Java中关于子类覆盖父类的抛出异常问题,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-04-04
  • Java内存溢出实现原因及解决方案

    Java内存溢出实现原因及解决方案

    这篇文章主要介绍了Java内存溢出实现原因及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • MapReduce核心思想图文详解

    MapReduce核心思想图文详解

    今天小编就为大家分享一篇关于MapReduce核心思想图文详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • java Jersey框架初体验

    java Jersey框架初体验

    本篇主要是Jersey体验,你将在不做任何编码的情况下,体验Jersey框架的神气魅力!本文还假定你在eclipse里安装了Maven插件
    2016-07-07
  • Java 和 Javascript 的 Date 与 .Net 的 DateTime 之间的相互转换

    Java 和 Javascript 的 Date 与 .Net 的 DateTime 之间的相互转换

    这篇文章主要介绍了Java 和 Javascript 的 Date 与 .Net 的 DateTime 之间的相互转换的相关资料,非常不错具有参考借鉴价值,需要的朋友可以参考下
    2016-06-06
  • SpringBoot Jpa企业开发示例详细讲解

    SpringBoot Jpa企业开发示例详细讲解

    这篇文章主要介绍了SpringBoot Jpa企业开发示例,Jpa可以通过实体类生成数据库的表,同时自带很多增删改查方法,大部分sql语句不需要我们自己写,配置完成后直接调用方法即可,很方便
    2022-11-11

最新评论