操作Excel,除了使用POI你还会其他的?


   温馨提示: 本文总共6334字,阅读完大概需要6-8分钟,希望您能耐心看完,倘若你对该知识点已经比较熟悉,你可以直接通过目录跳转到你感兴趣的地方,希望阅读本文能够对您有所帮助,如果阅读过程中有什么好的建议、看法,欢迎在文章下方留言或者私信我,您的意见对我非常宝贵,再次感谢你阅读本文。

一: 使用场景
一: 使用场景

在日常的系统开发中,系统支持批量数据的操作是一个很常见的功能,其中,最常用的方式是使用excel表格对数据进行批量添加、删除,如:批量新建订单、批量添加商品等。

二: 技术选型
二: 技术选型

现在市面上有很多技术实现来支持excel数据解析如:POI、JXL等,但是,这些技术或多或少都存在着一些问题,下面进行具体分析:

(一): POI

    POI是目前使用最多的用来做excel解析的框架,但这个框架还存在在这个许多问题。现在使用POI技术来解析excel文件的,大多数都是使用到它的userMode模式,好处是上手比较简单,而且网上比较多封装好的代码,虽然复制一下就可以运行,这个对于数据量不大的文件的时候是可以使用,但是当数据量大的时候会存在巨大隐患。
        1、userMode模式存在着一个巨大的问题就是内存消耗很大,一个几兆的文件解析需要上百兆的内存,当并发量大的时候就会容易出现OOM(内存溢出)或者频繁进行full GC回收),导致程序执行缓慢甚至崩溃。
        2、如果有深入了解过POI的会发现,其他它针对这个情况提供了一种叫SAX的模式,但是,这种模式相对复杂,且对excel 03版本和07版本不兼容,两个版本的数据存储方式不一样,所以解析也不一样,这样需要同个功能需要进行两套代码开发,时间周期长,且不易于维护。
        3、在大并发情况下,POI还存在着一些未知的错误,如果需要POI团队修复,周期不确定。

(二)JXL

    它是纯javaAPI,在跨平台上表现的非常完美,代码可以再windows或者Linux上运行而无需重新编写,但是它也存在着许多缺点。
        1、效率低,格式支持比POI还少。
        2、支持Excel 95-2000的所有版本,但是excel2007以后的版本暂时不支持。
.
(三)EasyExcel(推荐使用)

    阿里巴巴出的产品,相信看到这里很多人应该更有信心(毕竟阿里出的东西很是很有质量保障滴)。它是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel,选择使用它有以下原因:

        1、开源,代码放在github上,有问题随时issue
        2、解决了POI解析excel非常耗费内存的问题,它是通过磁盘存储,一行一返回,最大程度解决了内存占用大的问题。
        3、社区活跃度量大,网上的相关文档也比较多。

(四)POI解析模式和EasyExcel解析模型图
解析方式对比
一行一返回方式

三: 常用API介绍
三: 常用API介绍

(一) 监视器(不能被Spring容器管理,每次读取Excel都需要新new一个,如果需要使用Spring容器对象,则通过构造函数传入):

  由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器(这个是必须实现的,所以我们要兼容所有的对象,监听器的泛型使用Object类型)
在这里插入图片描述
(二) 读Excel:

  1、EasyExcel.read(…) —》它有三个重载的方法
读ExcelApi
  2、sheet() --》指定读取的sheet,doRead --》执行读取数据操作
在这里插入图片描述
  3、ExcelReader.readAll() --》 执行读取Excel文件中的所有sheet
在这里插入图片描述
  4、ExcelReader实例.finish() –》完成读取操作,并关闭流(一定要注意关闭流,因为easyExcel是使用磁盘的方式进行数据解析,所以解析过程中会创建临时文件,如果不关闭,最后可能会导致磁盘崩溃)

(三) 写Excel:

  1、EasyExcel.write(…) —》它有六个重载的方法写Excel操作
  2、writeSheet() —》向excel文件中的sheet写入数据
在这里插入图片描述
  3、ExcelWriter.write(…) —》插入sheet到excel文件中,这样就完成了数据写入,实际上就是嵌套一样,现将数据写入到sheet,再将 sheet插入到excel中
  4、ExcelWriter实例.finish() --》完成写入操作,并关闭流(一定要注意关闭流,因为easyExcel是使用磁盘的方式进行数据解析,所以解析过程中会创建临时文件,如果不关闭,最后可能会导致磁盘崩溃)

