SpringBoot集成EasyExcel实现百万级别的数据导入导出实践指南

本文主要是介绍SpringBoot集成EasyExcel实现百万级别的数据导入导出实践指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《SpringBoot集成EasyExcel实现百万级别的数据导入导出实践指南》本文将基于开源项目springboot-easyexcel-batch进行解析与扩展,手把手教大家如何在SpringBo...

项目结构概览

springboot-easyexcel-BATch
├── src/main/Java/com/example/easyexcel
│   ├── controller/      # 导入导出接口
│   ├── listener/        # 导入监听器
│   ├── model/           # 实体类
│   ├── service/         # 业务逻辑
│   └── Application.java # 启动类
└── src/main/resources
    ├── application.yml  # 线程池配置
    └── templates/       # 前端demo

核心依赖

<!-- Spring Boot 2.2.1 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
</parent>

<!-- EasyExcel 2.2.11(稳定版) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.11</version>
</dependency>

百万级导出实战

场景

需求数据量策略
导出用户表100万+分Sheet + 分批查询 + 边查边写

核心代码

package com.example.easyexcel.service;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.easyexcel.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

@Service
@Slf4j
public class ExcelExportService {

    private final ThreadPoolTaskExecutor excelExecutor;
    private final UserService userService;

    // 每个Sheet的数据量
    private static final int DATA_PER_SHEET = 100000;

    // 每次查询的数据量
    private static final int QUERY_BATCH_SIZE = 10000;

    public ExcelExportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
        this.excelExecutor = excelExecutor;
        this.userService = userService;
    }

    /**
     * 导出百万级用户数据(优化内存版本)
     */
    public void exportMillionUsers(HttpServletResponse response, long totalCount) throws IOException {
        // 设置响应头
        response.setContentType("application/vnd.openXMLformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("百万用户数据", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);

        // 计算总Sheet数
        int sheetCount = (int) (totalCount / DATA_PER_SHEET + (totalCount % DATA_PER_SHEET > 0 ? 1 : 0));
        log.info("需要生成的Sheet总数:{}", sheetCount);

        try (OutputStream os = response.getOutputStream()) {
            // 创建ExcelWriter,直接写入响应输出流
            ExcelWriter excelWriter = EasyExcel.write(os, User.class).build();

            // 用于保证Sheet写入顺序的前一个Future
            CompletableFuture<Void> previousFuture = CompletableFuture.completedFuture(null);

            for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
                final int currentSheetNo = sheetNo;
                long start = currentSheetNo * (long) DATA_PER_SHEET;
                long end = Math.min((currentSheetNo + 1) * (long) DATA_PER_SHEET, totalCount);

                // 每个Sheet的处理依赖于前一个Sheet完成,保证顺序
                previousFuture = previousFuture.thenRunAsync(() -> {
                    try {
                        log.info("开始处理Sheet {} 的数据({} - {})", currentSheetNo, start, end);
                        writeSheetData(excelWriter, currentSheetNo, start, end);
                        log.info("完成处理Sheet {} 的数据", currentSheetNo);
                    } catch (Exception e) {
                        log.error("处理Sheet {} 数据失败", currentSheetNo, e);
                        throw new RuntimeException("处理Sheet " + currentSheetNo + " 数据失败", e);
                    }
                }, excelExecutor);
            }

            // 等待所有Sheet处理完成
            previousFuture.join();

            // 完成写入
            excelWriter.finish();
            log.info("所有Sheet写入完成");

        } catch (Exception e) {
            log.error("Excel导出失败", e);
            throw e;
        }
    }

    /**
     * 写入单个Sheet的数据
     */
    private void writeSheetData(ExcelWriter excelWriter, int sheetNo, long start, long end) {
        String sheetName = "用户数据" + (sheetNo + 1);
        WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, sheetName).build();

        long totalToQuery = end - start;
        int totalWritten = 0;

        // 分批查询并写入,每批查询后立即写入,不缓存大量数据
        for (long i = 0; i < totalToQuery; i += QUERY_BATCH_SIZE) {
            long currentStart = start + i;
            long currentEnd = Math.min(start + i + QUERY_BATCH_SIZE, end);

            // 调用UserService查询数据
            List<User> batchData = userService.findUsersByRange(currentStart, currentEnd);

            if (batchData == null || batchData.isEmpty()) {
                log.info("{} - {} 范围没有数据", currentStart, phpcurrentEnd);
                break; // 没有更多数据,提前退出
            }

            // 直接写入这一批数据
            excelWriter.write(batchData, writeSheet);
            totalWritten += batchData.size();

            log.info("Sheet {} 已写入 {} KdwYxrYas- {} 范围的数据,累计 {} 条",
                    sheetName, currentStart, currentEnd, totalWritten);

            // 清除引用,帮助GC
            batchData = new ArrayList<>();
        }

        log.info("Sheet {} 写入完成,共 {} 条数据", sheetName, totalWritten);
    }
}


