【笑小枫的SpringBoot系列】【十二】JAVA使用EasyExcel导入excel

简介: 【笑小枫的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操作,只为演示,特殊的可以根据这个实际情况进行拓展。🐾


d3433edbac28caf21774854af459fd87.png

87320f5d5565f063b531cc9c9eae4ff6.png


点击下载模板(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控制台打印结果如下:


4bdfa354e5b40121f69b5b122e295e5a.png


postman返回的结果数据如下:


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


模拟一下,数据转换错误的场景,故意把时间写错,如下图:


b17096db644d448ea930b31131d076c0.png

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


46b99259c6aee68e6810cd5d257b01d2.png

在controller添加@RequestMapping("/example")可以避免token校验,在拦截器里面已经放开了/example/**的请求。

postman返回的结果数据如下:


6b524c51d17054e9e3ffc3a6777b7683.png


相关属性解读


注解


  • 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 需要读的表格有几行头数据。默认有一行头,也就是认为第二行开始起为数据。
  • head 与clazz二选一。读取文件头对应的列表,会根据列表匹配数据,建议使用class。
  • clazz 与head二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据。
  • autoTrim 字符串、表头等数据自动trim
  • password 读的时候是否需要使用密码

ReadWorkbook(理解成excel对象)参数


  • excelType 当前excel的类型 默认会自动判断
  • inputStream 与file二选一。读取文件的流,如果接收到的是流就只用,不用流建议使用file参数。因为使用了inputStream easyexcel会帮忙创建临时文件,最终还是file
  • file 与inputStream二选一。读取文件的文件。
  • autoCloseStream 自动关闭流。
  • readCache 默认小于5M用 内存,超过5M会使用 EhCache,这里不建议使用这个参数。
  • useDefaultListener @since 2.1.4 默认会加入ModelBuildEventListener 来帮忙转换成传入class的对象,设置成false后将不会协助转换对象,自定义的监听器会接收到Map<Integer,CellData>对象,如果还想继续接听到class对象,请调用readListener方法,加入自定义的beforeListener、 ModelBuildEventListener、 自定义的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


关于笑小枫💕


本章到这里结束了,喜欢的朋友关注一下我呦😘😘,大伙的支持,就是我坚持写下去的动力。

老规矩,懂了就点赞收藏;不懂就问,日常在线,我会就会回复哈~🤪

笑小枫个人博客:https://www.xiaoxiaofeng.com

本文源码:https://github.com/hack-feng/maple-demo

目录
相关文章
|
12天前
|
Java API Apache
Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
【10月更文挑战第29天】Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
60 5
|
22天前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
135 1
|
12天前
|
前端开发 Java easyexcel
SpringBoot操作Excel实现单文件上传、多文件上传、下载、读取内容等功能
SpringBoot操作Excel实现单文件上传、多文件上传、下载、读取内容等功能
50 8
|
12天前
|
Java API Apache
|
1月前
|
JavaScript 前端开发 Java
解决跨域问题大集合:vue-cli项目 和 java/springboot(6种方式) 两端解决(完美解决)
这篇文章详细介绍了如何在前端Vue项目和后端Spring Boot项目中通过多种方式解决跨域问题。
318 1
解决跨域问题大集合:vue-cli项目 和 java/springboot(6种方式) 两端解决(完美解决)
|
14天前
|
监控 前端开发 Java
Java SpringBoot –性能分析与调优
Java SpringBoot –性能分析与调优
|
15天前
|
存储 Java API
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
23 4
|
16天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
21天前
|
JSON Java Maven
实现Java Spring Boot FCM推送教程
本指南介绍了如何在Spring Boot项目中集成Firebase云消息服务(FCM),包括创建项目、添加依赖、配置服务账户密钥、编写推送服务类以及发送消息等步骤,帮助开发者快速实现推送通知功能。
51 2
|
20天前
|
缓存 Java 程序员
Java|SpringBoot 项目开发时,让 FreeMarker 文件编辑后自动更新
在开发过程中,FreeMarker 文件编辑后,每次都需要重启应用才能看到效果,效率非常低下。通过一些配置后,可以让它们免重启自动更新。
23 0