EasyExcel使用
说明:EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。(官方语,官网:https://easyexcel.opensource.alibaba.com/)
本文介绍EasyExcel使用,读取下面这个excel文件
简单使用
(1)创建项目
创建一个Maven项目,pom文件如下:
<?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 的依赖
<!-- easyexcel依赖 --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.3</version></dependency>
(2)创建pojo对象
定义一个 pojo 对象,与读取的数据对应
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文件中的列名,也可以填列的序号,如下,填列名比较好些,一眼就能知道对应关系。
@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 接口,如下:
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 读取内容上下文,可以用来获取当前行号*/@Overridepublic void invoke(Student student, AnalysisContext analysisContext) {studentData.add(student);}/*** 解析完成后执行的方法** @param analysisContext 读取内容上下文,可以用来获取当前行号*/@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {System.out.println("读取完成");}
}
(4)使用
接下来就能使用了,创建一个上传文件的接口,传入一个 List 集合,用于接收读取的数据。这里用 linkedList 是保证读取的数据顺序与excel文件中的顺序一致。
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 文件也能使用这个类。
如下
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);}
}
错误信息对象如下
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 错误信息*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorInfo {/*** 行索引*/private int rowIndex;/*** 列索引*/private int columnIndex;/*** 错误信息*/private String message;
}
接着,改造读取学生数据监听器,如下,解析后进行相关的校验,没问题再加入到返回结果中
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 读取内容上下文,可以用来获取当前行号*/@Overridepublic void invoke(Student student, AnalysisContext analysisContext) {// 获取读取的行号Integer rowIdx = analysisContext.readRowHolder().getRowIndex();// 检查数据checkDate(student, rowIdx, 0);}/*** 解析完成后执行的方法** @param analysisContext 读取内容上下文,可以用来获取当前行号*/@Overridepublic 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));}}
}
接口使用这里,就传入一个返回结果对象
@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是我们对象内的属性,用于存储校验不通过的数据行索引,属于我们代码内部数据,没有必要返回给前端,可以在对象属性上加上这行注解,避免被序列化返回给前端。
@Getter(value = AccessLevel.NONE)private final Set<Integer> rowErrorSet = new HashSet<>();
属性上加了这个注解,类上也要加 @Getter 注解,表示该类都生成 Getter 方法,但 rowErrorSet 不生成
@Getter
public class ParseResult<T> {
这样 rowErrorSet 就不会被返回给前端了
再进一步
我们再来考虑一个问题,excel 文件中的数据有的是布尔类型,或者是枚举类型,数据值是备选列表中的一个,这种情况我们要怎么把文件中的布尔类型、枚举类型,转为我们代码中的true、false或者是枚举型的code值?
首先,先在对象中增加这两个字段
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
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;
}
这时,需要创建两个类型转换器,如下:
(布尔类型转换器)
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*/@Overridepublic Boolean convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,GlobalConfiguration globalConfiguration) {return "是".equals(cellData.getStringValue());}/*** javaBean 转 excel*/@Overridepublic WriteCellData<?> convertToExcelData(Boolean value, ExcelContentProperty contentProperty,GlobalConfiguration globalConfiguration) {String strValue = value ? "是" : "否";return new WriteCellData<>(strValue);}
}
(成绩枚举类型转换器)
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*/@Overridepublic String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,GlobalConfiguration globalConfiguration) {return ScoreEnum.getEnumByDesc(cellData.getStringValue()).getCode();}/*** javaBean 转 excel*/@Overridepublic WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,GlobalConfiguration globalConfiguration) {return new WriteCellData<>(ScoreEnum.getEnumByDesc(value).getDesc());}
}
成绩枚举里要增加两个静态方法,用于根据code、desc查对应的枚举项
/*** 根据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 属性里,指定对应的转换器,如下:
@ExcelProperty(value = "是否成年", converter = BooleanConverter.class)private Boolean adultOrNot;@ExcelProperty(value = "成绩", converter = ScoreEnumConverter.class)private String score;
OK,读取监听器这里,写入数据时,加上这两个字段
// 该行没有错误才加入到返回数据集合中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 的使用,以及可能遇到的问题