说明:EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。(官方语,官网:https://easyexcel.opensource.alibaba.com/)
本文介绍EasyExcel使用,读取下面这个excel文件

简单使用
(1)创建项目
创建一个Maven项目,pom文件如下:
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>2.7.12</version>
<relativePath/>
</parent>
<groupId>com.hezy</groupId>
<artifactId>excel_parse_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<!-- easyexcel依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
其中,下面这个是 easyexcel 的依赖
xml
<!-- easyexcel依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.3</version>
</dependency>
(2)创建pojo对象
定义一个 pojo 对象,与读取的数据对应
java
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 学生对象
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
@ExcelProperty("学号")
public String no;
@ExcelProperty("姓名")
public String name;
@ExcelProperty("性别")
public String sex;
@ExcelProperty("班级")
public String room;
}
这里的@ExcelProperty
注解内可以填excel文件中的列名,也可以填列的序号,如下,填列名比较好些,一眼就能知道对应关系。
java
@ExcelProperty(index = 1)
public String no;
@ExcelProperty(index = 2)
public String name;
@ExcelProperty(index = 3)
public String sex;
@ExcelProperty(index = 4)
public String room;
另外,个人经验,项目中凡涉及解析、序列化操作的对象,最好实现其全参构造、无参构造方法,并实现序列化接口。
(3)创建读取监听器
创建一个读取监听器,实现 EasyExcel 接口,如下:
java
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.hezy.pojo.Student;
import java.util.List;
/**
* 读取学生数据监听器
*/
public class StudentReadListener implements ReadListener<Student> {
/**
* 返回对象
*/
private final List<Student> studentData;
public StudentReadListener(List<Student> studentList) {
this.studentData = studentList;
}
/**
* 这里每次读取一行都会进行回调
*
* @param student 逐行解析封装完成的学生对象
* @param analysisContext 读取内容上下文,可以用来获取当前行号
*/
@Override
public void invoke(Student student, AnalysisContext analysisContext) {
studentData.add(student);
}
/**
* 解析完成后执行的方法
*
* @param analysisContext 读取内容上下文,可以用来获取当前行号
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("读取完成");
}
}
(4)使用
接下来就能使用了,创建一个上传文件的接口,传入一个 List 集合,用于接收读取的数据。这里用 linkedList 是保证读取的数据顺序与excel文件中的顺序一致。
java
import com.alibaba.excel.EasyExcel;
import com.hezy.listener.StudentReadListener;
import com.hezy.pojo.Student;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.LinkedList;
import java.util.List;
@RestController
public class FileController {
@PostMapping("/import")
public List<Student> parseDeviceSummaryExcel(MultipartFile file) throws Exception {
// 定义一个结果
List<Student> result = new LinkedList<>();
// 注意这里定义表头占一行,默认取sheet1中的数据
EasyExcel.read(file.getInputStream(), Student.class,
new StudentReadListener(result)).headRowNumber(1).sheet(0).doRead();
return result;
}
}
调用,发送,一把过

控制台可见执行了解析完成的代码

更近一步
一般来说,开放 excel 模板给用户,填写的数据百分百是有不符合校验的,所以说我们最好能设计一个返回对象
,返回能通过校验的有用数据,和不能通过的校验的错误信息。另外,考虑到复用性,这个对象要设计成通用的,读取其他 excel 文件也能使用这个类。
如下
java
import java.util.*;
/**
* 解析结果
* @param <T>
*/
public class ParseResult<T> {
/**
* 错误信息
*/
private final List<ErrorInfo> errors = new LinkedList<>();
/**
* 联系人信息
*/
private final List<T> parseData = new LinkedList<>();
/**
* 数据校验不通过的数据的行号
*/
private final Set<Integer> rowErrorSet = new HashSet<>();
/**
* 添加错误信息
*
* @param rowIndex 行索引
* @param columnIndex 列索引
* @param message 错误信息
*/
public void addError(int rowIndex, int columnIndex, String message) {
// 添加错误信息
errors.add(new ErrorInfo(rowIndex + 1, columnIndex + 1, message));
// 行号加入到集合中
rowErrorSet.add(rowIndex);
}
/**
* 添加数据到返回结果中
*
* @param data 数据对象
*/
public void addData(T data) {
parseData.add(data);
}
/**
* 判断该行是否有错误,有错误则不添加到返回结果中
*
* @param row 行号
* @return true 表示有错误,false 表示没有错误
*/
public boolean hasError(int row) {
return rowErrorSet.contains(row);
}
}
错误信息对象如下
java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 错误信息
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorInfo {
/**
* 行索引
*/
private int rowIndex;
/**
* 列索引
*/
private int columnIndex;
/**
* 错误信息
*/
private String message;
}
接着,改造读取学生数据监听器,如下,解析后进行相关的校验,没问题再加入到返回结果中
java
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.hezy.pojo.ParseResult;
import com.hezy.pojo.Student;
import org.apache.commons.lang3.StringUtils;
/**
* 读取学生数据监听器
*/
public class StudentReadListener implements ReadListener<Student> {
/**
* 返回对象
*/
private final ParseResult parseResult;
public StudentReadListener(ParseResult parseResult) {
this.parseResult = parseResult;
}
/**
* 这里每次读取一行都会进行回调
*
* @param student 逐行解析封装完成的学生对象
* @param analysisContext 读取内容上下文,可以用来获取当前行号
*/
@Override
public void invoke(Student student, AnalysisContext analysisContext) {
// 获取读取的行号
Integer rowIdx = analysisContext.readRowHolder().getRowIndex();
// 检查数据
checkDate(student, rowIdx, 0);
}
/**
* 解析完成后执行的方法
*
* @param analysisContext 读取内容上下文,可以用来获取当前行号
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("读取完成");
}
/**
* 这里进行行数据校验
*
* @param student 行数据
* @param rowIdx 行号
* @param offset 列号,应与你所判断的字段所处列一致
*/
public void checkDate(Student student, int rowIdx, int offset) {
String no = student.getNo();
if (StringUtils.isBlank(no)) {
parseResult.addError(rowIdx, offset, "学号不能为空");
}
String name = student.getName();
if (StringUtils.isBlank(name) || name.length() > 20) {
parseResult.addError(rowIdx, 1 + offset, "姓名不能为空,并且不能超过20个字");
}
String sex = student.getSex();
if (StringUtils.isBlank(sex) || sex.length() > 10) {
parseResult.addError(rowIdx, 2 + offset, "性别不能为空,并且不能超过10个字");
}
String room = student.getRoom();
if (StringUtils.isBlank(room) || room.length() > 20) {
parseResult.addError(rowIdx, 3 + offset, "班级不能为空,并且不能超过20个字");
}
// 该行没有错误才加入到返回数据集合中
if (!parseResult.hasError(rowIdx)) {
parseResult.addData(new Student(no, name, sex, room));
}
}
}
接口使用这里,就传入一个返回结果对象
java
@PostMapping("/import")
public ParseResult<Student> parseDeviceSummaryExcel(MultipartFile file) throws Exception {
// 定义一个结果
ParseResult<Student> result = new ParseResult<>();
// 注意这里定义表头占一行,默认取sheet1中的数据
EasyExcel.read(file.getInputStream(), Student.class,
new StudentReadListener(result)).headRowNumber(1).sheet(0).doRead();
return result;
}
调用,测试,把 excel 文件中的数据,随便删掉几个,使校验不通过

返回结果里有校验通过,能用的数据,也有校验不通过的错误信息,还提供了错误的单元格位置,就很nice

其中rowErrorSet是我们对象内的属性,用于存储校验不通过的数据行索引,属于我们代码内部数据,没有必要返回给前端,可以在对象属性上加上这行注解,避免被序列化返回给前端。
java
@Getter(value = AccessLevel.NONE)
private final Set<Integer> rowErrorSet = new HashSet<>();
属性上加了这个注解,类上也要加 @Getter 注解,表示该类都生成 Getter 方法,但 rowErrorSet 不生成
java
@Getter
public class ParseResult<T> {
这样 rowErrorSet 就不会被返回给前端了

再进一步
我们再来考虑一个问题,excel 文件中的数据有的是布尔类型,或者是枚举类型,数据值是备选列表中的一个,这种情况我们要怎么把文件中的布尔类型、枚举类型,转为我们代码中的true、false或者是枚举型的code值?

首先,先在对象中增加这两个字段
java
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 学生对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable {
@ExcelProperty("学号")
public String no;
@ExcelProperty("姓名")
public String name;
@ExcelProperty("性别")
public String sex;
@ExcelProperty("班级")
public String room;
@ExcelProperty("是否成年")
private Boolean adultOrNot;
@ExcelProperty("成绩")
private String score;
}
其中成绩,对应的是枚举,如下,excel 文件中填的是枚举的 desc,但是我们代码中需要的是枚举的 code
java
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 成绩枚举
*
* @author hezy
* @version 1.0.0
* @create 2025/7/19 18:01
*/
@AllArgsConstructor
@Getter
public enum ScoreEnum {
A("A", "优秀"),
B("B", "良好"),
C("C", "及格");
private final String code;
private final String desc;
}
这时,需要创建两个类型转换器,如下:
(布尔类型转换器)
java
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* 布尔类型转换器
*/
public class BooleanConverter implements Converter<Boolean> {
/**
* excel 转 javaBean
*/
@Override
public Boolean convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return "是".equals(cellData.getStringValue());
}
/**
* javaBean 转 excel
*/
@Override
public WriteCellData<?> convertToExcelData(Boolean value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
String strValue = value ? "是" : "否";
return new WriteCellData<>(strValue);
}
}
(成绩枚举类型转换器)
java
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.hezy.enums.ScoreEnum;
/**
* 成绩枚举转换器
*
* @author hezy
* @version 1.0.0
* @create 2025/7/19 18:12
*/
public class ScoreEnumConverter implements Converter<String> {
/**
* excel 转 javaBean
*/
@Override
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return ScoreEnum.getEnumByDesc(cellData.getStringValue()).getCode();
}
/**
* javaBean 转 excel
*/
@Override
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return new WriteCellData<>(ScoreEnum.getEnumByDesc(value).getDesc());
}
}
成绩枚举里要增加两个静态方法,用于根据code、desc查对应的枚举项
java
/**
* 根据desc获取枚举
*/
public static ScoreEnum getEnumByDesc(String desc) {
return Arrays.stream(ScoreEnum.values())
.filter(scoreEnum -> scoreEnum.getDesc().equals(desc))
.findFirst()
.orElse(null);
}
/**
* 根据desc获取枚举
*/
public static ScoreEnum getEnumByCode(String code) {
return Arrays.stream(ScoreEnum.values())
.filter(scoreEnum -> scoreEnum.getDesc().equals(code))
.findFirst()
.orElse(null);
}
回到对象上,在学生对象属性上,@ExcelProperty 属性里,指定对应的转换器,如下:
java
@ExcelProperty(value = "是否成年", converter = BooleanConverter.class)
private Boolean adultOrNot;
@ExcelProperty(value = "成绩", converter = ScoreEnumConverter.class)
private String score;
OK,读取监听器这里,写入数据时,加上这两个字段
java
// 该行没有错误才加入到返回数据集合中
if (!parseResult.hasError(rowIdx)) {
parseResult.addData(new Student(no, name, sex, room, student.getAdultOrNot(), student.getScore()));
}
调用接口,查看返回值,可见对应属性的值被转换成了布尔类型、枚举类型对应枚举项的code值

可能遇到的问题
使用 EasyExcel 时,如果你没有遇到问题,那么万事大吉,如果遇到了问题,数据解析不出来,或者解析出来的数据都是默认值,0、null 这些,需要关注以下两个地方:
-
使用了lombok注解给对象生成 Setter/Getter 方法,可能会导致数据无法写入到对象,可手动生成 Setter/Getter 方法;
-
对象属性名有以"is"开头的,导致数据无法写入,这个在阿里巴巴开发手册中亦有记载,不要以"is"开头给属性命名;
总结
本文介绍了 EasyExcel 的使用,以及可能遇到的问题