文章目录
- 前言
- 一、简介
- 二、使用步骤
- 1. 引入依赖
- 2. 前提准备
- 3. 实现导出
- 4. 实现导入
- 三、我所遇到的问题
- 四、总结
前言
在日常开发中经常会遇到一些 excel
表导入导出的需求,以往会使用 POI
封装成工具类来处理这些导入导出的需求,但是 POI
在导入大文件时非常占用内存,甚至出现 OOM
,所以目前很多公司都会使用节省内存的 EasyExcel
,虽然说在网上关于 EasyExcel
的教程五花八门的有很多,我从中也学到不少,不过这里我还是将目前我项目中使用的方式总结一下分享出来。
一、简介
官网:https://easyexcel.opensource.alibaba.com/
官方文档:https://easyexcel.opensource.alibaba.com/docs/current/
EasyExcel
是 alibaba
开源的一个 excel
处理框架,底层是对 POI
的封装,其最大的特点就是 使用简单、节省内存
,不同于 POI
的一次性将 excel
文件内容全部读取然后加载到内存中再做处理,EasyExcel
是从磁盘中一行行读取数据,逐个解析,并将解析后的结果以观察者的模式通知处理。
特点:
- 性能高效:采用了异步导入导出的方式,并且底层使用了
NIO
技术实现,使其在导入导出大量数据时的性能非常高效 - 易于使用:提供了简单易用的
API
,用户可以通过少量的代码实现导入导出功能 - 功能强大:除了最基本的导入导出功能,还可以进行合并单元格、数据校验、自定义样式等增强功能
- 扩展性好:用户可以自定义
Converter
对自定义类型进行转换,或者继承EasyExcelListener
来自定义监听器实现更加灵活的需求
二、使用步骤
1. 引入依赖
<!-- easyExcel 表格依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
当引入了 easyexcel
的依赖之后,相当于间接的引入了 poi
、poi-ooxml
的依赖,如果你项目中已经引入了 POI
的话,就可能出现兼容问题,所以我推荐引入较高版本的 easyexcel
,这里我是使用了 easyexcel-3.3.2
最新一版的依赖解决了这一冲突的。
2. 前提准备
这里我使用我数据库中的 访问日志
- t_access_log
作为导入和导出的数据,表数据如下:
select * from t_access_log;
实体类:
package com.mike.bean.inner.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.mike.common.core.domain.web.entity.BaseEntity;
import com.mike.common.core.domain.web.entity.GaeaBaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
/**
* <p>
* 访问记录表
* </p>
*
* @author mike
* @since 2023-05-30
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_access_log")
@EqualsAndHashCode(callSuper = true)
@ApiModel(value="AccessLog对象", description="访问记录表")
public class AccessLog extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "自增主键")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "登录名")
private String loginName;
@ApiModelProperty(value = "访问路径")
private String accessPath;
@ApiModelProperty(value = "访问IP")
private String accessIp;
@ApiModelProperty(value = "创建时间")
@TableField(value = "create_time", fill = FieldFill.INSERT)
@JsonFormat(pattern = DateFormatConstant.NORMAL)
private Date createTime;
@ApiModelProperty(value = "访问状态:0已拦截;1已放行")
private Boolean state;
}
3. 实现导出
用 easyExcel
实现导出功能还是比较简单的,几行代码就能解决,就是需要定义一个输出的对象,如果我要导出我的访问记录表数据,那么我的输出对象可以这么写:
package com.mike.server.system.domain.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.*;
import com.alibaba.excel.enums.poi.BorderStyleEnum;
import com.alibaba.excel.enums.poi.FillPatternTypeEnum;
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
// 头背景设置
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
//标题高度
@HeadRowHeight(40)
//内容高度
@ContentRowHeight(30)
//内容居中,左、上、右、下的边框显示
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
public class AccessLogEasyVo {
@ApiModelProperty(value = "自增主键")
@ExcelProperty("自增主键")
// 格子宽度
@ColumnWidth(15)
private Integer id;
@ApiModelProperty(value = "登录名")
@ExcelProperty("登录名")
@ColumnWidth(15)
private String loginName;
@ApiModelProperty(value = "访问路径")
@ExcelProperty("访问路径")
@ColumnWidth(15)
private String accessPath;
@ApiModelProperty(value = "访问IP")
@ExcelProperty("访问IP")
@ColumnWidth(15)
private String accessIp;
@ApiModelProperty(value = "创建时间")
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ColumnWidth(15)
private Date createTime;
@ApiModelProperty(value = "访问状态:0已拦截;1已放行")
@ExcelProperty("访问状态")
@ColumnWidth(15)
private Boolean state;
}
表头信息用注解 @ExcelProperty("表头名称")
表示,其它注解就不一一说明了,可查阅官方文档
代码编写:
TestController.java
@GetMapping("/export/easy")
@ApiOperation(value = "示例:导出", produces = "application/octet-stream")
public ResponseBean<String> easyExport(HttpServletResponse response) {
testService.easyExport(response);
return ResponseBean.success();
}
TestService.java
void easyExport(HttpServletResponse response);
TestServiceImpl.java
@Override
public void easyExport(HttpServletResponse response) {
/*
* 以导出 access-log 中的数据为例
*
* 使用 阿里 的 easyExcel 框架进行导出
*/
// 设置响应体
String finalFileName = "文件名称" + "_(截止"+ StringUtils.getNowTimeStr(DateFormatConstant.Y0M0D)+")";
// 设置content—type 响应类型
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
try {
// 这里URLEncoder.encode可以防止中文乱码
finalFileName = URLEncoder.encode(finalFileName, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
response.setHeader("Content-disposition", "attachment;filename=" + finalFileName + ".xlsx");
// 先将需要导出的数据查出来
List<AccessLog> accessLogs = accessLogMapper.selectAll();
// 封装 vo 对象,vo 对象中的字段上添加了 @ExcelProperty,与 excel 表头相对应
List<AccessLogEasyVo> accessLogEasyVos = CopyUtils.copyList(accessLogs, AccessLogEasyVo.class);
try {
EasyExcel.write(response.getOutputStream(), AccessLogEasyVo.class)
.sheet("Sheet1").doWrite(accessLogEasyVos);
} catch (IOException e) {
log.error("export excel error:",e);
throw new CommonException(ExceptionEnum.EXPORT_EXCEL_ERROR);
}
}
因为设置响应体这段代码基本上是固定格式的,所以可以抽出来
package com.mike.common.core.utils.excel;
import com.mike.common.core.constant.DateFormatConstant;
import com.mike.common.core.utils.StringUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class EasyExcelUtil {
/**
* 初始化响应体
* @param response 请求头
* @param fileName 导出名称
*/
public static void initResponse(HttpServletResponse response, String fileName) {
// 最终文件名:文件名_(截止yyyy-MM-dd) --> 这块地方得根据你们自己项目做更改了
String finalFileName = fileName + "_(截止"+ StringUtils.getNowTimeStr(DateFormatConstant.Y0M0D)+")";
// 设置content—type 响应类型
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
try {
// 这里URLEncoder.encode可以防止中文乱码
finalFileName = URLEncoder.encode(finalFileName, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
response.setHeader("Content-disposition", "attachment;filename=" + finalFileName + ".xlsx");
}
}
TestService.java
简化为:
@Override
public void easyExport(String loginName) {
// 设置响应体
EasyExcelUtil.initResponse(response, "文件名称");
// 先将需要导出的数据查出来
List<AccessLog> accessLogs = accessLogMapper.selectAll();
// 封装 vo 对象,vo 对象中的字段上添加了 @ExcelProperty,与 excel 表头相对应
List<AccessLogEasyVo> accessLogEasyVos = CopyUtils.copyList(accessLogs, AccessLogEasyVo.class);
try {
EasyExcel.write(response.getOutputStream(), AccessLogEasyVo.class)
.sheet("Sheet1").doWrite(accessLogEasyVos);
} catch (IOException e) {
log.error("export excel error:",e);
throw new CommonException(ExceptionEnum.EXPORT_EXCEL_ERROR);
}
}
基本套路就是:① 设置下导出文件响应体信息;② 查询数据;③ 转换成 easyExcel
的输出对象;④ 使用 EasyExcel
导出
相关工具类:CopyUtils.java
package com.mike.common.core.utils;
import com.mike.common.core.utils.bean.BeanUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
public class CopyUtils {
/**
* 复制集合
*/
public static <T,K> List<T> copyList(List<K> sourceList, Class<T> clazz) {
if (CollectionUtils.isEmpty(sourceList)) {
return null;
}
ArrayList<T> target = new ArrayList<>();
sourceList.forEach(k -> target.add(convert(k, clazz)));
return target;
}
/**
* 复制对象
*/
public static <T,K> T convert(K source, Class<T> clazz) {
T t = BeanUtils.instantiateClass(clazz);
BeanUtils.copyProperties(source, t);
return t;
}
}
测试:
4. 实现导入
导入的代码相对于导出而已会复杂一点,前面说到 EasyExcel
会一行行的从表格当中读取数据并进行解析,再将解析后的结果以观察者模式通知处理,再官方的文档中是使用到了 ReadListener
这样的一个监听器来处理这些结果数据
我们可以按照官方文档的方式去实现 ReadListener
类,或者去继承ReadListener
的抽象类 AnalysisEventListener
,这里我是采用了继承 AnalysisEventListener
的写法,代码如下:
ExcelDateListener.java
package com.mike.common.core.domain.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class ExcelDateListener<M> extends AnalysisEventListener<M> {
private final ExcelReaderListenerCallback<M> callback;
// 每隔50条存储数据库,实际使用中可以3000条,然后清理list,方便内存回收
private static final int BATCH_COUNT = 50;
// 表头数据
Map<Integer,String> headMap=new HashMap<>();
// 缓存数据
List<M> cacheList = new ArrayList<>();
public ExcelDateListener(ExcelReaderListenerCallback<M> callback) {
this.callback = callback;
}
/**
* 这里会一行行的返回头
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
this.headMap=headMap;
log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
}
@Override
public void invoke(M data, AnalysisContext analysisContext) {
cacheList.add(data);
// 在这里可以做一些其他的操作,就靠自己去拓展了
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cacheList.size() >= BATCH_COUNT) {
// 这里是存数据库的操作
callback.convertData(cacheList,headMap);
// 存储完成清理 list
cacheList.clear();
}
}
/**
* 所有数据解析完成了 都会来调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
callback.convertData(cacheList,headMap);
cacheList.clear();
}
/**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行
*/
@Override
public void onException(Exception exception, AnalysisContext context) {
// 如果是某一个单元格的转换异常 能获取到具体行号
// 如果要获取头的信息 配合invokeHeadMap使用
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
log.error("第{}行,第{}列解析异常", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex());
}
}
/**
* 读取条额外信息:批注、超链接、合并单元格信息等
*/
@Override
public void extra(CellExtra extra, AnalysisContext context) {
log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
switch (extra.getType()) {
case COMMENT:
log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),
extra.getText());
break;
case HYPERLINK:
if ("Sheet1!A1".equals(extra.getText())) {
log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
extra.getColumnIndex(), extra.getText());
} else if ("Sheet2!A1".equals(extra.getText())) {
log.info(
"额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"
+ "内容是:{}",
extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
extra.getLastColumnIndex(), extra.getText());
} else {
log.error("Unknown hyperlink!");
}
break;
case MERGE:
log.info(
"额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
extra.getLastColumnIndex());
break;
default:
}
}
}
ExcelReaderListenerCallback.java
package com.mike.common.core.domain.excel;
import java.util.List;
import java.util.Map;
public interface ExcelReaderListenerCallback<T> {
/**
* 数据处理
* @param data 数据
* @param headMap 表头
*/
void convertData(List<T> data, Map<Integer,String> headMap);
}
代码编写:
这里我准备了一张表格如下所示:
现在我要将这张表格数据导入到日志访问记录 t_access_log
表中
TestController.java
@ApiOperation(value = "示例:导入")
@PostMapping("/importDate/easy")
@ApiImplicitParam(name = "file", value = "文件", dataTypeClass = MultipartFile.class, required = true)
public ResponseBean<String> easyImportDate(@RequestPart("file") MultipartFile file) {
testService.easyImportDate(file);
return ResponseBean.success();
}
TestService.java
void easyImportDate(MultipartFile file);
TestServiceImpl.java
@Override
public void easyImportDate(MultipartFile file) {
/*
* 以导出 access-log 中的数据为例
*
* 使用 阿里 的 easyExcel 框架进行导入
*/
try {
EasyExcel.read(file.getInputStream(), AccessLogEasyVo.class,
new ExcelDateListener<AccessLogEasyVo>(new ExcelReaderListenerCallback<AccessLogEasyVo>() {
@Override
public void convertData(List<AccessLogEasyVo> data, Map<Integer, String> headMap) {
//导入文件表头:登录名 访问路径 访问IP 创建时间 访问状态
ArrayList<AccessLog> accessLogs = new ArrayList<>();
for (AccessLogEasyVo o : data) {
accessLogs.add(AccessLog.builder()
.loginName(o.getLoginName())
.accessPath(o.getAccessPath())
.accessIp(o.getAccessIp())
.state(o.getState())
.build());
}
if (!CollectionUtils.isEmpty(accessLogs)) {
int row = accessLogMapper.insertBatchSomeColumn(accessLogs);
log.info("insert access-log row: {}", row);
}
}
}))
//.extraRead(CellExtraTypeEnum.COMMENT) // 需要读取批注 默认不读取
//.extraRead(CellExtraTypeEnum.HYPERLINK) // 需要读取超链接 默认不读取
//.extraRead(CellExtraTypeEnum.MERGE) // 需要读取合并单元格信息 默认不读取
.sheet().doRead();
} catch (IOException e) {
log.error("import excel error:",e);
throw new CommonException(ExceptionEnum.IMPORT_EXCEL_ERROR);
}
}
这里用到了导出时所封装的输出对象 AccessLogEasyVo
类,我们要自己去编写 convertData()
处理表格数据的逻辑,这里我只是做了一个简单的数据类型转换。
测试:
从日志的打印信息中就能看到数据已经插入成功了
三、我所遇到的问题
问题一:EasyExcel
和 POI
依赖冲突问题
当导入 easyexcel
依赖后,如果你项目中以前也有导入过 poi
相关的依赖就可能会出现这个问题,主要会表现在两个方面,一个是你项目中有些关于 poi
的有些类爆红导入包失败,一个就是你在运行的时候出现报错,例如:
我项目中开始的包如下:
<!-- easyExcel 表格依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.1</version>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
引入依赖的时候项目没有报错,启动项目也没有,但是执行导入时候就报错了
然后我将 easyexcel
的版本替换成 3.3.2
就解决了这个问题
如果你还是有冲突的话,不妨看看试试以下方法:
<!-- easyexcel -->
<!-- 3+ 版本的 easyexcel,使用 poi 5+ 版本时,需要自己引入 poi 5+ 版本的包,且手动排除:poi-ooxml-schemas -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
<exclusions>
<exclusion>
<artifactId>poi-ooxml-schemas</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Excel 97-2003 工作簿 -->
<!-- 这是遵循二进制文件格式的旧 Excel 文件。该格式的文件扩展名为 .xls -->
<!-- 为了兼容性,这个依赖项也是要加的 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.2</version>
</dependency>
<!-- 用于操作 Excel 2007+ 工作簿 -->
<!-- 这是 Excel 2007 和更高版本的默认基于 XML 的文件格式。该格式的文件扩展名为 .xlsx -->
<!-- 它遵循 Office Open XML (OOXML) 格式,这是一种由 Microsoft 开发的基于 XML 的压缩文件格式,用于表示办公文档 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.2</version>
</dependency>
问题二:org.springframework.http.converter.HttpMessageNotWritableException: No converter for ...
导出文件时出错,但是文件还是能够正常导出,文件内容也是正常的
这个问题的原因就是我项目是整合了 swagger
,所以在接口上我有添加 produces = "application/octet-stream"
,表示以流的方式输出
然后在代码中又设置了一次
所以才会出现以上问题
我最后将 response.setContentType("application/vnd.ms-excel");
这段代码注释掉就解决了
四、总结
以上就是我如何整合 EasyExcel
的全部过程以及基础的使用方法,日后如果工作中遇到关于 EasyExcel
的其它一些问题或者写法我也会分享出来,比如说复杂表头的编写、动态表头的实现、合并单元格和修改单元格样式等等。
参考博客:
EasyExcel 的基本使用:https://www.cnblogs.com/aitiknowledge/archive/2022/02/28/15937517.html
EasyExcel 入门使用教程:https://backend.devrank.cn/traffic-information/7301271005489367077
EasyExcel 的基本使用:https://blog.csdn.net/weixin_42001592/article/details/128402350