(四) 常用注解

  1、@ExcelProperty: 作用在excel表数据对应的JAVA实体上,有以下属性:

      (1) : value – 指定导出时该字段对应的标题名称,或者是读取时匹配excel表格中表头的名称,符合则将表头中对应的数据填充到此处,如果这个名称存在多个,只能读取到一个。
      (2) : index – 指定该字段和excel文件的哪一列对应,默认是0,不推荐和value属性同时指定,如果需要制定,那么value的值最好指定为导出数据对应表头的标题名,index的值则指定为读取excel文件时该字段属性对应的列的位置。
      (3) : converter属性则是指定对应的转换器,可以自己书写一个转换器,在读取数据的时候进行对数据的格式化,如:给每一列数据都加上自己自定义的东西

  2、@ExcelIgnoreUnannotated:默认情况下Java类中的所有属性都添加读写,在类上面加入@ExcelIgnoreUnannotated注解,加入这个注解后只有加了@ExcelProperty才会参与读写。

  3、@ExcelIgnore: 被标注的属性不参加Excel的读写,相当于直接省略。

四: 测试
四: 测试

(一):添加依赖

		// easyExcel坐标
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.1.7</version>
        </dependency>

(二): JAVA映射实体

package com.elvis.easyexcel.model;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Demo implements Serializable {
    private static final long serialVersionUID = -920481620956257604L;
    @ExcelIgnore
    @ExcelProperty(value = "姓名", index = 0)
    private String stringType;
    @ExcelProperty(value = "姓名2", index = 1)
    private Integer integerType;
    // 这里使用String类型接收才能格式化,如果使用Date类型则无法格式化
    @ExcelProperty(value = "姓名3", index = 2)
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    private String dateType;
    @ExcelProperty(value = "姓名4", index = 3)
    private Double doubleType;
    @ExcelProperty(value = "姓名5", index = 4)
    private Long longType;
    @ExcelProperty(value = "姓名6", index = 5)
    private Float floatType;
    @ExcelProperty(value = "姓名7", index = 6)
    private Boolean booleanType;
    @ExcelProperty(value = "姓名8", index = 7)
    private Short shortType;
}

(三): 添加监听器

package com.elvis.easyexcel.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Slf4j
public class ObjectListener extends AnalysisEventListener<Object> {
    // 读取到的数据
    private List<Object> readData = new ArrayList<>();

    /**
     *  解析数据进入的方法
     * @param o 本次读到的数据
     * @param analysisContext
     */
    @Override
    public void invoke(Object o, AnalysisContext analysisContext) {
        JSONObject jsonObject = new JSONObject(o);
        log.info("读取到的数据:{}", jsonObject.toString());
        if(Objects.nonNull(o)){
            readData.add(o);
        }
    }
    /**
     * 所有数据解析完成了 都会来调用
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("所有数据解析完成了 都会来调用");
    }

    /**
     * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
     * 如果你的程序在读取解析时即使有异常也不想后面的解析失败的,在此处打出解析错误日志即可
     * 如果你的程序只有解析过程出错就解析解析的话,这在此处手动抛出异常即可
     * @param exception
     * @param context
     * @throws Exception
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
        // 如果是某一个单元格的转换异常 能获取到具体行号
        // 如果要获取头的信息 配合invokeHeadMap使用
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
            log.error("第{}行,第{}列解析异常", excelDataConvertException.getRowIndex(),
                    excelDataConvertException.getColumnIndex());
        }
    }
    // 反馈解析完成的数据
    public List<Object> getReadData(){
        return readData;
    }
}

(四): 书写工具类(这个工具类可以直接使用,如果有需要的,直接复制就可以)

package com.elvis.easyexcel.utils;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.elvis.easyexcel.listener.ObjectListener;
import com.elvis.easyexcel.model.Demo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;

@Slf4j
public class EasyExcelUtils {
    /**
     * @param in    读取的文件流
     * @param model 与excel文件数据对应的实体
     * @param type  通用的数据读取解析监听器
     * @return
     */
    public static List<Object> readExcelFile(InputStream in, Object model, ObjectListener type) {
        ExcelReader reader = null;
        try {
            reader = EasyExcel.read(in, model.getClass(), type).build();
            reader.readAll();
        } catch (Exception e) {
            log.error("读取excel文件错误:" + e.getMessage());
            return null;
        } finally {
            // 关闭流,读的时候会创建临时文件,不关闭到时磁盘会崩的
            if (reader != null) {
                reader.finish();
            }
        }
        return type.getReadData();
    }


