EasyExcel不愧是最好用的的Excel导入导出工具,性能爆表,用起来真优雅

EasyExcel不愧是最好用的的Excel导入导出工具,性能爆表,用起来真优雅

操作Excel实现导入导出是个非常常见的需求,之前介绍了一款非常好用的工具EasyPoi 。有读者提出在数据量大的情况下,EasyPoi占用内存大,性能不够好。今天给大家推荐一款性能更好的Excel导入导出工具EasyExcel,希望对大家有所帮助!

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

EasyExcel简介 EasyExcel是一款阿里开源的Excel导入导出工具,具有处理快速、占用内存小、使用方便的特点,在Github上已有22k+Star,可见其非常流行。

EasyExcel读取75M(46W行25列)的Excel,仅需使用64M内存,耗时20s,极速模式还可以更快!

集成 在SpringBoot中集成EasyExcel非常简单,仅需一个依赖即可。

com.alibaba

easyexcel

3.0.5

复制代码 使用 EasyExcel和EasyPoi的使用非常类似,都是通过注解来控制导入导出。接下来我们以会员信息和订单信息的导入导出为例,分别实现下简单的单表导出和具有一对多关系的复杂导出。

简单导出 我们以会员信息的导出为例,来体验下EasyExcel的导出功能。

首先创建一个会员对象Member,封装会员信息,这里使用了EasyExcel的注解; /**

* 购物会员

* Created by macro on 2021/10/12.

*/

@Data

@EqualsAndHashCode(callSuper = false)

public class Member {

@ExcelProperty("ID")

@ColumnWidth

private Long id;

@ExcelProperty("用户名")

@ColumnWidth

private String username;

@ExcelIgnore

private String password;

@ExcelProperty("昵称")

@ColumnWidth

private String nickname;

@ExcelProperty("出生日期")

@ColumnWidth

@DateTimeFormat("yyyy-MM-dd")

private Date birthday;

@ExcelProperty("手机号")

@ColumnWidth

private String phone;

@ExcelIgnore

private String icon;

@ExcelProperty(value = "性别", converter = GenderConverter.class)

@ColumnWidth

private Integer gender;

}

复制代码 上面代码使用到了EasyExcel的核心注解,我们分别来了解下: @ExcelProperty:核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器; @ColumnWidth:用于设置表格列的宽度; @DateTimeFormat:用于设置日期转换格式。 在EasyExcel中,如果你想实现枚举类型到字符串的转换(比如gender属性中,0->男,1->女),需要自定义转换器,下面为自定义的GenderConverter代码实现; /**

* excel性别转换器

* Created by macro on 2021/12/29.

*/

public class GenderConverter implements Converter {

@Override

public Class supportJavaTypeKey() {

//对象属性类型

return Integer.class;

}

@Override

public CellDataTypeEnum supportExcelTypeKey() {

//CellData属性类型

return CellDataTypeEnum.STRING;

}

@Override

public Integer convertToJavaData(ReadConverterContext context) throws Exception {

//CellData转对象属性

String cellStr = context.getReadCellData().getStringValue();

if (StrUtil.isEmpty(cellStr)) return null;

if ("男".equals(cellStr)) {

return 0;

} else if ("女".equals(cellStr)) {

return 1;

} else {

return null;

}

}

@Override

public WriteCellData convertToExcelData(WriteConverterContext context) throws Exception {

//对象属性转CellData

Integer cellValue = context.getValue();

if (cellValue == null) {

return new WriteCellData<>("");

}

if (cellValue == 0) {

return new WriteCellData<>("男");

} else if (cellValue == 1) {

return new WriteCellData<>("女");

} else {

return new WriteCellData<>("");

}

}

}

复制代码 接下来我们在Controller中添加一个接口,用于导出会员列表到Excel,还需给响应头设置下载excel的属性,具体代码如下; /**

* EasyExcel导入导出测试Controller

* Created by macro on 2021/10/12.

*/

@Controller

@Api(tags = "EasyExcelController", description = "EasyExcel导入导出测试")

@RequestMapping("/easyExcel")

public class EasyExcelController {

@SneakyThrows(IOException.class)

@ApiOperation(value = "导出会员列表Excel")

@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)

public void exportMemberList(HttpServletResponse response) {

setExcelRespProp(response, "会员列表");

List memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);

EasyExcel.write(response.getOutputStream())

.head(Member.class)

.excelType(ExcelTypeEnum.XLSX)

.sheet("会员列表")

.doWrite(memberList);

}

/**

* 设置excel下载响应头属性

*/

private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

response.setCharacterEncoding("utf-8");

String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");

response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

}

}

复制代码 运行项目,通过Swagger测试接口,注意在Swagger中访问接口无法直接下载,需要点击返回结果中的下载按钮才行,访问地址:http://localhost:8088/swagger-ui/

下载完成后,查看下文件,一个标准的Excel文件已经被导出了。

简单导入 接下来我们以会员信息的导入为例,来体验下EasyExcel的导入功能。