效果

指标优化前优化后
内存峰值1.2GB100MB
耗时45s18s

百万级导入实战

场景

需求数据量策略
导入用户表100万+分Sheet + 监听器 + 批量插入

监听器和Service(核心)

package com.example.easyexcel.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.example.easyexcel.model.User;
import com.example.easyexcel.service.UserService;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 用户数据导入监听器(独立类实现)
 */
@Slf4j
public class UserImportListener extends AnalysisEventListener<User> {

    // 批量保存阈值(可根据内存调整)
    private static final int BATCH_SIZE = 5000;
    
    // 临时存储批次数据
    private final List<User> batchList = new ArrayList<>(BATCH_SIZE);
    
    // 导入结果统计
    private final AtomicLong successCount = new AtomicLong(0);
    private final AtomicLong failCount = new AtomicLong(0);
    
    // 业务服务(通过构造器注入)
    private final UserService userService;

    public UserImportListener(UserService userService) {
        this.userService = userService;
    }

    /**
     * 每读取一行数据触发
     */
    @Override
    public void invoke(User user, AnalysisContext context) {
        // 数据验证
        if (validateUser(user)) {
            batchList.add(user);
            successCount.incrementAndGet();
js            
            // 达到批次大小则保存
            if (batchList.size() >= BATCH_SIZE) {
                saveBatchData();
                // 清空列表释放内存
                batchList.clear();
            }
        } else {
            failCount.incrementAndGet();
            log.warn("数据验证失败: {}", user);
        }
    }

    /**
     * 所有数据读取完成后触发
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 处理剩余数据
        if (!batchList.isEmpty()) {
            saveBatchData();
            batchList.clear();
        }
        log.info("当前Sheet导入结束,成功: {}, 失败: {}", successCount.get(), failCount.get());
    }

    /**
     * 批量保存数据
     */
    private void saveBatchData() {
        try {
            // 调用业务层批量保存(带事务)
            userService.batchSaveUsers(batchList);
            log.debug("批量保存成功,数量: {}", batchList.size());
        } catch (Exception e) {
            log.error("批量保存失败,数量: {}", batchList.size(), e);
            // 失败处理:可记录失败数据到文件或数据库
            handleSaveFailure(batchList);
        }
    }

  KDwYxrYas  /**
     * 数据验证逻辑
     */
    private boolean validateUser(User user) {
        // 基础字段验证(根据实际业务调整)
        if (user == null) return false;
        if (user.getId() == null) return false;
        if (user.getName() == null || user.getName().trim().isEmpty()) return false;
        return true;
    }

    /**
     * 处理保存失败的数据
     */
    private void handleSaveFailure(List<User> failedData) {
        // 实现失败数据的处理逻辑(例如写入失败日志表)
        // userService.saveFailedData(failedData);
    }

    // Getter方法用于统计结果
    public long getSuccessCount() {
        return successCount.get();
    }

    public long getFailCount() {
        return failCount.get();
    }
}

导入Service类

package com.example.easyexcel.service;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.example.easyexcel.listener.SheetCountListener;
import com.example.easyexcel.listener.UserImportListener;
import com.example.easyexcel.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ajstomic.AtomicLong;


/**
 * 百万级Excel数据导入服务
 */
@Service
@Slf4j
public class ExcelImportService {

    private final ThreadPoolTaskExecutor excelExecutor;
    private final UserService userService;

    public ExcelImportService(ThreadPoolTaskExecutor excelExecutor, UserService userService) {
        this.excelExecutor = excelExecutor;
        this.userService = userService;

    }

