乐趣区

关于springboot:笑小枫的SpringBoot系列十二JAVA使用EasyExcel导入excel

性能背景

简略的说下这个性能的背景需要吧,有相似需要的能够复用

  • 实现 excel 导入(废话 …)
  • 多个 sheet 页一起导入
  • 第一个 sheet 页数据表头信息有两行,但只需依据第二行导入
  • 如果报错,依据不同的 sheet 页返回多个 List 记录报错起因
  • 数据量略微有些大(多个 sheet 页总量 50w 左右)

我的项目引入依赖

gradle:

compile "com.alibaba:easyexcel:3.1.0"

maven:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.0</version>
</dependency>

留神:3+ 版本的的 easyexcel,应用 poi 5+ 版本时,须要手动排除:poi-ooxml-schemas,例如:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.0</version>
    <exclusions>
        <exclusion>
            <artifactId>poi-ooxml-schemas</artifactId>
            <groupId>org.apache.poi</groupId>
        </exclusion>
    </exclusions>
</dependency>

Excel 模板

这里演示一下两个 sheet 页,第一个 sheet 页取第二行题目,第二个 sheet 页取第一行题目的 excel 操作,只为演示,非凡的能够依据这个理论状况进行拓展。🐾

点击下载模板(http://file.xiaoxiaofeng.site/blog/image/ 笑小枫测试导入.xls)

我的项目编码

在 config.bean 包下新建 excel 包,用于寄存 excel 解决相干的代码

  • 在 excel 包下定义通用的 CommonExcel.java 对象,只有用于记录行号
package com.maple.demo.config.bean.excel;

import com.alibaba.excel.annotation.ExcelIgnore;
import lombok.Data;

/**
 * @author 笑小枫
 * @date 2022/7/22
 */
@Data
public class CommonExcel {
    
    /**
     * 行号
     */
    @ExcelIgnore
    private Integer rowIndex;

}
  • 在 excel 包下定义经销商信息对象ImportCompany.java,代码如下:

@ExcelProperty 对用的是 excel 的题目名称,如果不加 @ExcelProperty,默认对应列号

package com.maple.demo.config.bean.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;

/**
 * @author 笑小枫
 * @date 2022/7/22
 */
@Data
public class ImportCompany {

    // -------------------- 根本信息 start -------------

    @ExcelProperty("公司名称")
    private String companyName;

    @ExcelProperty("省份")
    private String province;

    @ExcelProperty("成立工夫")
    private Date startDate;

    @ExcelProperty("企业状态")
    private String entStatus;

    @ExcelProperty("注册地址")
    private String registerAddress;

    // ---------------- 根本信息 end ---------------------

    // ---------------- 经营信息 start ---------------------

    @ExcelProperty("员工数")
    private String employeeMaxCount;

    @ExcelProperty("经营规模")
    private String newManageScaleName;

    @ExcelProperty("所属区域省")
    private String businessProvinceName;

    @ExcelProperty("所属区域市")
    private String businessCityName;

    @ExcelProperty("所属区域区县")
    private String businessAreaName;

    // ---------------- 经营信息 end ---------------------
}
  • 在 excel 包下定义联系人信息对象ImportContact.java,代码如下:
package com.maple.demo.config.bean.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * @author 笑小枫
 * @date 2022/7/22
 */
@Data
public class ImportContact {@ExcelProperty("公司名称")
    private String companyName;

    @ExcelProperty("姓名")
    private String name;

    @ExcelProperty("身份证号码")
    private String idCard;

    @ExcelProperty("电话号码")
    private String mobile;

    @ExcelProperty("职位")
    private String contactPostName;
}
  • 在 listener 包下定义 excel 解决的监听器ImportExcelListener.java,代码如下:
package com.maple.demo.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.read.metadata.holder.ReadRowHolder;
import com.alibaba.excel.util.ListUtils;
import com.maple.demo.config.bean.excel.CommonExcel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;

import java.util.List;
import java.util.function.Consumer;

/**
 * @author 笑小枫
 * @date 2022/7/22
 */
@Slf4j
public class ImportExcelListener<T> implements ReadListener<T> {

    /**
     * 默认一次读取 1000 条,可依据理论业务和服务器调整
     */
    private static final int BATCH_COUNT = 1000;
    /**
     * Temporary storage of data
     */
    private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

    private final List<String> errorMsgList;

    /**
     * consumer
     */
    private final Consumer<List<T>> consumer;

    public ImportExcelListener(Consumer<List<T>> consumer, List<String> errorMsgList) {
        this.consumer = consumer;
        this.errorMsgList = errorMsgList;
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        // 记录行号
        if (data instanceof CommonExcel) {ReadRowHolder readRowHolder = context.readRowHolder();
            ((CommonExcel) data).setRowIndex(readRowHolder.getRowIndex() + 1);
        }
        cachedDataList.add(data);
        if (cachedDataList.size() >= BATCH_COUNT) {consumer.accept(cachedDataList);
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {if (CollectionUtils.isNotEmpty(cachedDataList)) {consumer.accept(cachedDataList);
        }
    }

    /**
     * 在转换异样 获取其余异样下会调用本接口。抛出异样则进行读取。如果这里不抛出异样则 持续读取下一行。*/
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        // 如果是某一个单元格的转换异样 能获取到具体行号
        String errorMsg = String.format("%s,第 %d 行解析异样", context.readSheetHolder().getReadSheet().getSheetName(),
                context.readRowHolder().getRowIndex() + 1);
        if (exception instanceof ExcelDataConvertException) {ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
            errorMsg = String.format("第 %d 行,第 %d 列数据解析异样",
                    excelDataConvertException.getRowIndex() + 1,
                    excelDataConvertException.getColumnIndex() + 1);
            log.error("{},第 {} 行,第 {} 列解析异样,数据为:{}",
                    context.readSheetHolder().getReadSheet().getSheetName(),
                    excelDataConvertException.getRowIndex() + 1,
                    excelDataConvertException.getColumnIndex() + 1,
                    excelDataConvertException.getCause().getMessage());
        } else {log.error(errorMsg + exception.getMessage());
        }
        errorMsgList.add(errorMsg);
    }
}
  • 编写 controller 进行测试,代码如下:
.readSheet(0)  读取哪个 sheet 页,默认从 0 开始

.head(ExcelCompany.class) 对应定义的 sheet 页对象,不同的 sheet 页应用对应的对象

.registerReadListener 应用的监听器,这里定义的时通用的,依据不同的业务逻辑,能够定义不同的监听器解决,如需非凡的返回解决,能够定义多个参数的结构器,在监听器外面解决返回

.headRowNumber(2) 题目行在第几行
package com.maple.demo.controller;

import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.fastjson.JSON;
import com.maple.demo.config.bean.excel.ImportCompany;
import com.maple.demo.config.bean.excel.ImportContact;
import com.maple.demo.listener.ImportExcelListener;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 笑小枫
 * @date 2022/7/22
 */
@Slf4j
@RestController
@RequestMapping("/example")
@Api(tags = "实例演示 - 导入 Excel")
public class TestImportExcelController {@PostMapping("/importExcel")
    public Map<String, List<String>> importExcel(@RequestParam(value = "file") MultipartFile file) {List<String> companyErrorList = new ArrayList<>();
        List<String> contactErrorList = new ArrayList<>();
        try (ExcelReader excelReader = EasyExcelFactory.read(file.getInputStream()).build()) {
            // 公司信息结构器
            ReadSheet dealerSheet = EasyExcelFactory
                    .readSheet(0)
                    .head(ImportCompany.class)
                    .registerReadListener(new ImportExcelListener<ImportCompany>(data -> {
                        // 解决你的业务逻辑,最好抽出一个办法独自解决逻辑
                        log.info("公司信息数据 ----------------------------------------------");
                        log.info("公司信息数据:" + JSON.toJSONString(data));
                        log.info("公司信息数据 ----------------------------------------------");
                    }, companyErrorList))
                    .headRowNumber(2)
                    .build();

            // 联系人信息结构器
            ReadSheet contactSheet = EasyExcelFactory
                    .readSheet(1)
                    .head(ImportContact.class)
                    .registerReadListener(new ImportExcelListener<ImportContact>(data -> {
                        // 解决你的业务逻辑,最好抽出一个办法独自解决逻辑
                        log.info("联系人信息数据 ------------------------------------------");
                        log.info("联系人信息数据:" + JSON.toJSONString(data));
                        log.info("联系人信息数据 ------------------------------------------");
                    }, contactErrorList))
                    .build();

            // 这里留神 肯定要把 sheet1 sheet2 一起传进去,不然有个问题就是 03 版的 excel 会读取屡次,节约性能
            excelReader.read(dealerSheet, contactSheet);
        } catch (IOException e) {log.error("解决 excel 失败," + e.getMessage());
        }
        Map<String, List<String>> result = new HashMap<>(16);
        result.put("company", companyErrorList);
        result.put("contact", contactErrorList);
        log.info("导入 excel 实现,返回后果如下:" + JSON.toJSONString(result));
        return result;
    }
}

测试后果

因为须要上传 excel 文件,这里通过 postman 进行调用,idea 控制台打印后果如下:

postman 返回的后果数据如下:

{"msg": "","obj": {"contact": [],"company": []},
    "result": "0000",
    "serverTime": 1654569757952
}

模仿一下,数据转换谬误的场景,成心把工夫写错,如下图:

通过 postman 进行调用,idea 控制台打印后果如下:

在 controller 增加 @RequestMapping("/example") 能够防止 token 校验,在拦截器外面曾经放开了 /example/** 的申请。

postman 返回的后果数据如下:

相干属性解读

注解

  • ExcelProperty 指定以后字段对应 excel 中的那一列。能够依据名字或者 Index 去匹配。当然也能够不写,默认第一个字段就是 index=0,以此类推。千万留神,要么全副不写,要么全副用 index,要么全副用名字去匹配。千万别三个混着用,除非你十分理解源代码中三个混着用怎么去排序的。
  • ExcelIgnore 默认所有字段都会和 excel 去匹配,加了这个注解会疏忽该字段
  • DateTimeFormat 日期转换,用 String 去接管 excel 日期格局的数据会调用这个注解。外面的 value 参照java.text.SimpleDateFormat
  • NumberFormat 数字转换,用 String 去接管 excel 数字格局的数据会调用这个注解。外面的 value 参照java.text.DecimalFormat
  • ExcelIgnoreUnannotated 默认不加ExcelProperty 的注解的都会参加读写,加了不会参加

参数

通用参数

ReadWorkbook,ReadSheet 都会有的参数,如果为空,默认应用下级。

  • converter 转换器,默认加载了很多转换器。也能够自定义。
  • readListener 监听器,在读取数据的过程中会一直的调用监听器。
  • headRowNumber 须要读的表格有几行头数据。默认有一行头,也就是认为第二行开始起为数据。
  • headclazz 二选一。读取文件头对应的列表,会依据列表匹配数据,倡议应用 class。
  • clazzhead 二选一。读取文件的头对应的 class,也能够应用注解。如果两个都不指定,则会读取全副数据。
  • autoTrim 字符串、表头等数据主动 trim
  • password 读的时候是否须要应用明码

ReadWorkbook(了解成 excel 对象)参数

  • excelType 以后 excel 的类型 默认会主动判断
  • inputStreamfile 二选一。读取文件的流,如果接管到的是流就只用,不必流倡议应用 file 参数。因为应用了inputStream easyexcel 会帮忙创立临时文件,最终还是file
  • fileinputStream 二选一。读取文件的文件。
  • autoCloseStream 主动敞开流。
  • readCache 默认小于 5M 用 内存,超过 5M 会应用 EhCache, 这里不倡议应用这个参数。
  • useDefaultListener @since 2.1.4 默认会退出 ModelBuildEventListener 来帮忙转换成传入class 的对象,设置成 false 后将不会帮助转换对象,自定义的监听器会接管到 Map<Integer,CellData> 对象,如果还想持续接听到 class 对象,请调用 readListener 办法,退出自定义的 beforeListenerModelBuildEventListener、自定义的afterListener 即可。

ReadSheet(就是 excel 的一个 Sheet)参数

  • sheetNo 须要读取 Sheet 的编码,倡议应用这个来指定读取哪个 Sheet
  • sheetName 依据名字去匹配 Sheet,excel 2003 不反对依据名字去匹配

写在最初

本文只是用到局部性能,简略的做了一下总结,更多的性能,能够去官网查阅。

官网文档:https://www.yuque.com/easyexcel/doc/read

应用 EasyExcel 导出 excel:https://www.xiaoxiaofeng.com/archives/springboot13

对于笑小枫💕

本章到这里完结了,喜爱的敌人关注一下我呦😘😘,大伙的反对,就是我保持写下去的能源。
老规矩,懂了就点赞珍藏;不懂就问,日常在线,我会就会回复哈~🤪
后续文章会陆续更新,文档会同步在微信公众号、集体博客、CSDN 和 GitHub 放弃同步更新。😬
微信公众号:笑小枫
笑小枫集体博客:https://www.xiaoxiaofeng.com
CSDN:https://zhangfz.blog.csdn.net
本文源码:https://github.com/hack-feng/maple-demo

退出移动版