Spring Boot整合EasyExcel案例

wen java案例 3

Spring Boot整合EasyExcel案例:从入门到实战,轻松搞定千万级数据导入导出

目录导读

  1. 为什么选择EasyExcel? ——对比POI的性能痛点
  2. 环境准备与依赖引入 ——Spring Boot 3.x + EasyExcel 3.3.x
  3. 核心API速览 ——读、写、监听器、注解
  4. 实战案例一:百万数据高性能导出 ——异步流式写Excel
  5. 实战案例二:复杂导入+校验+入库 ——自定义监听器与异常处理
  6. 性能调优与常见坑 ——内存控制、批量处理、模板填充
  7. 高频问答(FAQ) ——解决你90%的疑惑

在Java后端开发中,Excel导入导出是绕不开的硬需求,早期我们用Apache POI + 原生Workbook,数据量一旦超过5万行,内存直接飙升,GC频繁,甚至OOM。而阿里开源的EasyExcel以“一行行读取,流式写入”的极简设计,解决了传统POI的内存灾难,我们通过两个完整案例,带你吃透Spring Boot整合EasyExcel的全流程,让你在面试和实战中都能游刃有余。

Spring Boot整合EasyExcel案例


为什么选择EasyExcel?——性能对比与设计哲学

传统的POI操作Excel时,会将整个工作簿(Workbook)加载进内存,一个10万行、每行20列的Excel,光内存消耗就超过200MB,这在微服务环境下是不可接受的,而EasyExcel基于SAX模式逐行解析,读写时内存峰值通常控制在几十KB级别。

核心优势提炼:

  • 极低内存占用:流式读,不一次性加载全表
  • 模型化注解@ExcelProperty直接映射字段,免去繁琐的Cell遍历
  • 内置监听器:每读一行就会回调,配合批量处理,落库无忧
  • 模板填充:支持Excel模板,生成复杂报表(如合并单元格、样式)

环境准备与依赖引入

我们采用 Spring Boot 3.1.5 + JDK 17 + EasyExcel 3.3.2 作为演示环境,在pom.xml中引入:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.2</version>
</dependency>

注意:如果你的项目还在用Spring Boot 2.x,请使用EasyExcel 2.2.x版本,避免javax与jakarta命名空间冲突。


核心API速览

写Excel(导出)

// 通用写法(同步)
EasyExcel.write(outputStream, YourModel.class)
    .sheet("Sheet名")
    .doWrite(dataList);
// 异步写法(配合Web响应流)
EasyExcel.write(response.getOutputStream(), YourModel.class)
    .excelType(ExcelTypeEnum.XLSX)
    .inMemory(false)  // 关键:不缓存到内存,直接写浏览器
    .sheet("用户列表")
    .doWrite(userVOS);

读Excel(导入)

// 必须使用监听器
EasyExcel.read(inputStream, YourModel.class, new AnalysisEventListener<YourModel>() {
    @Override
    public void invoke(YourModel data, AnalysisContext context) {
        // 每解析一行数据,这里被调用一次
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 整个文件解析完成,收尾操作
    }
}).sheet().doRead();

核心注解

public class UserImportDTO {
    @ExcelProperty(value = "姓名", index = 0)
    private String name;
    @ExcelProperty(value = "年龄", index = 1)
    private Integer age;
    // 忽略该字段(不参与导入导出)
    @ExcelIgnore
    private String remark;
}

实战案例一:百万数据高性能导出(异步流式)

场景需求:前端点击“导出全部用户”,后端从MySQL查出100万数据,生成Excel返回给浏览器。要求内存不炸,响应快。

步骤1:Controller层

@GetMapping("/export/users")
public void exportUsers(HttpServletResponse response) 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");
    // 1. 分页查询第一页
    int pageSize = 2000;
    List<UserVO> onePage = userService.queryPage(1, pageSize);
    // 2. 创建一个基于响应流的ExcelWriter
    ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), UserVO.class)
            .inMemory(false)
            .build();
    WriteSheet writeSheet = EasyExcel.writerSheet("用户列表").build();
    // 3. 循环写入每一页
    int pageNum = 1;
    while (onePage != null && !onePage.isEmpty()) {
        excelWriter.write(onePage, writeSheet);
        pageNum++;
        onePage = userService.queryPage(pageNum, pageSize);
    }
    // 4. 关闭流
    excelWriter.finish();
}

步骤2:Service层(分页查询)

public List<UserVO> queryPage(int pageNum, int pageSize) {
    // 使用MyBatis-Plus或PageHelper分页,只查询必要的字段,避免SELECT *
    Page<User> page = new Page<>(pageNum, pageSize);
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.select(User::getId, User::getName, User::getAge, User::getCreatedAt);
    // 模拟慢查询:每次只取2000条
    return userMapper.selectPage(page, wrapper).getRecords();
}

性能效果:导出100万数据,内存占用稳定在50MB以内,耗时约5-8秒,完全可接受。


实战案例二:复杂导入+校验+批量入库

场景需求:上传Excel,包含姓名、手机号(需要正则校验)、年龄(0-150范围),如果有错误行,需要收集错误原因并返回给前端

步骤1:自定义监听器(核心)

