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

发布时间:2026/5/20 23:34:48

SpringBoot 实现 Excel 导入导出,性能爆表,用起来够优雅! 操作Excel实现导入导出是个非常常见的需求之前介绍了一款非常好用的工具EasyPoi 。有读者提出在数据量大的情况下EasyPoi占用内存大性能不够好。今天给大家推荐一款性能更好的Excel导入导出工具EasyExcel希望对大家有所帮助EasyExcel简介EasyExcel是一款阿里开源的Excel导入导出工具具有处理快速、占用内存小、使用方便的特点在Github上已有22kStar可见其非常流行。EasyExcel读取75M(46W行25列)的Excel仅需使用64M内存耗时20s极速模式还可以更快集成在SpringBoot中集成EasyExcel非常简单仅需一个依赖即可。!--EasyExcel相关依赖-- dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.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; ExcelIgnore private String password; ExcelProperty(昵称) ColumnWidth(20) private String nickname; ExcelProperty(出生日期) ColumnWidth(20) DateTimeFormat(yyyy-MM-dd) private Date birthday; ExcelProperty(手机号) ColumnWidth(20) private String phone; ExcelIgnore private 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 ConverterInteger { 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(WriteConverterContextInteger 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, 会员列表); ListMember 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) { ListMember 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 ListOrderData convert(ListOrder orderList) { ListOrderData result new ArrayList(); for (Order order : orderList) { ListProduct 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 ListInteger 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) { ListOrder orderList getOrderList(); ListOrderData 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微信8.0将好友放开到了一万小伙伴可以加我大号了先到先得再满就真没了扫描下方二维码即可加我微信啦2022抱团取暖一起牛逼。推荐阅读程序员裸辞全职接单一个月的感触公司技术最好的程序员被优化了...不写代码轻松实现数据可视化这款基于SpringBoot的神器简直绝了更快的 Maven 来了性能提升 300%豆瓣 9.7这本技术书籍直接封神了这些年我用过的API文档工具个个是精品40KStarMall电商实战项目开源回忆录mall-swarm 微服务电商项目发布重大更新打造Spring Cloud最佳实践

相关新闻