    /**
     * 保存数据到excel文件
     *
     * @param data     数据(支持多个sheet写入,根据数据的个数写入对应个sheet,默认多个sheet写入的数据是同一个实体的)
     * @param savePath 保存的路径
     * @return 是否保存成功
     */
    public static Boolean writeExcelFileWithCommonEntity(List<List<Object>> data, String savePath) {
        if (CollectionUtils.isNotEmpty(data)) {
            ExcelWriter excelWriter = null;
            // 输出流放到try的小括号中,方法结束时会自动关闭流,这个是jdk1.8的新特性,对于经常忘记关流的小伙伴很友好哦
            try {
                // 获取到操作写入excel的操作对象,第二个参数是导出的excel文件的标题名对应的实体
                // 获取写入数据中的第一个元素的类类型
                excelWriter = EasyExcel.write(savePath).build();
                // 设置每个sheet的名称
                for (List<Object> objectList : data) {
                    Object item = objectList.get(0);
                    WriteSheet writeSheet = EasyExcel.writerSheet(1, "模板").head(item.getClass()).build();
                    excelWriter.write(objectList, writeSheet);
                }
            } catch (Exception e) {
                log.error("保存数据到excel错误:{}", e.getMessage());
                return false;
            } finally {
                if (excelWriter != null) {
                    excelWriter.finish();
                }
            }
        } else {
            return false;
        }
        return true;
    }


}

(五): 测试demo

package com.elvis;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.elvis.easyexcel.listener.ObjectListener;
import com.elvis.easyexcel.model.Demo;
import com.elvis.easyexcel.utils.EasyExcelUtils;
import org.json.JSONArray;
import org.junit.Test;

import javax.jws.Oneway;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

public class demo {
    @Test
    public void demo1() throws Exception{
        // 读取文件
        System.out.println("开始读取文件------------------------------------");
        String fileName = "E:\\newpath\\excelutils\\build\\classes\\小明.xlsx";
        InputStream in = new FileInputStream(new File(fileName));
        List<Object> objects = EasyExcelUtils.readExcelFile(in, new Demo(), new ObjectListener());
        JSONArray array = new JSONArray(objects);
        System.out.println(array);
        System.out.println("--------------------------------------------------------------");
        System.out.println("开始保存文件------------------------------------");
        String savePath = "E:\\newpath\\excelutils\\build\\classes\\保存文件测试.xlsx";
        List<List<Object>> data = new ArrayList<>();
        List<Object> item = new ArrayList<>();
        Demo abc = new Demo("abc",12,"2020-12-12 19:10:10",12.2,12l,12f,false,Short.parseShort("12"));
        item.add(abc);
        data.add(item);     EasyExcelUtils.writeExcelFileWithCommonEntity(data,savePath);
    }

}

(六): 效果展示
控制台输出
导出文件

五: 总结
五: 总结

     通过亲自测试发现EasyExcel的API很简洁,使用也很容易上手,而且速度也很快(在公司里使用了20多万数据测试,如果使用多线程的话,几秒就可以实现导出,非常的快,如果实现单线程的话,也只需要十秒左右),所以,快动手学起来吧,这个框架现在越来越流行了,面试只要涉及到导出的,基本不是POI的就是EasyExcel。

    由于篇幅限制,所以本文讲解的也只是EasyExcel常使用到的知识,其实它还有许多东西值的我们去研究,更加详细的请到EasyExcel官方手册进行查看,非常感谢你阅读本文,如果有什么疑问或者建议,欢迎在文章下方留言或者私信我,如果你觉的文字对你有帮助,请给我点赞和关注,后面还会书写更多的文章跟大家分享其他的知识,我已经将代码上传到github上,如果你觉的需要看更详细的代码,请点击:跳转(它的代码在spring_parent项目的spring-easyexcel-demo服务中)

  • 8
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT学习日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值