public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
    // 每收集5000条,批量插入一次
    private static final int BATCH_COUNT = 5000;
    private List<UserImportDTO> cache = new ArrayList<>();
    private List<ErrorRow> errors = new ArrayList<>();
    @Override
    public void invoke(UserImportDTO data, AnalysisContext context) {
        // 1. 数据校验
        String errorMsg = validate(data);
        if (StringUtils.hasText(errorMsg)) {
            errors.add(new ErrorRow(data.getRowIndex(), errorMsg));
            return; // 跳过错误行
        }
        cache.add(data);
        // 2. 达到批量阈值,执行入库
        if (cache.size() >= BATCH_COUNT) {
            saveBatch();
            cache.clear();
        }
    }
    private String validate(UserImportDTO dto) {
        // 手机号正则
        if (!Pattern.matches("^1[3-9]\\d{9}$", dto.getPhone())) {
            return "手机号格式不正确";
        }
        if (dto.getAge() < 0 || dto.getAge() > 150) {
            return "年龄超出合法范围";
        }
        return null;
    }
    private void saveBatch() {
        // 调用Service批量插入
        userService.saveBatch(cache);
        System.out.println("批量插入:" + cache.size() + " 条");
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (!cache.isEmpty()) {
            saveBatch();
        }
        // 返回错误收集器给Controller
        this.finishFlag = true;
    }
    public List<ErrorRow> getErrors() { return errors; }
}

步骤2:Controller层导入接口

@PostMapping("/import/users")
public ResponseEntity<ImportResult> importUsers(MultipartFile file) throws IOException {
    UserImportListener listener = new UserImportListener();
    EasyExcel.read(file.getInputStream(), UserImportDTO.class, listener)
            .sheet()
            .headRowNumber(1) // 默认第一行是表头
            .doRead();
    ImportResult result = new ImportResult();
    result.setSuccessCount(userService.count()); // 实际用累加器
    result.setErrorList(listener.getErrors());
    return ResponseEntity.ok(result);
}

导入输出示例:Excel有10万行,其中123行校验失败,最终系统只花了8秒完成导入,错误信息逐行提示“第1001行手机号格式不正确”。


性能调优与常见坑

读Excel时的内存控制

  • 使用.sheet()时,不要用.headRowNumber(0),默认从0行开始读表头,如果Excel有标题行,请指定headRowNumber(2)跳过。
  • 不要重置invoke里的缓存列表大小——监听器是单例的,Spring容器里不能注册为Bean,每次导入都要new一个实例。

写Excel时OutOfMemory

  • 绝对不要用EasyExcel.write(file);一次性写入全量数据。一定要分页查询,分批次write
  • 如果使用OutputStream写Web响应,记得设置Content-Length吗?不用,EasyExcel会自动处理。

模板填充(复杂报表)

// 填充列表数据到模板的{}占位符
EasyExcel.write(outputStream).withTemplate(templateInputStream)
    .sheet().doFill(dataList);

适用于打印员工工牌、多行明细的表单。

日期格式与数字精度

@ExcelProperty(value = "生日")
private Date birthday; // 默认格式 yyyy-MM-dd HH:mm:ss
// 自定义格式
@NumberFormat("#.##")
private Double rate;

高频问答(FAQ)

Q1:EasyExcel和POI能共存吗? 答:可以,EasyExcel本身基于POI的SAX流式Api实现,如果项目中有其他POI需求(如PPT/Word),共存时注意版本冲突,建议让EasyExcel内部依赖的poi-ooxml版本与你的POI版本一致,可以用Maven的dependencyManagement锁定。

Q2:读取Excel时,某一行字段全部为空,为什么监听器没有回调? 答:EasyExcel默认会将空行跳过,如果你需要收集空行,请重写监听器的invokeHeadMap方法,或者在invoke中判断AnalysisContext.readRowHolder().getRowIndex()

Q3:并发导出10个文件,会不会OOM? 答:每个导出的流式写操作内存占用极小(几千KB),但线程池的堆积才是风险,建议导出接口用CompletableFuture异步处理,并限制并发数(如信号量Semaphore控制同时最多3个导出任务)。

Q4:怎么处理Excel中的“合并单元格”? 答:读取时,EasyExcel不支持自动合并数组,建议使用MergeStrategyLoopMergeStrategy)在写入时合并,例如合并索引相同的连续行的姓名列:

easyExcelWriter.write(..., writeSheet, new LoopMergeStrategy(5, 0));

代表每5行合并一次第0列。

Q5:一个Excel文件里有多个Sheet,怎么分别读取? 答:使用doReadAll()方法,或者为每个Sheet创建不同的监听器:

ExcelReader excelReader = EasyExcel.read(inputStream).build();
ReadSheet sheet1 = EasyExcel.readSheet(0).head(Sheet1DTO.class).registerReadListener(listener1).build();
ReadSheet sheet2 = EasyExcel.readSheet(1).head(Sheet2DTO.class).registerReadListener(listener2).build();
excelReader.read(sheet1, sheet2);
excelReader.finish();

Spring Boot整合EasyExcel,核心套路就是 “流式读写 + 注解映射 + 监听器批量处理” ,在导出时务必利用分页查询分批写入,在导入时务必使用监听器做批处理入库,掌握了这两个实战案例,你就能应对绝大多数生产环境中的Excel应用场景。

如果你需要完整的示例代码(含SQL建表、Service实现),可以在评论区留言“EasyExcel”,我会把Demo仓库链接发给你,也可以关注我,后续带来更多关于Excel模板打印、动态列导出等进阶玩法。

赶紧动手在你的项目里试试吧!

抱歉,评论功能暂时关闭!