SpringBoot 實現 excel 全自由匯入匯出,效能強的離譜,用起來還特優雅

程式設計師志哥發表於2022-03-03

一、簡介

在實際的業務系統開發過程中,操作 Excel 實現資料的匯入匯出基本上是個非常常見的需求。

之前,我們有介紹一款非常好用的工具:EasyPoi,有讀者提出在資料量大的情況下,EasyPoi 會佔用記憶體大,效能不夠好,嚴重的時候,還會出現記憶體異常的現象。

今天我給大家推薦一款效能更好的 Excel 匯入匯出工具:EasyExcel,希望對大家有所幫助!

easyexcel 是阿里開源的一款 Excel匯入匯出工具,具有處理速度快、佔用記憶體小、使用方便的特點,底層邏輯也是基於 apache poi 進行二次開發的,目前的應用也是非常廣!

相比 EasyPoi,EasyExcel 的處理資料效能非常高,讀取 75M (46W行25列) 的Excel,僅需使用 64M 記憶體,耗時 20s,極速模式還可以更快!

廢話也不多說了,下面直奔主題!

二、實踐

在 SpringBoot 專案中整合 EasyExcel 其實非常簡單,僅需一個依賴即可。

<!--EasyExcel相關依賴-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.0.5</version>
</dependency>

EasyExcel 的匯出匯入支援兩種方式進行處理

  • 第一種是通過實體類註解方式來生成檔案和反解析檔案資料對映成物件
  • 第二種是通過動態引數化生成檔案和反解析檔案資料

下面我們以使用者資訊的匯出匯入為例,分別介紹兩種處理方式。

簡單匯出

首先,我們只需要建立一個UserEntity使用者實體類,然後新增對應的註解欄位即可,示例程式碼如下:

public class UserWriteEntity {

    @ExcelProperty(value = "姓名")
    private String name;
	
    @ExcelProperty(value = "年齡")
    private int age;
	
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    @ExcelProperty(value = "操作時間")
    private Date time;
	
    //set、get...
}

然後,使用 EasyExcel 提供的EasyExcel工具類,即可實現檔案的匯出。

public static void main(String[] args) throws FileNotFoundException {
    List<UserWriteEntity> dataList = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        UserWriteEntity userEntity = new UserWriteEntity();
        userEntity.setName("張三" + i);
        userEntity.setAge(20 + i);
        userEntity.setTime(new Date(System.currentTimeMillis() + i));
        dataList.add(userEntity);
    }
    //定義檔案輸出位置
    FileOutputStream outputStream = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user1.xlsx"));
    EasyExcel.write(outputStream, UserWriteEntity.class).sheet("使用者資訊").doWrite(dataList);
}

執行程式,開啟檔案內容結果!

簡單匯入

這種簡單固定表頭的 Excel 檔案,如果想要讀取檔案資料,操作也很簡單。

以上面的匯出檔案為例,使用 EasyExcel 提供的EasyExcel工具類,即可來實現檔案內容資料的快速讀取,示例程式碼如下:

首先建立讀取實體類

/**
 * 讀取實體類
 */
public class UserReadEntity {

    @ExcelProperty(value = "姓名")
    private String name;
    /**
     * 強制讀取第三個 這裡不建議 index 和 name 同時用,要麼一個物件只用index,要麼一個物件只用name去匹配
     */
    @ExcelProperty(index = 1)
    private int age;
	
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    @ExcelProperty(value = "操作時間")
    private Date time;
	
    //set、get...
}

然後讀取檔案資料,並封裝到物件裡面

public static void main(String[] args) throws FileNotFoundException {
    //同步讀取檔案內容
    FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-user1.xls"));
    List<UserReadEntity> list = EasyExcel.read(inputStream).head(UserReadEntity.class).sheet().doReadSync();
    System.out.println(JSONArray.toJSONString(list));
}

執行程式,輸出結果如下:

