SpringBoot实现Excel导入导出,性能爆表,用起来够优雅~

本文主要是介绍SpringBoot实现Excel导入导出,性能爆表,用起来够优雅~,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

EasyExcel简介

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

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

集成

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

<!--EasyExcel相关依赖-->
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.0.5</version>
</dependency>

使用

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

简单导出

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

  • 首先创建一个会员对象Member,封装会员信息,这里使用了EasyExcel的注解;
/*** 购物会员* Created by macro on 2021/10/12.*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {@ExcelProperty("ID")@ColumnWidth(10)private Long id;@ExcelProperty("用户名")@ColumnWidth(20)private String username;@ExcelIgnoreprivate String password;@ExcelProperty("昵称")@ColumnWidth(20)private String nickname;@ExcelProperty("出生日期")@ColumnWidth(20)@DateTimeFormat("yyyy-MM-dd")private Date birthday;@ExcelProperty("手机号")@ColumnWidth(20)private String phone;@ExcelIgnoreprivate String icon;@ExcelProperty(value = "性别", converter = GenderConverter.class)@ColumnWidth(10)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<Integer> {@Overridepublic Class<?> supportJavaTypeKey() {//对象属性类型return Integer.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {//CellData属性类型return CellDataTypeEnum.STRING;}@Overridepublic 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;}}@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception {//对象属性转CellDataInteger 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<Member> 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)@ResponseBodypublic CommonResult importMemberList(@RequestPart("file") MultipartFile file) {List<Member> 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(10)@CustomMerge(needMerge = true, isPk = true)private String id;@ExcelProperty(value = "订单编码")@ColumnWidth(20)@CustomMerge(needMerge = true)private String orderSn;@ExcelProperty(value = "创建时间")@ColumnWidth(20)@DateTimeFormat("yyyy-MM-dd")@CustomMerge(needMerge = true)private Date createTime;@ExcelProperty(value = "收货地址")@CustomMerge(needMerge = true)@ColumnWidth(20)private String receiverAddress;@ExcelProperty(value = {"商品信息", "商品编码"})@ColumnWidth(20)private String productSn;@ExcelProperty(value = {"商品信息", "商品名称"})@ColumnWidth(20)private String name;@ExcelProperty(value = {"商品信息", "商品标题"})@ColumnWidth(30)private String subTitle;@ExcelProperty(value = {"商品信息", "品牌名称"})@ColumnWidth(20)private String brandName;@ExcelProperty(value = {"商品信息", "商品价格"})@ColumnWidth(20)private BigDecimal price;@ExcelProperty(value = {"商品信息", "商品数量"})@ColumnWidth(20)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<OrderData> convert(List<Order> orderList) {List<OrderData> result = new ArrayList<>();for (Order order : orderList) {List<Product> 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<Integer> needMergeColumnIndex = new ArrayList<>();/*** DTO数据类型*/private Class<?> elementType;public CustomMergeStrategy(Class<?> elementType) {this.elementType = elementType;}@Overridepublic void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {// 如果是标题,则直接返回if (isHead) {return;}// 获取当前sheetSheet 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) {// 获取当前sheetSheet 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,这里作一下判断,防止NPEif (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<Order> orderList = getOrderList();List<OrderData> 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吧。

参考资料

  • 项目地址:https://github.com/alibaba/easyexcel
  • 官方文档:https://www.yuque.com/easyexcel/doc/easyexcel

项目源码地址

https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-easyexcel

本文 GitHub https://github.com/macrozheng/mall-learning 已经收录,欢迎大家Star!

这篇关于SpringBoot实现Excel导入导出,性能爆表,用起来够优雅~的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/767551

相关文章

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

利用Python打造一个Excel记账模板

《利用Python打造一个Excel记账模板》这篇文章主要为大家详细介绍了如何使用Python打造一个超实用的Excel记账模板,可以帮助大家高效管理财务,迈向财富自由之路,感兴趣的小伙伴快跟随小编一... 目录设置预算百分比超支标红预警记账模板功能介绍基础记账预算管理可视化分析摸鱼时间理财法碎片时间利用财

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推