Spring Boot 集成 EasyExcel 的最佳实践:优雅实现 Excel 导入导出

发布于:2025-09-15 ⋅ 阅读:(25) ⋅ 点赞:(0)

1. 引入

在日常开发中,Excel 的导入和导出几乎是后台系统的标配功能。阿里巴巴的 EasyExcel 是当前主流的 Excel 处理框架,性能优异、使用简单。但在和 Spring Boot 集成时,如果处理不当,容易出现一些典型问题,比如:

  • 导出时报错 Can not close IO

  • 上传导入时文件流未关闭,导致文件句柄泄露

  • 异常响应与二进制流冲突

本文总结了一个通用的 ExcelUtils 工具类,帮助大家在项目中更优雅、安全地完成 Excel 的导入导出。

2. 导出 Excel 到前端

在 Web 项目中,导出功能本质上就是通过 HttpServletResponse 把二进制流写给前端。

关键点

  • 提前设置响应头(Content-TypeContent-Disposition);

  • 不要调用 .autoCloseStream(false),避免 Can not close IO 错误;

  • 只负责写出 Excel,不要在失败时再往 response 里写 JSON,避免流冲突。

实现代码

    /**
     * 将列表以 Excel 响应给前端
     *
     * @param response  响应
     * @param filename  文件名
     * @param sheetName Excel sheet 名
     * @param head      Excel head 头
     * @param data      数据列表哦
     * @param <T>       泛型,保证 head 和 data 类型的一致性
     * @throws IOException 写入失败的情况
     */
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) throws IOException {

        // 提前设置 header
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));

        // 输出 Excel(去掉 autoCloseStream(false),让 EasyExcel 正常关闭流)
        EasyExcel.write(response.getOutputStream(), head)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())   // 自动列宽
                .registerWriteHandler(new SelectSheetWriteHandler(head))            // 下拉框支持
                .registerConverter(new LongStringConverter())                       // Long 类型精度
                .sheet(sheetName)
                .doWrite(data);
    }

3. 导入 Excel 到后端

Excel 导入功能通常依赖 MultipartFile。这里需要注意:

关键点

  • 判断文件是否为空,提前抛出异常;

  • 使用 try-with-resources 确保输入流始终关闭;

  • 由于我们手动控制流关闭,所以需要 .autoCloseStream(false)

实现代码

    /**
     * 从上传的 Excel 文件读取数据
     *
     * @param file 上传的 Excel 文件
     * @param head 解析的实体类
     * @param <T>  泛型,保证 head 和 data 类型一致
     * @return 解析结果列表
     * @throws IOException 读取失败
     */
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        if (file == null || file.isEmpty()) {
            throw exception("导入数据不能为空!");
        }

        // try-with-resources,确保输入流正确关闭
        try (InputStream inputStream = file.getInputStream()) {
            return EasyExcel.read(inputStream, head, null)
                    .autoCloseStream(false) // 不让 EasyExcel 关流,由 try-with-resource 负责
                    .doReadAllSync();
        }
    }

4. ExcelUtils 工具类

import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;

/**
 * Excel 工具类
 *
 */
public class ExcelUtils {

    /**
     * 将列表以 Excel 响应给前端
     *
     * @param response  响应
     * @param filename  文件名
     * @param sheetName Excel sheet 名
     * @param head      Excel head 头
     * @param data      数据列表哦
     * @param <T>       泛型,保证 head 和 data 类型的一致性
     * @throws IOException 写入失败的情况
     */
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) throws IOException {

        // 提前设置 header
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));

        // 输出 Excel(去掉 autoCloseStream(false),让 EasyExcel 正常关闭流)
        EasyExcel.write(response.getOutputStream(), head)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())   // 自动列宽
                .registerWriteHandler(new SelectSheetWriteHandler(head))            // 下拉框支持
                .registerConverter(new LongStringConverter())                       // Long 类型精度
                .sheet(sheetName)
                .doWrite(data);
    }


    /**
     * 从上传的 Excel 文件读取数据
     *
     * @param file 上传的 Excel 文件
     * @param head 解析的实体类
     * @param <T>  泛型,保证 head 和 data 类型一致
     * @return 解析结果列表
     * @throws IOException 读取失败
     */
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        if (file == null || file.isEmpty()) {
            throw exception("导入数据不能为空!");
        }

        // try-with-resources,确保输入流正确关闭
        try (InputStream inputStream = file.getInputStream()) {
            return EasyExcel.read(inputStream, head, null)
                    .autoCloseStream(false) // 不让 EasyExcel 关流,由 try-with-resource 负责
                    .doReadAllSync();
        }
    }
}

5. 常见问题总结

  • 为什么导出不能用 autoCloseStream(false)
    因为 HttpServletResponse.getOutputStream() 是 Servlet 容器管理的流,不该手动干预,EasyExcel 默认关闭即可。

  • 为什么导入建议用 try-with-resources?
    MultipartFile.getInputStream() 不是容器流,如果 EasyExcel 不负责关闭,就需要我们手动关闭,避免内存泄露。

  • 导出失败时如何处理?
    推荐用 response.sendError(500, "导出失败"),而不是再往流里写 JSON。

6. 总结

通过对 ExcelUtils 的封装,我们可以在 Spring Boot 项目中更简洁、优雅地实现 Excel 导入导出,避免常见的流关闭异常和文件句柄泄露问题。

最终方案:

  • 导出 → 不要关闭流,交给 EasyExcel;

  • 导入 → 手动关闭流,避免泄露。

这样,一个通用的 ExcelUtils 工具类就能让团队开发更稳定、更高效。 🚀