[{"age":20,"name":"張三0","time":1616920360000},{"age":21,"name":"張三1","time":1616920360000},{"age":22,"name":"張三2","time":1616920360000},{"age":23,"name":"張三3","time":1616920360000},{"age":24,"name":"張三4","time":1616920360000},{"age":25,"name":"張三5","time":1616920360000},{"age":26,"name":"張三6","time":1616920360000},{"age":27,"name":"張三7","time":1616920360000},{"age":28,"name":"張三8","time":1616920360000},{"age":29,"name":"張三9","time":1616920360000}]

動態自由匯出匯入

在實際使用開發中,我們不可能每來一個 excel 匯入匯出需求,就編寫一個實體類,很多業務需求需要根據不同的欄位來動態匯入匯出,沒辦法基於實體類註解的方式來讀取檔案或者寫入檔案。

因此,基於EasyExcel提供的動態引數化生成檔案和動態監聽器讀取檔案方法,我們可以單獨封裝一套動態匯出匯出工具類,省的我們每次都需要重新編寫大量重複工作,以下就是小編我在實際使用過程,封裝出來的工具類,在此分享給大家!

  • 首先,我們可以編寫一個動態匯出工具類
public class DynamicEasyExcelExportUtils {

    private static final Logger log = LoggerFactory.getLogger(DynamicEasyExcelExportUtils.class);

    private static final String DEFAULT_SHEET_NAME = "sheet1";

    /**
     * 動態生成匯出模版(單表頭)
     * @param headColumns 列名稱
     * @return            excel檔案流
     */
    public static byte[] exportTemplateExcelFile(List<String> headColumns){
        List<List<String>> excelHead = Lists.newArrayList();
        headColumns.forEach(columnName -> { excelHead.add(Lists.newArrayList(columnName)); });
        byte[] stream = createExcelFile(excelHead, new ArrayList<>());
        return stream;
    }

    /**
     * 動態生成模版(複雜表頭)
     * @param excelHead   列名稱
     * @return
     */
    public static byte[] exportTemplateExcelFileCustomHead(List<List<String>> excelHead){
        byte[] stream = createExcelFile(excelHead, new ArrayList<>());
        return stream;
    }

    /**
     * 動態匯出檔案(通過map方式計算)
     * @param headColumnMap  有序列頭部
     * @param dataList       資料體
     * @return
     */
    public static byte[] exportExcelFile(LinkedHashMap<String, String> headColumnMap, List<Map<String, Object>> dataList){
        //獲取列名稱
        List<List<String>> excelHead = new ArrayList<>();
        if(MapUtils.isNotEmpty(headColumnMap)){
            //key為匹配符,value為列名,如果多級列名用逗號隔開
            headColumnMap.entrySet().forEach(entry -> {
                excelHead.add(Lists.newArrayList(entry.getValue().split(",")));
            });
        }
        List<List<Object>> excelRows = new ArrayList<>();
        if(MapUtils.isNotEmpty(headColumnMap) && CollectionUtils.isNotEmpty(dataList)){
            for (Map<String, Object> dataMap : dataList) {
                List<Object> rows = new ArrayList<>();
                headColumnMap.entrySet().forEach(headColumnEntry -> {
                    if(dataMap.containsKey(headColumnEntry.getKey())){
                        Object data = dataMap.get(headColumnEntry.getKey());
                        rows.add(data);
                    }
                });
                excelRows.add(rows);
            }
        }
        byte[] stream = createExcelFile(excelHead, excelRows);
        return stream;
    }


    /**
     * 生成檔案(自定義頭部排列)
     * @param rowHeads
     * @param excelRows
     * @return
     */
    public static byte[] customerExportExcelFile(List<List<String>> rowHeads, List<List<Object>> excelRows){
        //將行頭部轉成easyexcel能識別的部分
        List<List<String>> excelHead = transferHead(rowHeads);
        return createExcelFile(excelHead, excelRows);
    }

