App下載

EasyExcel 阿里巴巴開(kāi)源的Excel操作神器!

猿友 2020-09-29 15:01:46 瀏覽數(shù) (4702)
反饋

文章來(lái)源于公眾號(hào):Throwable ,作者Throwable

前提

導(dǎo)出數(shù)據(jù)到Excel是非常常見(jiàn)的后端需求之一,今天來(lái)推薦一款阿里出品的Excel操作神器:EasyExcel。EasyExcel從其依賴(lài)樹(shù)來(lái)看是對(duì)apache-poi的封裝,筆者從開(kāi)始接觸Excel處理就選用了EasyExcel,避免了廣泛流傳的apache-poi導(dǎo)致的內(nèi)存泄漏問(wèn)題。

引入EasyExcel依賴(lài)

引入EasyExcelMaven如下:



    com.alibaba
    easyexcel
    ${easyexcel.version}

當(dāng)前(2020-09)的最新版本為2.2.6。

API簡(jiǎn)介

Excel文件主要圍繞讀和寫(xiě)操作進(jìn)行處理,EasyExcelAPI也是圍繞這兩個(gè)方面進(jìn)行設(shè)計(jì)。先看讀操作的相關(guān)API

// 新建一個(gè)ExcelReaderBuilder實(shí)例
ExcelReaderBuilder readerBuilder = EasyExcel.read();
// 讀取的文件對(duì)象,可以是File、路徑(字符串)或者InputStream實(shí)例
readerBuilder.file("");
// 文件的密碼
readerBuilder.password("");
// 指定sheet,可以是數(shù)字序號(hào)sheetNo或者字符串sheetName,若不指定則會(huì)讀取所有的sheet
readerBuilder.sheet("");
// 是否自動(dòng)關(guān)閉輸入流
readerBuilder.autoCloseStream(true);
// Excel文件格式,包括ExcelTypeEnum.XLSX和ExcelTypeEnum.XLS
readerBuilder.excelType(ExcelTypeEnum.XLSX);
// 指定文件的標(biāo)題行,可以是Class對(duì)象(結(jié)合@ExcelProperty注解使用),或者List實(shí)例
readerBuilder.head(Collections.singletonList(Collections.singletonList("head")));
// 注冊(cè)讀取事件的監(jiān)聽(tīng)器,默認(rèn)的數(shù)據(jù)類(lèi)型為Map,第一列的元素的下標(biāo)從0開(kāi)始
readerBuilder.registerReadListener(new AnalysisEventListener() {


    @Override
    public void invokeHeadMap(Map headMap, AnalysisContext context) {
        // 這里會(huì)回調(diào)標(biāo)題行,文件內(nèi)容的首行會(huì)認(rèn)為是標(biāo)題行
    }


    @Override
    public void invoke(Object o, AnalysisContext analysisContext) {
        // 這里會(huì)回調(diào)每行的數(shù)據(jù)
    }


    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {


    }
});
// 構(gòu)建讀取器
ExcelReader excelReader = readerBuilder.build();
// 讀取數(shù)據(jù)
excelReader.readAll();
excelReader.finish();

可以看到,讀操作主要使用Builder模式和事件監(jiān)聽(tīng)(或者可以理解為「觀察者模式」)的設(shè)計(jì)。一般情況下,上面的代碼可以簡(jiǎn)化如下:

Map head = new HashMap();
List data = new LinkedList();
EasyExcel.read("文件的絕對(duì)路徑").sheet()
        .registerReadListener(new AnalysisEventListener() {


            @Override
            public void invokeHeadMap(Map headMap, AnalysisContext context) {
                head.putAll(headMap);
            }


            @Override
            public void invoke(Map row, AnalysisContext analysisContext) {
                data.add(row);
            }


            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                    // 這里可以打印日志告知所有行讀取完畢
            }
        }).doRead();