在Controller中添加会员信息导入的接口,这里需要注意的是使用@RequestPart注解修饰文件上传参数,否则在Swagger中就没法显示上传按钮了; /**

* EasyExcel导入导出测试Controller

* Created by macro on 2021/10/12.

*/

@Controller

@Api(tags = "EasyExcelController", description = "EasyExcel导入导出测试")

@RequestMapping("/easyExcel")

public class EasyExcelController {

@SneakyThrows

@ApiOperation("从Excel导入会员列表")

@RequestMapping(value = "/importMemberList", method = RequestMethod.POST)

@ResponseBody

public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {

List memberList = EasyExcel.read(file.getInputStream())

.head(Member.class)

.sheet()

.doReadSync();

return CommonResult.success(memberList);

}

}

复制代码 然后在Swagger中测试接口,选择之前导出的Excel文件即可,导入成功后会返回解析到的数据。

复杂导出 当然EasyExcel也可以实现更加复杂的导出,比如导出一个嵌套了商品信息的订单列表,下面我们来实现下!

使用EasyPoi实现

之前我们使用过EasyPoi实现该功能,由于EasyPoi本来就支持嵌套对象的导出,直接使用内置的@ExcelCollection注解即可实现,非常方便也符合面向对象的思想。

寻找方案

由于EasyExcel本身并不支持这种一对多的信息导出,所以我们得自行实现下,这里分享一个我平时常用的快速查找解决方案的办法。

我们可以直接从开源项目的issues里面去搜索,比如搜索下一对多,会直接找到有无一对多导出比较优雅的方案这个issue。

从此issue的回复我们可以发现,项目维护者建议创建自定义合并策略来实现,有位回复的老哥已经给出了实现代码,接下来我们就用这个方案来实现下。

解决思路

为什么自定义单元格合并策略能实现一对多的列表信息的导出呢?首先我们来看下将嵌套数据平铺,不进行合并导出的Excel。

看完之后我们很容易理解解决思路,只要把订单ID相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出了。

实现过程

首先我们得把原来嵌套的订单商品信息给平铺了,创建一个专门的导出对象OrderData,包含订单和商品信息,二级表头可以通过设置@ExcelProperty的value为数组来实现; /**

* 订单导出

* Created by macro on 2021/12/30.

*/

@Data

@EqualsAndHashCode(callSuper = false)

public class OrderData {

@ExcelProperty(value = "订单ID")

@ColumnWidth

@CustomMerge(needMerge = true, isPk = true)

private String id;

@ExcelProperty(value = "订单编码")

@ColumnWidth

@CustomMerge(needMerge = true)

private String orderSn;

@ExcelProperty(value = "创建时间")

@ColumnWidth

@DateTimeFormat("yyyy-MM-dd")

@CustomMerge(needMerge = true)

private Date createTime;

@ExcelProperty(value = "收货地址")

@CustomMerge(needMerge = true)

@ColumnWidth

private String receiverAddress;

@ExcelProperty(value = {"商品信息", "商品编码"})

@ColumnWidth

private String productSn;

@ExcelProperty(value = {"商品信息", "商品名称"})

@ColumnWidth

private String name;

@ExcelProperty(value = {"商品信息", "商品标题"})

@ColumnWidth

private String subTitle;

@ExcelProperty(value = {"商品信息", "品牌名称"})

@ColumnWidth

private String brandName;

@ExcelProperty(value = {"商品信息", "商品价格"})

@ColumnWidth

private BigDecimal price;

@ExcelProperty(value = {"商品信息", "商品数量"})

@ColumnWidth

private Integer count;

}

复制代码 然后将原来嵌套的Order对象列表转换为OrderData对象列表; /**

* EasyExcel导入导出测试Controller

* Created by macro on 2021/10/12.

*/

@Controller

@Api(tags = "EasyExcelController", description = "EasyExcel导入导出测试")

@RequestMapping("/easyExcel")

public class EasyExcelController {

private List convert(List orderList) {

List result = new ArrayList<>();

for (Order order : orderList) {

List productList = order.getProductList();

for (Product product : productList) {

OrderData orderData = new OrderData();

BeanUtil.copyProperties(product,orderData);

BeanUtil.copyProperties(order,orderData);

result.add(orderData);

}

}

return result;

}

}

复制代码 再创建一个自定义注解CustomMerge,用于标记哪些属性需要合并,哪个是主键; /**

* 自定义注解,用于判断是否需要合并以及合并的主键

*/

