Spring Boot 中结合 EasyPoi 实现大文件 Excel 的导入导出

在 Spring Boot 中结合 EasyPoi 实现大文件 Excel 的导入导出,需要思考内存占用、分片处理等问题,避免 OOM(内存溢出)。以下是具体实现方案:

一、环境准备

1. 引入依赖(pom.xml)

xml

<!-- Spring Boot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- EasyPoi 核心依赖 -->
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-base</artifactId>
    <version>4.4.0</version>
</dependency>
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-web</artifactId>
    <version>4.4.0</version>
</dependency>
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-annotation</artifactId>
    <version>4.4.0</version>
</dependency>

二、大文件 Excel 导出(分片处理)

大文件导出的核心是分片查询数据,避免一次性加载全部数据到内存。

1. 定义实体类(使用 EasyPoi 注解)

@Data
public class User {
    @Excel(name = "ID", width = 10)
    private Long id;

    @Excel(name = "姓名", width = 20)
    private String name;

    @Excel(name = "年龄", width = 10)
    private Integer age;

    @Excel(name = "注册时间", format = "yyyy-MM-dd HH:mm:ss", width = 20)
    private Date registerTime;
}

2. 导出工具类(分片查询 + 流式写入)

@Component
public class ExcelExportUtil {

    /**
     * 大文件Excel导出(分片处理)
     * @param response 响应对象
     * @param fileName 文件名
     * @param clazz 实体类class
     * @param dataProvider 数据提供器(分片查询数据)
     */
    public <T> void exportBigExcel(HttpServletResponse response, String fileName, 
                                  Class<T> clazz, DataProvider<T> dataProvider) throws IOException {
        // 设置响应头
        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-Disposition", 
                          "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".xlsx");

        // 初始化Excel导出参数
        ExportParams exportParams = new ExportParams();
        exportParams.setType(ExcelType.XSSF); // 大文件提议用XSSF(.xlsx)

        // 流式写入Excel
        Workbook workbook = null;
        try {
            workbook = ExcelExportUtil.exportBigExcel(exportParams, clazz, dataProvider);
            workbook.write(response.getOutputStream());
        } finally {
            if (workbook != null) {
                workbook.close();
            }
        }
    }

    // 数据提供器接口(分片查询数据)
    @FunctionalInterface
    public interface DataProvider<T> {
        List<T> provide(int pageNum, int pageSize); // 返回指定页的数据
    }
}

3. 控制器实现

@RestController
@RequestMapping("/excel")
public class ExcelController {

    @Autowired
    private ExcelExportUtil exportUtil;

    @Autowired
    private UserService userService; // 业务层(提供分片查询)

    @GetMapping("/export/big")
    public void exportBigExcel(HttpServletResponse response) throws IOException {
        // 调用工具类导出,通过DataProvider分片查询数据
        exportUtil.exportBigExcel(response, "用户列表", User.class, 
            (pageNum, pageSize) -> userService.queryByPage(pageNum, pageSize)); // 分页查询逻辑
    }
}

4. 业务层分页查询(示例)

@Service
public class UserService {
    // 模拟分页查询(实际项目中替换为数据库分页查询)
    public List<User> queryByPage(int pageNum, int pageSize) {
        int start = (pageNum - 1) * pageSize;
        // 从数据库查询第start到start+pageSize的数据
        return userMapper.selectByPage(start, pageSize);
    }
}

三、大文件 Excel 导入(SAX 解析)

大文件导入需避免一次性加载全部行到内存,应使用SAX 解析(逐行读取)。

1. 自定义 SAX 解析监听器

public class BigExcelListener extends AnalysisEventListener<User> {
    private static final int BATCH_SIZE = 1000; // 批量处理阈值
    private List<User> batchList = new ArrayList<>(BATCH_SIZE);
    private UserService userService; // 业务层(用于保存数据)

    // 构造器注入业务层
    public BigExcelListener(UserService userService) {
        this.userService = userService;
    }

    // 逐行解析时触发
    @Override
    public void invoke(User user, AnalysisContext context) {
        batchList.add(user);
        // 达到阈值时批量保存
        if (batchList.size() >= BATCH_SIZE) {
            saveData();
            batchList.clear(); // 清空列表释放内存
        }
    }

    // 解析完成后触发(处理剩余数据)
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData(); // 保存最后一批数据
    }

    // 批量保存数据
    private void saveData() {
        userService.batchSave(batchList); // 批量插入数据库
    }
}

2. 导入工具类

@Component
public class ExcelImportUtil {

    /**
     * 大文件Excel导入(SAX解析)
     * @param file 上传的文件
     * @param clazz 实体类class
     * @param listener 自定义监听器
     */
    public <T> void importBigExcel(MultipartFile file, Class<T> clazz, AnalysisEventListener<T> listener) throws Exception {
        // 获取文件输入流
        InputStream inputStream = file.getInputStream();
        // 构建SAX解析器
        ExcelReader reader = ExcelReaderBuilder
                .create()
                .file(inputStream)
                .head(clazz) // 表头对应实体类
                .autoTrim(true) // 自动trim字符串
                .build();
        // 开始解析(逐行处理)
        reader.read(new Sheet(1, 0, clazz), listener); // 第1个sheet,从第0行开始读
        inputStream.close();
    }
}

3. 控制器实现

@RestController
@RequestMapping("/excel")
public class ExcelController {

    @Autowired
    private ExcelImportUtil importUtil;

    @Autowired
    private UserService userService;

    @PostMapping("/import/big")
    public String importBigExcel(@RequestParam("file") MultipartFile file) throws Exception {
        // 使用自定义监听器处理导入
        importUtil.importBigExcel(file, User.class, new BigExcelListener(userService));
        return "导入成功";
    }
}

4. 业务层批量保存(示例)

@Service
public class UserService {
    // 批量保存(实际项目中使用MyBatis的batch插入)
    @Transactional
    public void batchSave(List<User> users) {
        userMapper.batchInsert(users);
    }
}

四、关键优化点

  1. 导出优化:使用XSSF格式(.xlsx),支持更大文件(相比 HSSF 的.xls 的 65536 行限制)。分片查询数据(如每次查 1000 行),避免一次性加载全量数据。
  2. 导入优化:基于 SAX 的逐行解析,内存占用低。批量插入数据库(减少 SQL 执行次数)。
  3. 其他提议:大文件处理提议用异步任务(如@Async),避免阻塞主线程。增加进度条反馈(可结合 WebSocket 实时更新进度)。限制文件大小(如 100MB 以内),避免恶意上传超大文件。

通过以上方式,可有效处理大文件 Excel 的导入导出,避免内存溢出问题,适用于十万级甚至百万级数据量的场景。

© 版权声明

相关文章

3 条评论

  • 头像
    他是只野怪 投稿者

    ExcelExportUtil里面的exportBigExcel自己调自己?玩死循环呢?

    无记录
    回复
  • 头像
    -天生门面命- 读者

    共享学习,欢迎点赞,感谢关注

    无记录
    回复
  • 头像
    A安琪拉儿_ 投稿者

    收藏了,感谢分享

    无记录
    回复