如果需要讀取數(shù)據(jù)并且轉(zhuǎn)換為對(duì)應(yīng)的對(duì)象列表,則需要指定標(biāo)題行的Class,結(jié)合注解@ExcelProperty使用:

文件內(nèi)容:


|訂單編號(hào)|手機(jī)號(hào)|
|ORDER_ID_1|112222|
|ORDER_ID_2|334455|


@Data
private static class OrderDTO {


    @ExcelProperty(value = "訂單編號(hào)")
    private String orderId;


    @ExcelProperty(value = "手機(jī)號(hào)")
    private String phone;
}


Map head = new HashMap();
List data = new LinkedList();
EasyExcel.read("文件的絕對(duì)路徑").head(OrderDTO.class).sheet()
        .registerReadListener(new AnalysisEventListener() {


            @Override
            public void invokeHeadMap(Map headMap, AnalysisContext context) {
                head.putAll(headMap);
            }


            @Override
            public void invoke(OrderDTO row, AnalysisContext analysisContext) {
                data.add(row);
            }


            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                // 這里可以打印日志告知所有行讀取完畢
            }
        }).doRead();

「如果數(shù)據(jù)量巨大,建議使用Map類(lèi)型讀取和操作數(shù)據(jù)對(duì)象,否則大量的反射操作會(huì)使讀取數(shù)據(jù)的耗時(shí)大大增加,極端情況下,例如屬性多的時(shí)候反射操作的耗時(shí)有可能比讀取和遍歷的時(shí)間長(zhǎng)」。

接著看寫(xiě)操作的API

// 新建一個(gè)ExcelWriterBuilder實(shí)例
ExcelWriterBuilder writerBuilder = EasyExcel.write();
// 輸出的文件對(duì)象,可以是File、路徑(字符串)或者OutputStream實(shí)例
writerBuilder.file("");
// 指定sheet,可以是數(shù)字序號(hào)sheetNo或者字符串sheetName,可以不設(shè)置,由下面提到的WriteSheet覆蓋
writerBuilder.sheet("");
// 文件的密碼
writerBuilder.password("");
// Excel文件格式,包括ExcelTypeEnum.XLSX和ExcelTypeEnum.XLS
writerBuilder.excelType(ExcelTypeEnum.XLSX);
// 是否自動(dòng)關(guān)閉輸出流
writerBuilder.autoCloseStream(true);
// 指定文件的標(biāo)題行,可以是Class對(duì)象(結(jié)合@ExcelProperty注解使用),或者List實(shí)例
writerBuilder.head(Collections.singletonList(Collections.singletonList("head")));
// 構(gòu)建ExcelWriter實(shí)例
ExcelWriter excelWriter = writerBuilder.build();
List 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è)置等方法,筆者覺(jué)得不常用,這里不做舉例,內(nèi)容的樣式通常在輸出文件之后再次加工會(huì)更加容易操作。寫(xiě)操作一般可以簡(jiǎn)化如下:

List head = new ArrayList();
List data = new LinkedList();
EasyExcel.write("輸出文件絕對(duì)路徑")
        .head(head)
        .excelType(ExcelTypeEnum.XLSX)
        .sheet("target")
        .doWrite(data);

實(shí)用技巧

下面簡(jiǎn)單介紹一下生產(chǎn)中用到的實(shí)用技巧。

多線(xiàn)程讀

使用EasyExcel多線(xiàn)程讀建議在限定的前提條件下使用:

  • 源文件已經(jīng)被分割成多個(gè)小文件,并且每個(gè)小文件的標(biāo)題行和列數(shù)一致。
  • 機(jī)器內(nèi)存要充足,因?yàn)椴l(fā)讀取的結(jié)果最后需要合并成一個(gè)大的結(jié)果集,全部數(shù)據(jù)存放在內(nèi)存中。

經(jīng)常遇到外部反饋的多份文件需要緊急進(jìn)行數(shù)據(jù)分析或者交叉校對(duì),為了加快文件讀取,筆者通常使用這種方式批量讀取格式一致的Excel文件