    /**
     * 生成檔案
     * @param excelHead
     * @param excelRows
     * @return
     */
    private static byte[] createExcelFile(List<List<String>> excelHead, List<List<Object>> excelRows){
        try {
            if(CollectionUtils.isNotEmpty(excelHead)){
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                EasyExcel.write(outputStream).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                        .head(excelHead)
                        .sheet(DEFAULT_SHEET_NAME)
                        .doWrite(excelRows);
                return outputStream.toByteArray();
            }
        } catch (Exception e) {
            log.error("動態生成excel檔案失敗,headColumns:" + JSONArray.toJSONString(excelHead) + ",excelRows:" + JSONArray.toJSONString(excelRows), e);
        }
        return null;
    }

    /**
     * 將行頭部轉成easyexcel能識別的部分
     * @param rowHeads
     * @return
     */
    public static List<List<String>> transferHead(List<List<String>> rowHeads){
        //將頭部列進行反轉
        List<List<String>> realHead = new ArrayList<>();
        if(CollectionUtils.isNotEmpty(rowHeads)){
            Map<Integer, List<String>> cellMap = new LinkedHashMap<>();
            //遍歷行
            for (List<String> cells : rowHeads) {
                //遍歷列
                for (int i = 0; i < cells.size(); i++) {
                    if(cellMap.containsKey(i)){
                        cellMap.get(i).add(cells.get(i));
                    } else {
                        cellMap.put(i, Lists.newArrayList(cells.get(i)));
                    }
                }
            }
            //將列一行一行加入realHead
            cellMap.entrySet().forEach(item -> realHead.add(item.getValue()));
        }
        return realHead;
    }