    /**
     * 多线程导入百万级用户数据(每个Sheet一个线程)
     */
    public void importMillionUsers(MultipartFile file) throws IOException {
        // 1. 保存成临时文件,避免多线程共用 InputStream
        java.io.File tmpFile = java.io.File.createTempFile("excel_", ".xlsx");
        file.transferTo(tmpFile);          // Spring 提供的零拷贝
        tmpFile.deleteOnExit();            // JVM 退出时自动清理

        ExcelTypeEnum excelType = getExcelType(file.getOriginalFilename());

        // 2. 拿 sheet 数量
        int sheetCount;
        try (InputStream in = new java.io.FileInputStream(tmpFile)) {
            sheetCount = getSheetCount(in);
        }
        log.info("开始导入,总 Sheet 数: {}", sheetCount);

        // 3. 并发读,每个 Sheet 独立 FileInputStream
        AtomicLong totalSuccess = new AtomicLong(0);
        AtomicLong totalFail    = new AtomicLong(0);

        List<CompletableFuture<Void>> futures = new ArrayList<>(sheetCount);
        for (int sheetNo = 0; sheetNo < sheetCount; sheetNo++) {
            final int idx = sheetNo;
            futures.add(CompletableFuture.runAsync(() -> {
                try (InputStream in = new java.io.FileInputStream(tmpFile)) {
                    UserImportListener listener = new UserImportListener(userService);
                    EasyExcel.read(in, User.class, listener)
                            .excelType(excelType)
                            .sheet(idx)
                            .doRead();

                    totalSuccess.addAndGet(listener.getSuccessCount());
                    totalFail.addAndGet(listener.getFailCount());
                    log.info("Sheet {} 完成,成功: {}, 失败: {}", idx, listener.getSuccessCount(), listener.getFailCount());
                } catch (IOException e) {
                    throw new RuntimeException("Sheet " + idx + " 读取失败", e);
                }
            }, excelExecutor));
        }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        log.info("全部导入完成,总成功: {},总失败: {}", totalSuccess.get(), totalFail.get());
    }

    /**
     * 获取Excel中的Sheet数量
     */
    private int getSheetCount(InputStream inputStream) {
        SheetCountListener countListener = new SheetCountListener();
        EasyExcel.read(inputStream)
                .registerReadListener(countListener)
                .doReadAll();
        return countListener.getSheetCount();
    }

    /**
     * 获取Excel文件类型
     *
     */
    public ExcelTypeEnum getExcelType(String fileName) {
        if (fileName == null) return null;
        if (fileName.toLowerCase().endsWith(".xlsx")) {
            return ExcelTypeEnum.XLSX;
        } else if (fileName.toLowerCase().endsWith(".xls")) {
            return ExcelTypeEnum.XLS;
        }
        return null;
    }


}

Controller

 @PostMapping("/import")
@ApiOperation("导入用户数据")
public ResponseEntity<String> importUsers(@RequestParam("file") MultipartFile file) {
    try {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("请选择要导入的文件");
        }

        String fileName = file.getOriginalFilename();
        ExcelTypeEnum excelType = importService.getExcelType(fileName);
        if (excelType == null) {
            return ResponseEntity.badRequest().body("不支持的文件类型,文件名:" +  fileName);
        }

        importService.importMillionUsers(file);
        return ResponseEntity.ok("文件导入成功,正在后台处理数据");
    } catch (Exception e) {
        log.error("导入用户数据失败", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("导入失败:" + e.getMessage());
    }
}

性能优化技巧

技巧说明
分批查询避免一次性加载全表
分批写入每5k条批量插入
临时文件并发读时先 MultipartFile.transferTo(tmp)
线程池配置专用线程池,隔离业务线程
# application.yml
spring:
  task:
    execution:
      pool:
        core-size: 10
        max-size: 30
        queue-capacity: 1000

常见问题 & 解决方案

问题解决方案
Can not create temporary file!并发读时先保存临时文件,再独立流读取
Stream Closed每个任务独立 InputStream
OutOfMemoryError分批处理 + 及时 clear()

总结

Spring Boot + EasyExcel零侵入 的情况下即可完成百万级数据的导入导出。

通过 分批、并发、顺序写 等技巧,内存占用降低 90% 以上。

以上就是SpringBoot集成EasyExcel实现百万级别的数据导入导出实践指南的详细内容,更多关于SpringBoot EasyExcel数据导入导出的资料请关注China编程(www.chinasem.cn)其它相关文章!

这篇关于SpringBoot集成EasyExcel实现百万级别的数据导入导出实践指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155595

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

JDK21对虚拟线程的几种用法实践指南

《JDK21对虚拟线程的几种用法实践指南》虚拟线程是Java中的一种轻量级线程,由JVM管理,特别适合于I/O密集型任务,:本文主要介绍JDK21对虚拟线程的几种用法,文中通过代码介绍的非常详细,... 目录一、参考官方文档二、什么是虚拟线程三、几种用法1、Thread.ofVirtual().start(

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置