@Target({ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

@Inherited

public @interface CustomMerge {

/**

* 是否需要合并单元格

*/

boolean needMerge() default false;

/**

* 是否是主键,即该字段相同的行合并

*/

boolean isPk() default false;

}

复制代码 再创建自定义单元格合并策略类CustomMergeStrategy,当Excel中两列主键相同时,合并被标记需要合并的列; /**

* 自定义单元格合并策略

*/

public class CustomMergeStrategy implements RowWriteHandler {

/**

* 主键下标

*/

private Integer pkIndex;

/**

* 需要合并的列的下标集合

*/

private List needMergeColumnIndex = new ArrayList<>();

/**

* DTO数据类型

*/

private Class elementType;

public CustomMergeStrategy(Class elementType) {

this.elementType = elementType;

}

@Override

public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {

// 如果是标题,则直接返回

if (isHead) {

return;

}

// 获取当前sheet

Sheet sheet = writeSheetHolder.getSheet();

// 获取标题行

Row titleRow = sheet.getRow(0);

if (null == pkIndex) {

this.lazyInit(writeSheetHolder);

}

// 判断是否需要和上一行进行合并

// 不能和标题合并,只能数据行之间合并

if (row.getRowNum() <= 1) {

return;

}

// 获取上一行数据

Row lastRow = sheet.getRow(row.getRowNum() - 1);

// 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并

if (lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {

for (Integer needMerIndex : needMergeColumnIndex) {

CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(),

needMerIndex, needMerIndex);

sheet.addMergedRegionUnsafe(cellRangeAddress);

}

}

}

/**

* 初始化主键下标和需要合并字段的下标

*/

private void lazyInit(WriteSheetHolder writeSheetHolder) {

// 获取当前sheet

Sheet sheet = writeSheetHolder.getSheet();

// 获取标题行

Row titleRow = sheet.getRow(0);

// 获取DTO的类型

Class eleType = this.elementType;

// 获取DTO所有的属性

Field[] fields = eleType.getDeclaredFields();

// 遍历所有的字段,因为是基于DTO的字段来构建excel,所以字段数 >= excel的列数

for (Field theField : fields) {

// 获取@ExcelProperty注解,用于获取该字段对应在excel中的列的下标

ExcelProperty easyExcelAnno = theField.getAnnotation(ExcelProperty.class);

// 为空,则表示该字段不需要导入到excel,直接处理下一个字段

if (null == easyExcelAnno) {

continue;

}

// 获取自定义的注解,用于合并单元格

CustomMerge customMerge = theField.getAnnotation(CustomMerge.class);

// 没有@CustomMerge注解的默认不合并

if (null == customMerge) {

continue;

}

for (int index = 0; index < fields.length; index++) {

Cell theCell = titleRow.getCell(index);

// 当配置为不需要导出时,返回的为null,这里作一下判断,防止NPE

if (null == theCell) {

continue;

}

// 将字段和excel的表头匹配上

if (easyExcelAnno.value()[0].equalsIgnoreCase(theCell.getStringCellValue())) {

if (customMerge.isPk()) {

pkIndex = index;

}

if (customMerge.needMerge()) {

needMergeColumnIndex.add(index);

}

}

}

}

// 没有指定主键,则异常

if (null == this.pkIndex) {

throw new IllegalStateException("使用@CustomMerge注解必须指定主键");

}

}

}

复制代码 接下来在Controller中添加导出订单列表的接口,将我们自定义的合并策略CustomMergeStrategy给注册上去; /**

* EasyExcel导入导出测试Controller

* Created by macro on 2021/10/12.

*/

@Controller

@Api(tags = "EasyExcelController", description = "EasyExcel导入导出测试")

@RequestMapping("/easyExcel")

public class EasyExcelController {

@SneakyThrows

@ApiOperation(value = "导出订单列表Excel")

@RequestMapping(value = "/exportOrderList", method = RequestMethod.GET)

public void exportOrderList(HttpServletResponse response) {

List orderList = getOrderList();

List orderDataList = convert(orderList);

setExcelRespProp(response, "订单列表");

EasyExcel.write(response.getOutputStream())

.head(OrderData.class)

.registerWriteHandler(new CustomMergeStrategy(OrderData.class))

.excelType(ExcelTypeEnum.XLSX)

.sheet("订单列表")

.doWrite(orderDataList);

}

}

复制代码 在Swagger中访问接口测试,导出订单列表对应Excel;

下载完成后,查看下文件,由于EasyExcel需要自己来实现,对比之前使用EasyPoi来实现麻烦了不少。

其他使用 由于EasyExcel的官方文档介绍的比较简单,如果你想要更深入地进行使用的话,建议大家看下官方Demo。

总结 体验了一把EasyExcel,使用还是挺方便的,性能也很优秀。但是比较常见的一对多导出实现比较复杂,而且功能也不如EasyPoi 强大。如果你的Excel导出数据量不大的话,可以使用EasyPoi,如果数据量大,比较在意性能的话,还是使用EasyExcel吧。

参考资料 项目地址:github.com/alibaba/eas… 官方文档:www.yuque.com/easyexcel/d… 项目源码地址 github.com/macrozheng/…

原文链接:https://juejin.cn/post/7051751438715715620

相关推荐

天涯明月刀副本攻略,各副本BOSS怎么打
bat365手机版app

天涯明月刀副本攻略,各副本BOSS怎么打

📅 07-06 👀 5902
无线监控摄像头要多少带宽
365app下载365足球网站

无线监控摄像头要多少带宽

📅 09-18 👀 8422
华为电话手表智能手表报价
bat365手机版app

华为电话手表智能手表报价

📅 08-09 👀 9706