    /**
     * 匯出檔案測試
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        //匯出包含資料內容的檔案(方式一)
        LinkedHashMap<String, String> headColumnMap = Maps.newLinkedHashMap();
        headColumnMap.put("className","班級");
        headColumnMap.put("name","學生資訊,姓名");
        headColumnMap.put("sex","學生資訊,性別");
        List<Map<String, Object>> dataList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Map<String, Object> dataMap = Maps.newHashMap();
            dataMap.put("className", "一年級");
            dataMap.put("name", "張三" + i);
            dataMap.put("sex", "男");
            dataList.add(dataMap);
        }
        byte[] stream1 = exportExcelFile(headColumnMap, dataList);
        FileOutputStream outputStream1 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx"));
        outputStream1.write(stream1);
        outputStream1.close();


        //匯出包含資料內容的檔案(方式二)
        //頭部,第一層
        List<String> head1 = new ArrayList<>();
        head1.add("第一行頭部列1");
        head1.add("第一行頭部列1");
        head1.add("第一行頭部列1");
        head1.add("第一行頭部列1");
        //頭部,第二層
        List<String> head2 = new ArrayList<>();
        head2.add("第二行頭部列1");
        head2.add("第二行頭部列1");
        head2.add("第二行頭部列2");
        head2.add("第二行頭部列2");
        //頭部,第三層
        List<String> head3 = new ArrayList<>();
        head3.add("第三行頭部列1");
        head3.add("第三行頭部列2");
        head3.add("第三行頭部列3");
        head3.add("第三行頭部列4");

        //封裝頭部
        List<List<String>> allHead = new ArrayList<>();
        allHead.add(head1);
        allHead.add(head2);
        allHead.add(head3);

        //封裝資料體
        //第一行資料
        List<Object> data1 = Lists.newArrayList(1,1,1,1);
        //第二行資料
        List<Object> data2 = Lists.newArrayList(2,2,2,2);
        List<List<Object>> allData = Lists.newArrayList(data1, data2);

        byte[] stream2 = customerExportExcelFile(allHead, allData);
        FileOutputStream outputStream2 = new FileOutputStream(new File("/Users/panzhi/Documents/easyexcel-export-user6.xlsx"));
        outputStream2.write(stream2);
        outputStream2.close();


    }
}
  • 然後,編寫一個動態匯入工具類
/**
 * 建立一個檔案讀取監聽器
 */
public class DynamicEasyExcelListener extends AnalysisEventListener<Map<Integer, String>> {
    private static final Logger LOGGER = LoggerFactory.getLogger(UserDataListener.class);
    /**
     * 表頭資料(儲存所有的表頭資料)
     */
    private List<Map<Integer, String>> headList = new ArrayList<>();
    /**
     * 資料體
     */
    private List<Map<Integer, String>> dataList = new ArrayList<>();
    /**
     * 這裡會一行行的返回頭
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        LOGGER.info("解析到一條頭資料:{}", JSON.toJSONString(headMap));
        //儲存全部表頭資料
        headList.add(headMap);
    }
    /**
     * 這個每一條資料解析都會來呼叫
     *
     * @param data
     *            one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        LOGGER.info("解析到一條資料:{}", JSON.toJSONString(data));
        dataList.add(data);
    }
    /**
     * 所有資料解析完成了 都會來呼叫
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 這裡也要儲存資料,確保最後遺留的資料也儲存到資料庫
        LOGGER.info("所有資料解析完成!");
    }
    public List<Map<Integer, String>> getHeadList() {
        return headList;
    }
    public List<Map<Integer, String>> getDataList() {
        return dataList;
    }
}
  • 動態匯入工具類
/**
 * 編寫匯入工具類
 */
public class DynamicEasyExcelImportUtils {
    /**
     * 動態獲取全部列和資料體,預設從第一行開始解析資料
     * @param stream
     * @return
     */
    public static List<Map<String,String>> parseExcelToView(byte[] stream) {
        return parseExcelToView(stream, 1);
    }
    /**
     * 動態獲取全部列和資料體
     * @param stream           excel檔案流
     * @param parseRowNumber   指定讀取行
     * @return
     */
    public static List<Map<String,String>> parseExcelToView(byte[] stream, Integer parseRowNumber) {
        DynamicEasyExcelListener readListener = new DynamicEasyExcelListener();
        EasyExcelFactory.read(new ByteArrayInputStream(stream)).registerReadListener(readListener).headRowNumber(parseRowNumber).sheet(0).doRead();
        List<Map<Integer, String>> headList = readListener.getHeadList();
        if(CollectionUtils.isEmpty(headList)){
            throw new RuntimeException("Excel未包含表頭");
        }
        List<Map<Integer, String>> dataList = readListener.getDataList();
        if(CollectionUtils.isEmpty(dataList)){
            throw new RuntimeException("Excel未包含資料");
        }
        //獲取頭部,取最後一次解析的列頭資料
        Map<Integer, String> excelHeadIdxNameMap = headList.get(headList.size() -1);
        //封裝資料體
        List<Map<String,String>> excelDataList = Lists.newArrayList();
        for (Map<Integer, String> dataRow : dataList) {
            Map<String,String> rowData = new LinkedHashMap<>();
            excelHeadIdxNameMap.entrySet().forEach(columnHead -> {
                rowData.put(columnHead.getValue(), dataRow.get(columnHead.getKey()));
            });
            excelDataList.add(rowData);
        }
        return excelDataList;
    }
    /**
     * 檔案匯入測試
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        FileInputStream inputStream = new FileInputStream(new File("/Users/panzhi/Documents/easyexcel-export-user5.xlsx"));
        byte[] stream = IoUtils.toByteArray(inputStream);
        List<Map<String,String>> dataList = parseExcelToView(stream, 2);
        System.out.println(JSONArray.toJSONString(dataList));
        inputStream.close();
    }
}

為了方便後續的操作流程,在解析資料的時候,會將列名作為key

三、小結

在實際的業務開發過程中,根據引數動態實現 Excel 的匯出匯入還是非常廣的。

當然,EasyExcel 的功能還不只上面介紹的那些內容,還有基於模版進行 excel的填充,web 端 restful 的匯出匯出,使用方法大致都差不多,更多的功能,會在後續的文章中再次介紹,如果有描述不對的地方,歡迎網友批評吐槽!

四、參考

1、easyexcel - 介面文件

相關文章