阿里巴巴开源的Excel操作神器!
前提
導(dǎo)出數(shù)據(jù)到Excel是非常常見的后端需求之一,今天來推薦一款阿里出品的Excel操作神器:EasyExcel。EasyExcel從其依賴樹來看是對apache-poi的封裝,筆者從開始接觸Excel處理就選用了EasyExcel,避免了廣泛流傳的apache-poi導(dǎo)致的內(nèi)存泄漏問題。
引入EasyExcel依賴
引入EasyExcel的Maven如下:
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>${easyexcel.version}</version> </dependency>當(dāng)前(2020-09)的最新版本為2.2.6。
API簡介
Excel文件主要圍繞讀和寫操作進行處理,EasyExcel的API也是圍繞這兩個方面進行設(shè)計。先看讀操作的相關(guān)API:
//?新建一個ExcelReaderBuilder實例 ExcelReaderBuilder?readerBuilder?=?EasyExcel.read(); //?讀取的文件對象,可以是File、路徑(字符串)或者InputStream實例 readerBuilder.file(""); //?文件的密碼 readerBuilder.password(""); //?指定sheet,可以是數(shù)字序號sheetNo或者字符串sheetName,若不指定則會讀取所有的sheet readerBuilder.sheet(""); //?是否自動關(guān)閉輸入流 readerBuilder.autoCloseStream(true); //?Excel文件格式,包括ExcelTypeEnum.XLSX和ExcelTypeEnum.XLS readerBuilder.excelType(ExcelTypeEnum.XLSX); //?指定文件的標(biāo)題行,可以是Class對象(結(jié)合@ExcelProperty注解使用),或者List<List<String>>實例 readerBuilder.head(Collections.singletonList(Collections.singletonList("head"))); //?注冊讀取事件的監(jiān)聽器,默認(rèn)的數(shù)據(jù)類型為Map<Integer,String>,第一列的元素的下標(biāo)從0開始 readerBuilder.registerReadListener(new?AnalysisEventListener()?{@Overridepublic?void?invokeHeadMap(Map?headMap,?AnalysisContext?context)?{//?這里會回調(diào)標(biāo)題行,文件內(nèi)容的首行會認(rèn)為是標(biāo)題行}@Overridepublic?void?invoke(Object?o,?AnalysisContext?analysisContext)?{//?這里會回調(diào)每行的數(shù)據(jù)}@Overridepublic?void?doAfterAllAnalysed(AnalysisContext?analysisContext)?{} }); //?構(gòu)建讀取器 ExcelReader?excelReader?=?readerBuilder.build(); //?讀取數(shù)據(jù) excelReader.readAll(); excelReader.finish();可以看到,讀操作主要使用Builder模式和事件監(jiān)聽(或者可以理解為「觀察者模式」)的設(shè)計。一般情況下,上面的代碼可以簡化如下:
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();如果需要讀取數(shù)據(jù)并且轉(zhuǎn)換為對應(yīng)的對象列表,則需要指定標(biāo)題行的Class,結(jié)合注解@ExcelProperty使用:
文件內(nèi)容:|訂單編號|手機號| |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();「如果數(shù)據(jù)量巨大,建議使用Map<Integer, String>類型讀取和操作數(shù)據(jù)對象,否則大量的反射操作會使讀取數(shù)據(jù)的耗時大大增加,極端情況下,例如屬性多的時候反射操作的耗時有可能比讀取和遍歷的時間長」。
接著看寫操作的API:
//?新建一個ExcelWriterBuilder實例 ExcelWriterBuilder?writerBuilder?=?EasyExcel.write(); //?輸出的文件對象,可以是File、路徑(字符串)或者OutputStream實例 writerBuilder.file(""); //?指定sheet,可以是數(shù)字序號sheetNo或者字符串sheetName,可以不設(shè)置,由下面提到的WriteSheet覆蓋 writerBuilder.sheet(""); //?文件的密碼 writerBuilder.password(""); //?Excel文件格式,包括ExcelTypeEnum.XLSX和ExcelTypeEnum.XLS writerBuilder.excelType(ExcelTypeEnum.XLSX); //?是否自動關(guān)閉輸出流 writerBuilder.autoCloseStream(true); //?指定文件的標(biāo)題行,可以是Class對象(結(jié)合@ExcelProperty注解使用),或者List<List<String>>實例 writerBuilder.head(Collections.singletonList(Collections.singletonList("head"))); //?構(gòu)建ExcelWriter實例 ExcelWriter?excelWriter?=?writerBuilder.build(); List<List<String>>?data?=?new?ArrayList<>(); //?構(gòu)建輸出的sheet WriteSheet?writeSheet?=?new?WriteSheet(); writeSheet.setSheetName("target"); excelWriter.write(data,?writeSheet); //?這一步一定要調(diào)用,否則輸出的文件有可能不完整 excelWriter.finish();ExcelWriterBuilder中還有很多樣式、行處理器、轉(zhuǎn)換器設(shè)置等方法,筆者覺得不常用,這里不做舉例,內(nèi)容的樣式通常在輸出文件之后再次加工會更加容易操作。寫操作一般可以簡化如下:
List<List<String>>?head?=?new?ArrayList<>(); List<List<String>>?data?=?new?LinkedList<>(); EasyExcel.write("輸出文件絕對路徑").head(head).excelType(ExcelTypeEnum.XLSX).sheet("target").doWrite(data);實用技巧
下面簡單介紹一下生產(chǎn)中用到的實用技巧。
多線程讀
使用EasyExcel多線程讀建議在限定的前提條件下使用:
源文件已經(jīng)被分割成多個小文件,并且每個小文件的標(biāo)題行和列數(shù)一致。
機器內(nèi)存要充足,因為并發(fā)讀取的結(jié)果最后需要合并成一個大的結(jié)果集,全部數(shù)據(jù)存放在內(nèi)存中。
經(jīng)常遇到外部反饋的多份文件需要緊急進行數(shù)據(jù)分析或者交叉校對,為了加快文件讀取,筆者通常使用這種方式批量讀取格式一致的Excel文件
?一個簡單的例子如下:
@Slf4j public?class?EasyExcelConcurrentRead?{static?final?int?N_CPU?=?Runtime.getRuntime().availableProcessors();public?static?void?main(String[]?args)?throws?Exception?{//?假設(shè)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("讀取[{}]目錄下的文件成功,一共加載:{}行數(shù)據(jù)",?dir,?mergeResult.size());//?其他業(yè)務(wù)邏輯.....}@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()提交并發(fā)讀的任務(wù),然后使用Future#get()等待所有任務(wù)完成之后再合并最終的讀取結(jié)果。
?注意,一般文件的寫操作不能并發(fā)執(zhí)行,否則很大的概率會導(dǎo)致數(shù)據(jù)錯亂
?多Sheet寫
多Sheet寫,其實就是使用同一個ExcelWriter實例,寫入多個WriteSheet實例中,每個Sheet的標(biāo)題行可以通過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的數(shù)據(jù)")),?firstSheet);WriteSheet?secondSheet?=?new?WriteSheet();secondSheet.setSheetName("second");secondSheet.setHead(Collections.singletonList(Collections.singletonList("第二個Sheet的Head")));//?寫入第二個命名為second的SheetexcelWriter.write(Collections.singletonList(Collections.singletonList("第二個Sheet的數(shù)據(jù)")),?secondSheet);excelWriter.finish();} }效果如下:
分頁查詢和批量寫
在一些數(shù)據(jù)量比較大的場景下,可以考慮分頁查詢和批量寫,其實就是分頁查詢原始數(shù)據(jù) -> 數(shù)據(jù)聚合或者轉(zhuǎn)換 -> 寫目標(biāo)數(shù)據(jù) -> 下一頁查詢....。其實數(shù)據(jù)量少的情況下,一次性全量查詢和全量寫也只是分頁查詢和批量寫的一個特例,因此可以把查詢、轉(zhuǎn)換和寫操作抽象成一個可復(fù)用的模板方法:
int?batchSize?=?定義每篇查詢的條數(shù); 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?轉(zhuǎn)換->?List<ExcelModel>?excelModelList;writer.write(excelModelList);} }參看筆者前面寫過的一篇非標(biāo)題黨生產(chǎn)應(yīng)用文章《百萬級別數(shù)據(jù)Excel導(dǎo)出優(yōu)化》,適用于大數(shù)據(jù)量導(dǎo)出的場景,代碼如下:
Excel上傳與下載
?下面的例子適用于Servlet容器,常見的如Tomcat,應(yīng)用于spring-boot-starter-web
?Excel文件上傳跟普通文件上傳的操作差不多,然后使用EasyExcel的ExcelReader讀取請求對象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();//?其他業(yè)務(wù)邏輯}return?ResponseEntity.ok("success"); }使用Postman請求如下:
使用EasyExcel進行Excel文件導(dǎo)出也比較簡單,只需要把響應(yīng)對象HttpServletResponse中攜帶的OutputStream對象附著到EasyExcel的ExcelWriter實例即可:
@GetMapping(path?=?"/download") public?void?download(HttpServletResponse?response)?throws?Exception?{//?這里文件名如果涉及中文一定要使用URL編碼,否則會亂碼String?fileName?=?URLEncoder.encode("文件名.xlsx",?StandardCharsets.UTF_8.toString());//?封裝標(biāo)題行List<List<String>>?head?=?new?ArrayList<>();//?封裝數(shù)據(jù)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編碼,否則一定會亂碼。
無論導(dǎo)入或者導(dǎo)出,如果數(shù)據(jù)量大比較耗時,使用了Nginx的話記得調(diào)整Nginx中的連接、讀寫超時時間的上限配置。
使用SpringBoot需要調(diào)整spring.servlet.multipart.max-request-size和spring.servlet.multipart.max-file-size的配置值,避免上傳的文件過大出現(xiàn)異常。
小結(jié)
EasyExcel的API設(shè)計簡單易用,可以使用他快速開發(fā)有Excel數(shù)據(jù)導(dǎo)入或者導(dǎo)出的場景,真是廣大 Javaer 人的福音。
往期推薦Java新特性:數(shù)據(jù)類型可以扔掉了?
多圖帶你徹底理解Java中的21種鎖!
JDK 竟然是這樣實現(xiàn)棧的?
關(guān)注下方二維碼,收獲更多干貨!
總結(jié)
以上是生活随笔為你收集整理的阿里巴巴开源的Excel操作神器!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 消息队列终极解决方案——Stream(下
- 下一篇: Redis 键值过期操作