提起Excel框架,比较知名的Apache-POI等框架,针对Excel的处理提供了非常丰富的功能,但是在处理大数据量级的Excel文件时,会占用极高的内存,
而且无论是导入还是导出速度都相对较慢,于是阿里巴巴的EasyExcel应运而生。EasyExcel是一个高效、低内存占用的Excel处理框架,提供了简洁易用的API接口,
使得我们能更加高效、灵活的处理Excel文件。当然,EasyExcel并非一个完全从零构建的开源项目,而是在Apache-POI的基础上做了进一步封装得到的。
接下来,以如下Excel文件为例,讲解基于EasyExcel的数据解析

读取Excel数据
我们只需要调用EasyExcel的read方法的即可,该方法需要传入两个参数,inputStream代表所读取的Excel文件输入流,ReadListener用来处理每一行读取到的数据。又因为每一行数据的处理随着业务的不同而不同,所以我们需要在调用read方法时自己传入ReadListener接口的实现子类对象(可以使用匿名内部类对象,或者lambda表达式)
/**
inputStream: 表示我们读取的Excel文件的输入流
clz: Excel文件实体类对应的映射实体类的Class对象
readListener: 数据读取监听器,每一行的数据都交给readListner来处理
*/
public static ExcelReaderBuilder read(InputStream inputStream, Class head, ReadListener readListener)
如果我们希望通过前端页面以文件上传的方式读取Excel文件内容,代码如下:
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
// 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
ReadListener<XXX> readListener= ....
// 2. 定义用来封装Excel每行数据的实体类的Class对象
Class<XXX> rowClass = XXX.class;
// 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
EasyExcel.read(file.getInputStream(), rowClass, readListener)
// 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
.sheet()
// 开始读取,方法执行完毕,那么整个Excel文件也读取完了
.doRead();
return "success";
}
也就是说,我们只需要调用EeasyExcel.read(…).sheet().doRead()方法即可完成按行对Excel文件内容的读取了。但是,在read方法中涉及到了如下两个参数,我们需要详细了解:
- 用来封装Excel行数据的映射实体类(有点类似于读取数据库表中的行数据)
- 用来读取到Excel行数据的ReadListener。(我们的主要代码就在这个ReadListener中)
映射实体类
Excel中的一行数据可以映射为一个对象(类似于数据库表中的一行数据也可以映射为一个对象)。因此我们在解析数据前需要先定义实体类,并且完成实体类属性与Excel文件字段的映射。
实体类成员变量与Excel字段的映射可以使用@ExcelProperty注解的index属性或者value属性定义:
- index属性: 定义的是实体类属性与Excel字段的列下标之间的映射(Excel中的列下标从0开始)
- value属性: 定义的是实体类属性与Excel字段名称之间的映射
两种方式都可以完成实体类属性与Excel字段的映射,但是对于某一个属性而言要么使用index,要么使用value属性,不要同时使用
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
/**
* 将该成员变量映射到excel的第3列即数字标题那个字段(列数从0开始所以第3列的index是2)
*/
@ExcelProperty(index = 2)
private Double doubleData;
/**
* 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
*/
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
}
ReadListener数据读取监听器
实际上EasyExcel读取Excel文件数据的方式是一行一行读取的,每读取一行数据就会将数据转化为实体类对象,并且交给ReadListener来处理,ReadListener的泛型类型在使用时需要指定为Excel实体类。
可以看到ReadListener主要包含3个方法:
- invoke方法: 通过data接收当前读取的一行数据转化而成的对象,还可以通过context.readRowHolder().getRowIndex()方法指定当前读取的行下标(行下标从0开始)
- hasNext方法: 决定是否继续接着继续解析剩下的Excel数据,如果返回false则不会继续向下读取数据,相当于终止数据读取过程,默认始终返回true。
- doAfterAllAnalysed:当excel数据全部成功解析完毕时,doAfterAllAnalysed方法会被调用。
/**
实际上如果我们定义了Excel文件的映射实体类
*/
public interface ReadListener<T> extends Listener {
/**
* When analysis one row trigger invoke function.
*
* @param data one row value. It is same as {@link AnalysisContext#readRowHolder()}
* @param context analysis context
*/
void invoke(T data, AnalysisContext context);
/**
* Verify that there is another piece of data.You can stop the read by returning false
*
* @param context
* @return
*/
default boolean hasNext(AnalysisContext context) {
return true;
}
/**
* if have something to do after all analysis
*
* @param context
*/
void doAfterAllAnalysed(AnalysisContext context);
}
定义处理示例Excel文件的ReadLisner子类如下(实际使用的时候也可以不用定义子类,使用lambda表达式或者匿名内部类对象都可以):
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {
/**
* 假设这个是一个Mapper,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private DemoMapper demoMapper;
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/
public DemoDataListener(DemoMapper demoMapper) {
this.demoMapper = demoMapper;
}
/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(DemoData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
// 向数据库保存一条数据
saveData(data);
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData(DemoData data) {
log.info("开始存储数据库!");
demoMapper.insert(data);
log.info("存储数据库成功!");
}
}
所以针对课件中给出的Excel数据的读取,完整的读取代码为:
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
// 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
ReadListener<DemoData> readListener = new DemoDataListener(demoMapper);
// 2. 定义用来封装Excel每行数据的实体类的Class对象
Class<DemoData> rowClass = DemoData.class;
// 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
EasyExcel.read(file.getInputStream(), rowClass, readListener)
// 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
.sheet()
// 开始读取,方法执行完毕,那么整个Excel文件也读取完了
.doRead();
return "success";
}
读取Excel头
ReadLisner中的invoke方法可以让我们获取到Excel文件中的Excel数据,但是如果我们还想获取Excel文件头数据呢?

此时我们可以使用ReadListener的一个抽象子类来完成——AnalysisEventListener,定义如下:
/**
* Receives the return of each piece of data parsed
*
* @author jipengfei
*/
public abstract class AnalysisEventListener<T> implements ReadListener<T> {
// 此方法我们无需关心,是框架内部使用
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
invokeHeadMap(ConverterUtils.convertToStringMap(headMap, context), context);
}
/**
* 这个方法才是我们用来获取Excel文件头的
* Returns the header as a map.Override the current method to receive header data.
*
* @param headMap: key为Excel列下标,value为该列的名称
*/
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {}
}
假设我们要是用AnalysisEventListener既读取示例Excel文件头数据,又要读取文件内容,我们可以定义自己的AnalysisEventListener的子类即可
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
public class MyHeaderReadListener extends AnalysisEventListener<DemoData> {
// 如果需要使用其他Mapper,或者Service的话,可以定义成员变量,以及对应的构造方法
// 我们只需要重写invokeHeadMap方法即可获取Excel文件头
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
// 通过headMap这个方法参数,获取Excel文件头对应下标的列名称
}
// 该方法就是ReadListener父接口中定义的方法,用来读取行数据
@Override
public void invoke(DemoData data, AnalysisContext context) {
}
// 该方法就是ReadListener父接口中定义的方法,在excel读取完毕后执行
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
在使用的时候,代码几乎是一样的
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
// 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
AnalysisEventListener<DemoData> analysisEventListener = new MyHeaderReadListener(....);
// 2. 定义用来封装Excel每行数据的实体类的Class对象
Class<DemoData> rowClass = DemoData.class;
// 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
EasyExcel.read(file.getInputStream(), rowClass, readListener)
// 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
.sheet()
// 开始读取,方法执行完毕,那么整个Excel文件也读取完了
.doRead();
return "success";
}
批量处理Excel数据
一个Excel数据中可能有上万条数据,有时我们想要批量处理,一次处理多行Excel数据怎么办呢?此时我们就可以对读取数据的DemoDataListener或者MyHeaderReadListener稍加改造即可,这里以DemoDataListener为例:
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {
/**
* 每100条数据处理一次,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* 假设这个是一个Mapper,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private DemoMapper demoMapper;
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/
public DemoDataListener(DemoMapper demoMapper) {
this.demoMapper = demoMapper;
}
/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(DemoData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
// 利用通用mapper,或者通用Service批量保存数据到数据库
log.info("存储数据库成功!");
}
}
改造完成之后的使用还是和之前一模一样:
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
// 1.创建用于读取数据的ReadListener对象, 泛型类型表示封装Excel每行数据的实体类
AnalysisEventListener<DemoData> analysisEventListener = new MyHeaderReadListener(....);
// 2. 定义用来封装Excel每行数据的实体类的Class对象
Class<DemoData> rowClass = DemoData.class;
// 3. 调用EeasyExcel类的静态方法read,按行读取Excel文件中的数据
EasyExcel.read(file.getInputStream(), rowClass, readListener)
// 指定读取的sheet我们的一个excel只使用默认的一个sheet所以不用指定
.sheet()
// 开始读取,方法执行完毕,那么整个Excel文件也读取完了
.doRead();
return "success";
}
Excel的上传分为了两个步骤:
- 校验: 当我们点击上传,并选择Excel文件后,前端页面首先会调用后端接口,校验Excel文件内容的正确性
- 上传: 当Excel文件中的数据都校验无误,才能点击确定,开始上传Excel数据
所以接下来,简单说明一下校验和上传
Excel数据校验
关于词汇管理的Excel文件我们可以校验如下内容:
- 校验单元格中是否有空数据(是否有对象的属性值为null)
- 校验单词格式是否为 词性.词义
- 校验例句格式是否为: 英文例句\n例句中文释义
- 校验单词所属的书籍或者章节是否存在
在以上校验中,如何一个校验有问题,都可以抛出异常ExcelParseException(项目中给大家提供),例如
public class ExcelParseException extends BusinessException {
// 行数
private int rowNum;
// 列数
private int colNum;
// 单元格值
private String cellValue;
public ExcelParseException(ResultCodeEnum codeEnum, int rowNum, int colNum, String cellValue, String message) {
super(message, codeEnum.getCode());
this.rowNum = rowNum;
this.colNum = colNum;
this.cellValue = cellValue;
}
public ExcelParseException(ResultCodeEnum codeEnum, int rowNum, String message) {
super(message, codeEnum.getCode());
this.rowNum = rowNum;
this.colNum = -1;
this.cellValue = null;
}
public ExcelParseException(ResultCodeEnum codeEnum, String message) {
super(message, codeEnum.getCode());
this.rowNum = -1;
this.colNum = -1;
this.cellValue = null;
}
@Override
public String getMessage() {
if (rowNum >= 0 && colNum >= 0 && cellValue != null) {
return String.format("Excel解析错误(行:%d,列:%d,值:%s) - %s",
rowNum, colNum, cellValue, super.getMessage());
} else if (rowNum >= 0) {
return String.format("Excel解析错误(行:%d) - %s", rowNum, super.getMessage());
} else {
return String.format("Excel解析错误 - %s", super.getMessage());
}
}
}
@Autowired
VocService vocService;
@PostMapping("/admin/voc/excel/validate")
public Result vocExcelVerify(@RequestParam("vocExcel") MultipartFile file) throws IOException {
// 在该service方法中完成Excel文件内容的读取
vocService.vocExcelValidate(file);
return Result.ok();
}
Excel数据上传
关于Excel文件上传需要注意的是:
- 最好能够批量处理,将多行数据在一个事务中处理
- 上传数据的过程中一旦出现问题,抛出ExcelParseException
@Autowired
VocService vocService;
@PostMapping("/admin/voc/excel")
public Result addVocExcel(@RequestParam("vocExcel") MultipartFile file) throws IOException {
// 获取当前登录的后台用户id
Long employeeId = StpKit.ADMIN.getLoginIdAsLong();
// 调用该方法完成Excel上传单词功能
vocService.addVocExcel(file, employeeId);
return Result.ok();
}
Excel模版下载
excel文件的下载没什么技术难度,代码直接给到大家,代码如下:
public ResponseEntity<byte[]> downloadTemplate() throws IOException {
public ResponseEntity<byte[]> downloadTemplate() throws IOException {
// 使用ClassPathResource读取资源文件
ClassPathResource resource;
resource = new ClassPathResource("templates/单词上传模版.xlsx");
// 检查文件是否存在
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
// 读取文件内容
InputStream inputStream = resource.getInputStream();
byte[] fileBytes = IOUtils.toByteArray(inputStream);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment",
URLEncoder.encode(resource.getFilename(), "UTF-8"));
return ResponseEntity.ok()
.headers(headers)
.body(fileBytes);
}