操作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;    @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 Converter<Integer> {    @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<Integer> 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<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...

  • 下载实现后,查看下文件,一个规范的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<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)@Inheritedpublic @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;    }    @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<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/ea...
  • 官网文档:https://www.yuque.com/easyexc...

我的项目源码地址

https://github.com/macrozheng...

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