目录
[2.1 开发环境](#2.1 开发环境)
[2.2 Maven依赖配置 (pom.xml)](#2.2 Maven依赖配置 (pom.xml))
[3.1 实体类代码](#3.1 实体类代码)
[3.2 注解详解](#3.2 注解详解)
[4.1 监听器设计要点](#4.1 监听器设计要点)
[5.1 服务接口](#5.1 服务接口)
[5.2 服务实现类](#5.2 服务实现类)
[6.1 自定义异常类](#6.1 自定义异常类)
[6.2 统一响应对象](#6.2 统一响应对象)
[七、打造Controller层:提供RESTful API](#七、打造Controller层:提供RESTful API)
[7.1 完整Controller代码](#7.1 完整Controller代码)
[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 监听器设计要点
-
批量处理 :通过
BATCH_COUNT控制每批处理数量,避免数据积压导致OOM -
错误收集:将校验失败的信息收集起来,解析完成后统一抛出,而不是中断导入
-
Service注入 :通过构造方法传入Service,解决监听器无法使用
@Autowired的问题 -
行号记录 :利用
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"的文件。