Spring Boot 3 + EasyExcel 3.x 实战:构建高效、可靠的Excel导入导出服务

目录

一、前言:为什么选择EasyExcel?

二、项目初始化与依赖引入

[2.1 开发环境](#2.1 开发环境)

[2.2 Maven依赖配置 (pom.xml)](#2.2 Maven依赖配置 (pom.xml))

三、定义核心数据模型:学生信息实体

[3.1 实体类代码](#3.1 实体类代码)

[3.2 注解详解](#3.2 注解详解)

四、实现Excel监听器:核心逻辑处理器

[4.1 监听器设计要点](#4.1 监听器设计要点)

五、构建Service层:业务逻辑与校验

[5.1 服务接口](#5.1 服务接口)

[5.2 服务实现类](#5.2 服务实现类)

六、Excel导入响应对象与异常处理

[6.1 自定义异常类](#6.1 自定义异常类)

[6.2 统一响应对象](#6.2 统一响应对象)

[七、打造Controller层:提供RESTful API](#七、打造Controller层:提供RESTful API)

[7.1 完整Controller代码](#7.1 完整Controller代码)

八、全局异常处理

九、配置文件 (application.yml)

十、测试与验证

[10.1 使用Postman测试导入](#10.1 使用Postman测试导入)

[10.2 测试导出](#10.2 测试导出)

十一、性能优化建议

十二、总结


一、前言:为什么选择EasyExcel?

在企业管理系统中,Excel导入导出是永恒的需求------报表生成、数据迁移、批量录入...然而,传统的Apache POI在处理大文件时,会将全部数据加载到内存中,一个几十MB的Excel就可能消耗几百MB甚至GB的内存,极易引发OOM。

阿里巴巴开源的EasyExcel 解决了这一痛点。它采用SAX解析模式,一行一行读取数据,内存占用极低。即使文件有几十万行数据,也能轻松应对。本文将带你从零开始,在Spring Boot 3项目中集成EasyExcel 3.x,实现一套结构清晰、健壮可靠的Excel导入导出功能。

最终成果预览:

  • 支持带数据校验的Excel导入,错误信息逐行反馈

  • 支持百万级数据的Excel导出,内存安全

  • 包含完整的异常处理机制

二、项目初始化与依赖引入

2.1 开发环境

  • JDK 17+

  • Spring Boot 3.2.0

  • EasyExcel 3.3.2

2.2 Maven依赖配置 (pom.xml)

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>excel-demo</artifactId>
    <version>1.0.0</version>
    
    <properties>
        <java.version>17</java.version>
        <easyexcel.version>3.3.2</easyexcel.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web 基础依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- EasyExcel 核心库 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>${easyexcel.version}</version>
        </dependency>
        
        <!-- Lombok 简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Bean Validation 用于数据校验 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- 可选:数据库操作 (本文以MyBatis-Plus为例) -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

依赖说明:

  • spring-boot-starter-web: 提供RESTful API能力

  • easyexcel: 核心Excel处理库

  • lombok: 简化实体类代码

  • validation: 提供数据校验注解(如@NotBlank、@Email等)

  • mybatis-plus: 可选,用于数据持久化演示

三、定义核心数据模型:学生信息实体

3.1 实体类代码

java 复制代码
package com.example.exceldemo.entity;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentStyle;
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
import jakarta.validation.constraints.*;
import lombok.Data;

import java.util.Date;

/**
 * 学生信息实体
 * 使用EasyExcel注解控制Excel列映射和样式
 */
@Data
public class Student {
    
    @ExcelProperty("学生ID")
    @ColumnWidth(10)
    private Long id;
    
    @NotBlank(message = "姓名不能为空")
    @Size(max = 20, message = "姓名长度不能超过20")
    @ExcelProperty("姓名")
    @ColumnWidth(15)
    private String name;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄最小为1岁")
    @Max(value = 150, message = "年龄最大为150岁")
    @ExcelProperty("年龄")
    @ColumnWidth(10)
    @ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
    private Integer age;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @ExcelProperty("邮箱")
    @ColumnWidth(25)
    private String email;
    
    @NotNull(message = "入学日期不能为空")
    @Past(message = "入学日期必须是过去时间")
    @DateTimeFormat("yyyy-MM-dd")
    @ExcelProperty("入学日期")
    @ColumnWidth(15)
    private Date enrollmentDate;
    
    @ExcelProperty("班级")
    @ColumnWidth(15)
    private String className;
}

3.2 注解详解

注解 作用 使用场景
@ExcelProperty 指定Excel列头名称,可配合index属性指定列顺序 必须,用于映射Java字段和Excel列
@ColumnWidth 设置Excel列宽 导出时控制显示效果
@ContentStyle 设置单元格样式(对齐、字体等) 导出时美化表格
@DateTimeFormat 日期格式化,如"yyyy-MM-dd HH:mm:ss" 处理日期类型字段
@NumberFormat 数字格式化,如"#.##%" 处理百分比等格式

四、实现Excel监听器:核心逻辑处理器

EasyExcel通过监听器逐行读取数据,我们需要继承AnalysisEventListener并重写核心方法。

java 复制代码
package com.example.exceldemo.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.example.exceldemo.entity.Student;
import com.example.exceldemo.service.StudentService;
import com.example.exceldemo.exception.ExcelImportException;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * 学生数据导入监听器
 * 核心特性:批量处理 + 错误收集
 */
@Slf4j
public class StudentDataListener implements ReadListener<Student> {
    
    /**
     * 批量处理阈值:每100条存入数据库
     */
    private static final int BATCH_COUNT = 100;
    
    /**
     * 缓存数据列表
     */
    private List<Student> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    
    /**
     * 错误信息收集
     */
    private List<String> errorMessages = new ArrayList<>();
    
    /**
     * 总行数统计
     */
    private int totalRows = 0;
    
    /**
     * 业务服务(通过构造方法注入)
     */
    private final StudentService studentService;
    
    /**
     * 构造方法传入Service,解决Spring Bean注入问题
     */
    public StudentDataListener(StudentService studentService) {
        this.studentService = studentService;
    }
    
    /**
     * 每解析一行数据,都会调用一次此方法
     */
    @Override
    public void invoke(Student student, AnalysisContext context) {
        totalRows++;
        log.info("解析第{}行数据:{}", totalRows, student);
        
        // 1. 数据校验(可在此处做复杂业务校验)
        try {
            // 调用服务层进行校验(包含JSR-380校验)
            studentService.validateAndProcess(student);
            
            // 校验通过,加入缓存
            cachedDataList.add(student);
            
        } catch (Exception e) {
            // 收集错误信息:记录是哪一行出错了
            String errorMsg = String.format("第%d行校验失败:%s", 
                totalRows + 1, // +1是因为Excel第一行是表头
                e.getMessage()
            );
            errorMessages.add(errorMsg);
            log.warn(errorMsg);
        }
        
        // 2. 达到批量处理阈值,存入数据库
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }
    
    /**
     * 所有数据解析完成后调用
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 处理剩余数据
        saveData();
        log.info("所有数据解析完成,共{}行,成功{}行,失败{}行", 
            totalRows, 
            totalRows - errorMessages.size(), 
            errorMessages.size()
        );
        
        // 如果有错误,抛出异常携带错误信息
        if (!errorMessages.isEmpty()) {
            throw new ExcelImportException(errorMessages, totalRows);
        }
    }
    
    /**
     * 批量保存数据到数据库
     */
    private void saveData() {
        if (!cachedDataList.isEmpty()) {
            log.info("批量保存{}条数据到数据库", cachedDataList.size());
            studentService.saveBatch(cachedDataList);
        }
    }
}

4.1 监听器设计要点

  1. 批量处理 :通过BATCH_COUNT控制每批处理数量,避免数据积压导致OOM

  2. 错误收集:将校验失败的信息收集起来,解析完成后统一抛出,而不是中断导入

  3. Service注入 :通过构造方法传入Service,解决监听器无法使用@Autowired的问题

  4. 行号记录 :利用totalRows记录当前处理行数,方便定位错误位置

五、构建Service层:业务逻辑与校验

5.1 服务接口

java 复制代码
package com.example.exceldemo.service;

import com.example.exceldemo.entity.Student;
import java.util.List;

public interface StudentService {
    
    /**
     * 校验并处理单条学生数据
     * @param student 学生数据
     * @throws IllegalArgumentException 校验失败时抛出异常
     */
    void validateAndProcess(Student student);
    
    /**
     * 批量保存学生数据
     * @param students 学生列表
     */
    void saveBatch(List<Student> students);
    
    /**
     * 查询所有学生(用于导出)
     */
    List<Student> listAll();
}

5.2 服务实现类

java 复制代码
package com.example.exceldemo.service.impl;

import com.example.exceldemo.entity.Student;
import com.example.exceldemo.service.StudentService;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class StudentServiceImpl implements StudentService {
    
    // 使用JSR-380 Validator进行声明式校验
    private final Validator validator;
    
    // 模拟数据库操作(实际项目中注入Mapper)
    // private final StudentMapper studentMapper;
    
    @Override
    public void validateAndProcess(Student student) {
        // 使用Validator进行JSR-380校验
        Set<ConstraintViolation<Student>> violations = validator.validate(student);
        
        if (!violations.isEmpty()) {
            String errorMsg = violations.stream()
                .map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
                .collect(Collectors.joining("; "));
            throw new IllegalArgumentException(errorMsg);
        }
        
        // 可以在此处添加更多业务校验,如:检查邮箱是否已存在
        // if (studentMapper.existsByEmail(student.getEmail())) {
        //     throw new IllegalArgumentException("邮箱已存在");
        // }
    }
    
    @Override
    public void saveBatch(List<Student> students) {
        // 批量插入数据库
        // studentMapper.insertBatch(students);
        log.info("批量保存{}条数据", students.size());
        // 模拟保存成功
    }
    
    @Override
    public List<Student> listAll() {
        // return studentMapper.selectList(null);
        // 模拟数据
        return List.of();
    }
}

六、Excel导入响应对象与异常处理

6.1 自定义异常类

java 复制代码
package com.example.exceldemo.exception;

import lombok.Getter;
import java.util.List;

/**
 * Excel导入异常
 * 封装导入过程中的错误信息和统计信息
 */
@Getter
public class ExcelImportException extends RuntimeException {
    
    private final List<String> errors;      // 错误详情列表
    private final int totalRows;             // 总行数
    private final int successRows;           // 成功行数
    private final int failedRows;             // 失败行数
    
    public ExcelImportException(List<String> errors, int totalRows) {
        super("Excel导入完成,但有部分数据校验失败,请查看详细信息");
        this.errors = errors;
        this.totalRows = totalRows;
        this.successRows = totalRows - errors.size();
        this.failedRows = errors.size();
    }
    
    /**
     * 获取简化的错误摘要信息
     */
    public String getErrorSummary() {
        if (errors.isEmpty()) {
            return "无错误";
        }
        StringBuilder summary = new StringBuilder();
        summary.append("共").append(errors.size()).append("处错误:");
        errors.forEach(msg -> summary.append(msg).append("; "));
        return summary.toString();
    }
}

6.2 统一响应对象

java 复制代码
package com.example.exceldemo.vo;

import lombok.Data;
import lombok.Builder;

@Data
@Builder
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    
    public static <T> Result<T> success(T data) {
        return Result.<T>builder()
            .code(200)
            .message("操作成功")
            .data(data)
            .build();
    }
    
    public static <T> Result<T> error(String message) {
        return Result.<T>builder()
            .code(500)
            .message(message)
            .build();
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        return Result.<T>builder()
            .code(code)
            .message(message)
            .build();
    }
}

七、打造Controller层:提供RESTful API

7.1 完整Controller代码

java 复制代码
package com.example.exceldemo.controller;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.example.exceldemo.entity.Student;
import com.example.exceldemo.exception.ExcelImportException;
import com.example.exceldemo.listener.StudentDataListener;
import com.example.exceldemo.service.StudentService;
import com.example.exceldemo.vo.Result;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/api/students")
@RequiredArgsConstructor
public class StudentController {
    
    private final StudentService studentService;
    
    /**
     * Excel导入接口
     * @param file Excel文件
     * @return 导入结果(包含成功/失败统计)
     */
    @PostMapping("/import")
    public Result<Map<String, Object>> importExcel(@RequestParam("file") MultipartFile file) {
        try {
            // 创建监听器(传入Service)
            StudentDataListener listener = new StudentDataListener(studentService);
            
            // 执行读取
            EasyExcel.read(file.getInputStream(), Student.class, listener)
                .excelType(ExcelTypeEnum.XLSX)  // 指定Excel类型
                .sheet()                         // 读取第一个sheet
                .headRowNumber(1)                 // 表头行数(默认第一行是表头)
                .doRead();
            
            // 如果没有异常,说明全部成功
            return Result.success(Map.of(
                "totalRows", listener.getTotalRows(),
                "successRows", listener.getTotalRows(),
                "failedRows", 0,
                "errors", List.of()
            ));
            
        } catch (ExcelImportException e) {
            // 部分数据校验失败
            Map<String, Object> result = new HashMap<>();
            result.put("totalRows", e.getTotalRows());
            result.put("successRows", e.getSuccessRows());
            result.put("failedRows", e.getFailedRows());
            result.put("errors", e.getErrors());
            return Result.error(400, "导入完成,但有" + e.getFailedRows() + "条数据校验失败")
                .withData(result);
                
        } catch (Exception e) {
            log.error("Excel导入失败", e);
            return Result.error("导入失败:" + e.getMessage());
        }
    }
    
    /**
     * Excel导出接口
     * @param response HTTP响应
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        try {
            // 1. 设置响应头
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            String fileName = URLEncoder.encode("学生数据", StandardCharsets.UTF_8)
                .replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", 
                "attachment;filename*=utf-8''" + fileName + ".xlsx");
            
            // 2. 查询数据
            List<Student> studentList = studentService.listAll();
            
            // 3. 写入Excel
            EasyExcel.write(response.getOutputStream(), Student.class)
                .excelType(ExcelTypeEnum.XLSX)
                .sheet("学生信息")
                .doWrite(studentList);
                
        } catch (Exception e) {
            log.error("Excel导出失败", e);
            // 重置response,避免返回乱码
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().println(Result.error("导出失败:" + e.getMessage()));
        }
    }
    
    /**
     * 模板下载接口
     * @param response HTTP响应
     */
    @GetMapping("/template")
    public void downloadTemplate(HttpServletResponse response) throws IOException {
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            String fileName = URLEncoder.encode("学生导入模板", StandardCharsets.UTF_8)
                .replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", 
                "attachment;filename*=utf-8''" + fileName + ".xlsx");
            
            // 创建一个空模板(只有表头)
            EasyExcel.write(response.getOutputStream(), Student.class)
                .excelType(ExcelTypeEnum.XLSX)
                .sheet("模板")
                .doWrite(List.of());  // 写入空数据,只生成表头
                
        } catch (Exception e) {
            log.error("模板下载失败", e);
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().println(Result.error("模板下载失败:" + e.getMessage()));
        }
    }
}

八、全局异常处理

java 复制代码
package com.example.exceldemo.handler;

import com.example.exceldemo.exception.ExcelImportException;
import com.example.exceldemo.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ExcelImportException.class)
    public Result<?> handleExcelImportException(ExcelImportException e) {
        log.warn("Excel导入异常:{}", e.getErrorSummary());
        return Result.error(400, e.getMessage())
            .withData(Map.of(
                "totalRows", e.getTotalRows(),
                "successRows", e.getSuccessRows(),
                "failedRows", e.getFailedRows(),
                "errors", e.getErrors()
            ));
    }
    
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统繁忙,请稍后重试");
    }
}

九、配置文件 (application.yml)

XML 复制代码
spring:
  application:
    name: excel-demo
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 50MB
  datasource:
    url: jdbc:mysql://localhost:3306/excel_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

# 日志配置
logging:
  level:
    com.example.exceldemo: debug
    com.alibaba.excel: warn

十、测试与验证

10.1 使用Postman测试导入

请求参数:

  • URL: http://localhost:8080/api/students/import

  • Method: POST

  • Body: form-data

    • Key: file

    • Value: 选择Excel文件

测试数据示例 (Excel内容):

姓名 年龄 邮箱 入学日期 班级
张三 20 zhangsan@example.com 2023-09-01 计算机1班
李四 25 lisi@example.com 2022-09-01 计算机2班
王五 17 wangwu@example.com 2024-09-01 计算机1班

成功响应示例:

java 复制代码
{
    "code": 200,
    "message": "操作成功",
    "data": {
        "totalRows": 3,
        "successRows": 3,
        "failedRows": 0,
        "errors": []
    }
}

失败响应示例(年龄校验失败):

java 复制代码
{
    "code": 400,
    "message": "导入完成,但有1条数据校验失败",
    "data": {
        "totalRows": 3,
        "successRows": 2,
        "failedRows": 1,
        "errors": [
            "第4行校验失败:age 年龄最小为1岁"
        ]
    }
}

10.2 测试导出

访问:http://localhost:8080/api/students/export

浏览器会自动下载名为"学生数据.xlsx"的文件。

相关推荐
匆匆忙忙之间游刃有余2 小时前
Openclaw 为什么突然火了?我拆完它的架构后,发现它正在把 AI 助手变成“数字分身”
人工智能·后端
如意机反光镜裸2 小时前
excel怎么快速导入oracle
数据库·oracle·excel
悟空码字2 小时前
别再让你的SpringBoot包"虚胖"了!这份瘦身攻略请收好
java·spring boot·后端
掘金者阿豪2 小时前
MiGPT GUI给小爱音箱装「AI 大脑」,自定义人设 + 百变音色!cpolar 内网穿透实验室第 726 个成功挑战
前端·后端
盐水冰2 小时前
【烘焙坊项目】后端搭建(13)- 数据统计--图形报表
java·后端·学习·spring
野犬寒鸦2 小时前
从零起步学习计算机操作系统:I/O篇
服务器·开发语言·网络·后端·面试
后端不背锅2 小时前
分布式事务解决方案:2PC、3PC、TCC、Saga
后端
二闹2 小时前
Python中@classmethod和@staticmethod的真正区别懂了吗?
后端·python