阿里巴巴开源的Excel操作神器!

前提

导出数据到Excel是非常常见的后端需求之一,今天来推荐一款阿里出品的Excel操作神器:EasyExcelEasyExcel从其依赖树来看是对apache-poi的封装,笔者从开始接触Excel处理就选用了EasyExcel,避免了广泛流传的apache-poi导致的内存泄漏问题。

引入EasyExcel依赖

引入EasyExcelMaven如下:

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>${easyexcel.version}</version>
</dependency>

当前(2020-09)的最新版本为2.2.6

API简介

Excel文件主要围绕读和写操作进行处理,EasyExcelAPI也是围绕这两个方面进行设计。先看读操作的相关API

// 新建一个ExcelReaderBuilder实例
ExcelReaderBuilder readerBuilder = EasyExcel.read();
// 读取的文件对象,可以是File、路径(字符串)或者InputStream实例
readerBuilder.file("");
// 文件的密码
readerBuilder.password("");
// 指定sheet,可以是数字序号sheetNo或者字符串sheetName,若不指定则会读取所有的sheet
readerBuilder.sheet("");
// 是否自动关闭输入流
readerBuilder.autoCloseStream(true);
// Excel文件格式,包括ExcelTypeEnum.XLSX和ExcelTypeEnum.XLS
readerBuilder.excelType(ExcelTypeEnum.XLSX);
// 指定文件的标题行,可以是Class对象(结合@ExcelProperty注解使用),或者List<List<String>>实例
readerBuilder.head(Collections.singletonList(Collections.singletonList("head")));
// 注册读取事件的监听器,默认的数据类型为Map<Integer,String>,第一列的元素的下标从0开始
readerBuilder.registerReadListener(new AnalysisEventListener() {@Overridepublic void invokeHeadMap(Map headMap, AnalysisContext context) {// 这里会回调标题行,文件内容的首行会认为是标题行}@Overridepublic void invoke(Object o, AnalysisContext analysisContext) {// 这里会回调每行的数据}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {}
});
// 构建读取器
ExcelReader excelReader = readerBuilder.build();
// 读取数据
excelReader.readAll();
excelReader.finish();

可以看到,读操作主要使用Builder模式和事件监听(或者可以理解为「观察者模式」)的设计。一般情况下,上面的代码可以简化如下:

Map<Integer, String> head = new HashMap<>();
List<Map<Integer, String>> data = new LinkedList<>();
EasyExcel.read("文件的绝对路径").sheet().registerReadListener(new AnalysisEventListener<Map<Integer, String>>() {@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {head.putAll(headMap);}@Overridepublic void invoke(Map<Integer, String> row, AnalysisContext analysisContext) {data.add(row);}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {// 这里可以打印日志告知所有行读取完毕}}).doRead();

如果需要读取数据并且转换为对应的对象列表,则需要指定标题行的Class,结合注解@ExcelProperty使用:

文件内容:|订单编号|手机号|
|ORDER_ID_1|112222|
|ORDER_ID_2|334455|@Data
private static class OrderDTO {@ExcelProperty(value = "订单编号")private String orderId;@ExcelProperty(value = "手机号")private String phone;
}Map<Integer, String> head = new HashMap<>();
List<OrderDTO> data = new LinkedList<>();
EasyExcel.read("文件的绝对路径").head(OrderDTO.class).sheet().registerReadListener(new AnalysisEventListener<OrderDTO>() {@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {head.putAll(headMap);}@Overridepublic void invoke(OrderDTO row, AnalysisContext analysisContext) {data.add(row);}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {// 这里可以打印日志告知所有行读取完毕}}).doRead();

「如果数据量巨大,建议使用Map<Integer, String>类型读取和操作数据对象,否则大量的反射操作会使读取数据的耗时大大增加,极端情况下,例如属性多的时候反射操作的耗时有可能比读取和遍历的时间长」

接着看写操作的API

// 新建一个ExcelWriterBuilder实例
ExcelWriterBuilder writerBuilder = EasyExcel.write();
// 输出的文件对象,可以是File、路径(字符串)或者OutputStream实例
writerBuilder.file("");
// 指定sheet,可以是数字序号sheetNo或者字符串sheetName,可以不设置,由下面提到的WriteSheet覆盖
writerBuilder.sheet("");
// 文件的密码
writerBuilder.password("");
// Excel文件格式,包括ExcelTypeEnum.XLSX和ExcelTypeEnum.XLS
writerBuilder.excelType(ExcelTypeEnum.XLSX);
// 是否自动关闭输出流
writerBuilder.autoCloseStream(true);
// 指定文件的标题行,可以是Class对象(结合@ExcelProperty注解使用),或者List<List<String>>实例
writerBuilder.head(Collections.singletonList(Collections.singletonList("head")));
// 构建ExcelWriter实例
ExcelWriter excelWriter = writerBuilder.build();
List<List<String>> data = new ArrayList<>();
// 构建输出的sheet
WriteSheet writeSheet = new WriteSheet();
writeSheet.setSheetName("target");
excelWriter.write(data, writeSheet);
// 这一步一定要调用,否则输出的文件有可能不完整
excelWriter.finish();

ExcelWriterBuilder中还有很多样式、行处理器、转换器设置等方法,笔者觉得不常用,这里不做举例,内容的样式通常在输出文件之后再次加工会更加容易操作。写操作一般可以简化如下:

List<List<String>> head = new ArrayList<>();
List<List<String>> data = new LinkedList<>();
EasyExcel.write("输出文件绝对路径").head(head).excelType(ExcelTypeEnum.XLSX).sheet("target").doWrite(data);

实用技巧

下面简单介绍一下生产中用到的实用技巧。

多线程读

使用EasyExcel多线程读建议在限定的前提条件下使用:

  • 源文件已经被分割成多个小文件,并且每个小文件的标题行和列数一致。

  • 机器内存要充足,因为并发读取的结果最后需要合并成一个大的结果集,全部数据存放在内存中。

经常遇到外部反馈的多份文件需要紧急进行数据分析或者交叉校对,为了加快文件读取,笔者通常使用这种方式批量读取格式一致的Excel文件

一个简单的例子如下:

@Slf4j
public class EasyExcelConcurrentRead {static final int N_CPU = Runtime.getRuntime().availableProcessors();public static void main(String[] args) throws Exception {// 假设I盘的temp目录下有一堆同格式的Excel文件String dir = "I:\\temp";List<Map<Integer, String>> mergeResult = Lists.newLinkedList();ThreadPoolExecutor executor = new ThreadPoolExecutor(N_CPU, N_CPU * 2, 0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(), new ThreadFactory() {private final AtomicInteger counter = new AtomicInteger();@Overridepublic Thread newThread(@NotNull Runnable r) {Thread thread = new Thread(r);thread.setDaemon(true);thread.setName("ExcelReadWorker-" + counter.getAndIncrement());return thread;}});Path dirPath = Paths.get(dir);if (Files.isDirectory(dirPath)) {List<Future<List<Map<Integer, String>>>> futures = Files.list(dirPath).map(path -> path.toAbsolutePath().toString()).filter(absolutePath -> absolutePath.endsWith(".xls") || absolutePath.endsWith(".xlsx")).map(absolutePath -> executor.submit(new ReadTask(absolutePath))).collect(Collectors.toList());for (Future<List<Map<Integer, String>>> future : futures) {mergeResult.addAll(future.get());}}log.info("读取[{}]目录下的文件成功,一共加载:{}行数据", dir, mergeResult.size());// 其他业务逻辑.....}@RequiredArgsConstructorprivate static class ReadTask implements Callable<List<Map<Integer, String>>> {private final String location;@Overridepublic List<Map<Integer, String>> call() throws Exception {List<Map<Integer, String>> data = Lists.newLinkedList();EasyExcel.read(location).sheet().registerReadListener(new AnalysisEventListener<Map<Integer, String>>() {@Overridepublic void invoke(Map<Integer, String> row, AnalysisContext analysisContext) {data.add(row);}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {log.info("读取路径[{}]文件成功,一共[{}]行", location, data.size());}}).doRead();return data;}}
}

这里采用ThreadPoolExecutor#submit()提交并发读的任务,然后使用Future#get()等待所有任务完成之后再合并最终的读取结果。

注意,一般文件的写操作不能并发执行,否则很大的概率会导致数据错乱

多Sheet写

Sheet写,其实就是使用同一个ExcelWriter实例,写入多个WriteSheet实例中,每个Sheet的标题行可以通过WriteSheet实例中的配置属性进行覆盖,代码如下:

public class EasyExcelMultiSheetWrite {public static void main(String[] args) throws Exception {ExcelWriterBuilder writerBuilder = EasyExcel.write();writerBuilder.excelType(ExcelTypeEnum.XLSX);writerBuilder.autoCloseStream(true);writerBuilder.file("I:\\temp\\temp.xlsx");ExcelWriter excelWriter = writerBuilder.build();WriteSheet firstSheet = new WriteSheet();firstSheet.setSheetName("first");firstSheet.setHead(Collections.singletonList(Collections.singletonList("第一个Sheet的Head")));// 写入第一个命名为first的SheetexcelWriter.write(Collections.singletonList(Collections.singletonList("第一个Sheet的数据")), firstSheet);WriteSheet secondSheet = new WriteSheet();secondSheet.setSheetName("second");secondSheet.setHead(Collections.singletonList(Collections.singletonList("第二个Sheet的Head")));// 写入第二个命名为second的SheetexcelWriter.write(Collections.singletonList(Collections.singletonList("第二个Sheet的数据")), secondSheet);excelWriter.finish();}
}

效果如下:

分页查询和批量写

在一些数据量比较大的场景下,可以考虑分页查询和批量写,其实就是分页查询原始数据 -> 数据聚合或者转换 -> 写目标数据 -> 下一页查询....。其实数据量少的情况下,一次性全量查询和全量写也只是分页查询和批量写的一个特例,因此可以把查询、转换和写操作抽象成一个可复用的模板方法:

int batchSize = 定义每篇查询的条数;
OutputStream outputStream = 定义写到何处;
ExcelWriter writer = new ExcelWriterBuilder().autoCloseStream(true).file(outputStream).excelType(ExcelTypeEnum.XLSX).head(ExcelModel.class);
for (;;){List<OriginModel> list = originModelRepository.分页查询();if (list.isEmpty()){writer.finish();break;}else {list 转换-> List<ExcelModel> excelModelList;writer.write(excelModelList);}
}

参看笔者前面写过的一篇非标题党生产应用文章《百万级别数据Excel导出优化》,适用于大数据量导出的场景,代码如下:

Excel上传与下载

下面的例子适用于Servlet容器,常见的如Tomcat,应用于spring-boot-starter-web

Excel文件上传跟普通文件上传的操作差不多,然后使用EasyExcelExcelReader读取请求对象MultipartHttpServletRequest中文件部分抽象的InputStream实例即可:

@PostMapping(path = "/upload")
public ResponseEntity<?> upload(MultipartHttpServletRequest request) throws Exception {Map<String, MultipartFile> fileMap = request.getFileMap();for (Map.Entry<String, MultipartFile> part : fileMap.entrySet()) {InputStream inputStream = part.getValue().getInputStream();Map<Integer, String> head = new HashMap<>();List<Map<Integer, String>> data = new LinkedList<>();EasyExcel.read(inputStream).sheet().registerReadListener(new AnalysisEventListener<Map<Integer, String>>() {@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {head.putAll(headMap);}@Overridepublic void invoke(Map<Integer, String> row, AnalysisContext analysisContext) {data.add(row);}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {log.info("读取文件[{}]成功,一共:{}行......", part.getKey(), data.size());}}).doRead();// 其他业务逻辑}return ResponseEntity.ok("success");
}

使用Postman请求如下:

使用EasyExcel进行Excel文件导出也比较简单,只需要把响应对象HttpServletResponse中携带的OutputStream对象附着到EasyExcelExcelWriter实例即可:

@GetMapping(path = "/download")
public void download(HttpServletResponse response) throws Exception {// 这里文件名如果涉及中文一定要使用URL编码,否则会乱码String fileName = URLEncoder.encode("文件名.xlsx", StandardCharsets.UTF_8.toString());// 封装标题行List<List<String>> head = new ArrayList<>();// 封装数据List<List<String>> data = new LinkedList<>();response.setContentType("application/force-download");response.setHeader("Content-Disposition", "attachment;filename=" + fileName);EasyExcel.write(response.getOutputStream()).head(head).autoCloseStream(true).excelType(ExcelTypeEnum.XLSX).sheet("Sheet名字").doWrite(data);
}

这里需要注意一下:

  • 文件名如果包含中文,需要进行URL编码,否则一定会乱码。

  • 无论导入或者导出,如果数据量大比较耗时,使用了Nginx的话记得调整Nginx中的连接、读写超时时间的上限配置。

  • 使用SpringBoot需要调整spring.servlet.multipart.max-request-sizespring.servlet.multipart.max-file-size的配置值,避免上传的文件过大出现异常。

小结

EasyExcelAPI设计简单易用,可以使用他快速开发有Excel数据导入或者导出的场景,真是广大 Javaer 人的福音。


往期推荐

Java新特性:数据类型可以扔掉了?


多图带你彻底理解Java中的21种锁!


JDK 竟然是这样实现栈的?


关注下方二维码,收获更多干货!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/545799.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

再谈指针

C语言为什么高效&#xff1f;因为C语言有指针。指针是C语言的精华&#xff0c;同时也是C语言的难点&#xff0c;很多人一学到指针就表示头大&#xff0c;指针的指向往往把人搞得晕头转向&#xff0c;甚至有的人为了避免使用指针居然不惜多写几十行代码&#xff0c;无疑增加了工…

vc给exe更改图标

第一步&#xff1a;将制作好的ioc格式图标&#xff0c;拷贝到自己工程所在的res文件夹中第二步&#xff1a;在vc开发环境中&#xff0c;insert-->resourse--〉单击icon然后选择右边的import找到刚才添加到res中的图标文件第三步&#xff1a;将m_hIcon AfxGetApp()->Load…

人工智能ai知识_人工智能中基于知识的代理层

人工智能ai知识Every agent that has a knowledge base and an inference system is known as a knowledge-based agent. The knowledge base contains all the information the agent has. This information can either be the data that is embedded into the agent in prior…

Word 2003中为什么修改一个段落的文章结果整篇文档的格式都变?

问题比如说&#xff0c;我选定某一段把颜色改成***&#xff0c;结果整篇文档都变成***了&#xff0c;按撤退健&#xff0c;才能达到效果&#xff08;只有这段变成***&#xff0c;其他的不变&#xff09;。答案打开格式菜单中的[样式和格式]&#xff0c;找到样式中的“正文”。 …

链表反转的两种实现方法,后一种击败了100%的用户!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;链表反转是一道很基础但又非常热门的算法面试题&#xff0c;它也在《剑指Offer》的第 24 道题出现过&#xff0c;至于它有多…

C++总结篇(4)内存管理

C语言中用malloc/realloc/calloc/free进行空间的申请与释放&#xff0c;在C中用新的方式进行空间的申请与释放。 申请一个int型的空间并释放 //C语言&#xff1a;int *ptr(int)malloc(sizeof(int));free(ptr); //C: int *ptr new int;delete ptr; C的申请方式更为简洁方便&…

debug和release的区别

1。Debug和Release有什么区别&#xff0c;为什么要使用Release版本&#xff01; 2。怎么把Debug转成ReleaseDebug版本包括调试信息&#xff0c;所以要比Release版本大很多&#xff08;可能大数百K至 数M&#xff09;。至于是否需要DLL支持&#xff0c;主要看你采用的编译选项。…

人工智能ai 学习_人工智能中学习代理的要素

人工智能ai 学习As already discussed, the Learning agents have the capability to improve their knowledge base by Learning from their surroundings by themselves, without any help or input from the user or the client. 如已经讨论的那样&#xff0c; 学习代理可以…

squid代理服务器(捎带的SNAT)

1.传统代理传统代理可以隐藏IP地址 多用于Internet 在Linux中 默认没有安装squid 所以要安装 在red hat中 还要安装perl 语言包的支持 squid代理服务器需要两块网卡 首先保证你的流量是从linux服务器上过的 所以先保证做完SNAT可以互相通信1&#xff09;配置网络参数在试验中一…

MySQL开源工具推荐,有了它我卸了珍藏多年Nactive!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;最近无意间发现了一款开源免费的 MySQL 客户端管理工具&#xff0c;磊哥试用了两天感觉还行&#xff0c;所以今天推荐给各位…

C++总结篇(3)String类

string是表示字符串的字符串类&#xff0c;该类的接口与常规容器的接口基本相同&#xff0c;再添加了一些专门用来操作string的常规操作。string在底层实际是&#xff1a;basic_string模板类的别名&#xff0c;typedef basic_string<char, char_traits, allocator> strin…

memoryTraining记忆训练小游戏

无聊的时候用C写了一个记忆训练的小游戏、、、 灵感源于一个flash的小游戏学到C语言就用C语言实验了一下&#xff0c;做出来。好久以前的东西了&#xff0c;数组用的还不咋样&#xff0c;现在看看把数组下标0漏掉了、、、掉了修补了修补&#xff0c;先扔这儿吧。源码下载

动态调用动态库方法 .so

2019独角兽企业重金招聘Python工程师标准>>> 关于动态调用动态库方法说明 一、 动态库概述 1、 动态库的概念 日常编程中&#xff0c;常有一些函数不需要进行编译或者可以在多个文件中使用&#xff08;如数据库输入/输 出操作或屏幕控制等标准任务函数&#…

C++总结篇(5)vector

vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素 进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是可以动态改变的&#xff0c;而且它的大小会被容器自…

清除缓存 c语言_如何用C语言设置,清除和切换单个位?

清除缓存 c语言Given a number and we have to 1) set a bit, 2) clear a bit and 3) toggle a bit. 给定一个数字&#xff0c;我们必须1)设置一个位&#xff0c;2)清除一个位&#xff0c;3)切换一个位。 1)设置一点 (1) Setting a bit) To set a particular bit of a number,…

算法图解:如何找出栈中的最小值?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;前面我们学习了很多关于栈的知识&#xff0c;比如《动图演示&#xff1a;手撸堆栈的两种实现方法&#xff01;》和《JDK 竟然…

数据库概况

New Words & Expressions:facilitate 使容易&#xff0c;促进retrieval n. 检索field n. 字段record 记录&#xff0c;alphabetically 按字母顺序地chronologically 按年代顺序排break down v. 分解build up 建造&#xff0c;装配&#xff0c;组成encyclopedia n. 百科全书…

30岁之前需要知道的10个人生底线,你知道几个?

http://blog.csdn.net/wojiushiwo987/article/details/8893302 引导语&#xff1a;现在的这些年轻人&#xff0c;你是否考虑过你人生成长发展风向与目标&#xff0c;一旦追求和愿望受阻后&#xff0c;你会如何思考对应&#xff0c;分析其原因的所在&#xff0c;你该如何面对去做…

用C语言设置程序开机自启动

当需要使某一程序在开机时就启动它&#xff0c;需要把它写进注册表的启动项中。 下面就展示一种简单的写法&#xff1a; #include <windows.h> #include <stdlib.h> #include <stdio.h>void ComputerStart(char *pathName) {//找到系统的启动项 char *szSub…

漫画:什么是布隆算法?

两周之前——爬虫的原理就不细说了&#xff0c;无非是通过种子URL来顺藤摸瓜&#xff0c;爬取出网站关联的所有的子网页&#xff0c;存入自己的网页库当中。但是&#xff0c;这其中涉及到一个小小的问题......URL去重方案第一版&#xff1a;HashSet创建一个HashSet集合&#xf…