一個(gè)簡(jiǎn)單的例子如下:

@Slf4j
public class EasyExcelConcurrentRead {


    static final int N_CPU = Runtime.getRuntime().availableProcessors();


    public static void main(String[] args) throws Exception {
        // 假設(shè)I盤(pán)的temp目錄下有一堆同格式的Excel文件
        String dir = "I:\\temp";
        List 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();


            @Override
            public 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 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 future : futures) {
                mergeResult.addAll(future.get());
            }
        }
        log.info("讀取[{}]目錄下的文件成功,一共加載:{}行數(shù)據(jù)", dir, mergeResult.size());
        // 其他業(yè)務(wù)邏輯.....
    }


    @RequiredArgsConstructor
    private static class ReadTask implements Callable {


        private final String location;


        @Override
        public List call() throws Exception {
            List data = Lists.newLinkedList();
            EasyExcel.read(location).sheet()
                    .registerReadListener(new AnalysisEventListener() {


                        @Override
                        public void invoke(Map row, AnalysisContext analysisContext) {
                            data.add(row);
                        }


                        @Override
                        public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                            log.info("讀取路徑[{}]文件成功,一共[{}]行", location, data.size());
                        }
                    }).doRead();
            return data;
        }
    }
}

這里采用ThreadPoolExecutor#submit()提交并發(fā)讀的任務(wù),然后使用Future#get()等待所有任務(wù)完成之后再合并最終的讀取結(jié)果。

注意,一般文件的寫(xiě)操作不能并發(fā)執(zhí)行,否則很大的概率會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)亂

多Sheet寫(xiě)

Sheet寫(xiě),其實(shí)就是使用同一個(gè)ExcelWriter實(shí)例,寫(xiě)入多個(gè)WriteSheet實(shí)例中,每個(gè)Sheet的標(biāo)題行可以通過(guò)WriteSheet實(shí)例中的配置屬性進(jìn)行覆蓋,代碼如下:

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("第一個(gè)Sheet的Head")));
        // 寫(xiě)入第一個(gè)命名為first的Sheet
        excelWriter.write(Collections.singletonList(Collections.singletonList("第一個(gè)Sheet的數(shù)據(jù)")), firstSheet);
        WriteSheet secondSheet = new WriteSheet();
        secondSheet.setSheetName("second");
        secondSheet.setHead(Collections.singletonList(Collections.singletonList("第二個(gè)Sheet的Head")));
        // 寫(xiě)入第二個(gè)命名為second的Sheet
        excelWriter.write(Collections.singletonList(Collections.singletonList("第二個(gè)Sheet的數(shù)據(jù)")), secondSheet);
        excelWriter.finish();
    }
}

效果如下:

多Sheet寫(xiě)

分頁(yè)查詢(xún)和批量寫(xiě)

在一些數(shù)據(jù)量比較大的場(chǎng)景下,可以考慮分頁(yè)查詢(xún)和批量寫(xiě),其實(shí)就是分頁(yè)查詢(xún)?cè)紨?shù)據(jù) -> 數(shù)據(jù)聚合或者轉(zhuǎn)換 -> 寫(xiě)目標(biāo)數(shù)據(jù) -> 下一頁(yè)查詢(xún)....。其實(shí)數(shù)據(jù)量少的情況下,一次性全量查詢(xún)和全量寫(xiě)也只是分頁(yè)查詢(xún)和批量寫(xiě)的一個(gè)特例,因此可以把查詢(xún)、轉(zhuǎn)換和寫(xiě)操作抽象成一個(gè)可復(fù)用的模板方法:

int batchSize = 定義每篇查詢(xún)的條數(shù);
OutputStream outputStream = 定義寫(xiě)到何處;
ExcelWriter writer = new ExcelWriterBuilder()
        .autoCloseStream(true)
        .file(outputStream)
        .excelType(ExcelTypeEnum.XLSX)
        .head(ExcelModel.class);
for (;;){
    List list = originModelRepository.分頁(yè)查詢(xún)();
    if (list.isEmpty()){
        writer.finish();
        break;
    }else {
        list 轉(zhuǎn)換-> List excelModelList;
        writer.write(excelModelList);
    }
}

參看筆者前面寫(xiě)過(guò)的一篇非標(biāo)題黨生產(chǎn)應(yīng)用文章《百萬(wàn)級(jí)別數(shù)據(jù)Excel導(dǎo)出優(yōu)化》,適用于大數(shù)據(jù)量導(dǎo)出的場(chǎng)景,代碼如下:

分頁(yè)查詢(xún)和批量寫(xiě)

Excel上傳與下載

下面的例子適用于Servlet容器,常見(jiàn)的如Tomcat,應(yīng)用于spring-boot-starter-web

Excel文件上傳跟普通文件上傳的操作差不多,然后使用EasyExcelExcelReader讀取請(qǐng)求對(duì)象MultipartHttpServletRequest中文件部分抽象的InputStream實(shí)例即可:

@PostMapping(path = "/upload")
public ResponseEntity upload(MultipartHttpServletRequest request) throws Exception {
    Map fileMap = request.getFileMap();
    for (Map.Entry part : fileMap.entrySet()) {
        InputStream inputStream = part.getValue().getInputStream();
        Map head = new HashMap();
        List data = new LinkedList();
        EasyExcel.read(inputStream).sheet()
                .registerReadListener(new AnalysisEventListener() {


                    @Override
                    public void invokeHeadMap(Map headMap, AnalysisContext context) {
                        head.putAll(headMap);
                    }


                    @Override
                    public void invoke(Map row, AnalysisContext analysisContext) {
                        data.add(row);
                    }


                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("讀取文件[{}]成功,一共:{}行......", part.getKey(), data.size());
                    }
                }).doRead();
        // 其他業(yè)務(wù)邏輯
    }
    return ResponseEntity.ok("success");
}

使用Postman請(qǐng)求如下:

使用Postman請(qǐng)求

使用EasyExcel進(jìn)行Excel文件導(dǎo)出也比較簡(jiǎn)單,只需要把響應(yīng)對(duì)象HttpServletResponse中攜帶的OutputStream對(duì)象附著到EasyExcelExcelWriter實(shí)例即可:

@GetMapping(path = "/download")
public void download(HttpServletResponse response) throws Exception {
    // 這里文件名如果涉及中文一定要使用URL編碼,否則會(huì)亂碼
    String fileName = URLEncoder.encode("文件名.xlsx", StandardCharsets.UTF_8.toString());
    // 封裝標(biāo)題行
    List head = new ArrayList();
    // 封裝數(shù)據(jù)
    List 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);
}

這里需要注意一下:

  • 文件名如果包含中文,需要進(jìn)行URL編碼,否則一定會(huì)亂碼。
  • 無(wú)論導(dǎo)入或者導(dǎo)出,如果數(shù)據(jù)量大比較耗時(shí),使用了Nginx的話(huà)記得調(diào)整Nginx中的連接、讀寫(xiě)超時(shí)時(shí)間的上限配置。
  • 使用SpringBoot需要調(diào)整spring.servlet.multipart.max-request-sizespring.servlet.multipart.max-file-size的配置值,避免上傳的文件過(guò)大出現(xiàn)異常。

小結(jié)

EasyExcelAPI設(shè)計(jì)簡(jiǎn)單易用,可以使用他快速開(kāi)發(fā)有Excel數(shù)據(jù)導(dǎo)入或者導(dǎo)出的場(chǎng)景,真是廣大 Javaer 人的福音。

以上就是W3Cschool編程獅關(guān)于EasyExcel 阿里巴巴開(kāi)源的Excel操作神器!的相關(guān)介紹了,希望對(duì)大家有所幫助。

0 人點(diǎn)贊