[EasyExcel] 匯出合併單元格

2023-09-07 12:00:17

前言

使用spring boot 對excel 進行操作在平時專案中要經常使用。常見通過jxl和poi 的方式進行操作。但他們都存在一個嚴重的問題就是非常的耗記憶體。這裡介紹一種 Easy Excel 工具來對excel進行操作。

一、Easy Excel是什麼?

EasyExcel是阿里巴巴開源的一個excel處理框架,以使用簡單、節省記憶體著稱。easyExcel能大大減少佔用記憶體的主要原因是在解析Excel時沒有將檔案資料一次性全部載入到記憶體中,而是從磁碟上一行行讀取資料,逐個解析。

二、使用EasyExcel 實現讀操作

從excel 中讀取資料,常用的場景就是讀取excel的資料,將相應的資料儲存到資料庫中。需要實現一定的邏輯處理。

1、匯入依賴

<dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>easyexcel</artifactId>
      <version>2.2.10</version>
</dependency>

2、建立讀取資料封裝類

@Data
public class User {

    @ExcelProperty(index = 0)
    private Integer id;

    @ExcelProperty(index = 1)
    private String name;

    @ExcelProperty(index = 2)
    private Integer age;
}

比如我們要讀取兩列的資料,就寫兩個屬性。@ExcelProperty(index = 0)來設定要讀取的列,index=0表示讀取第一列。

3、建立讀取excel的監聽類

監聽器繼承 AnalysisEventListener 類

@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {

    /**
     * 解析excel檔案的每一行
     * @param user 引數user即是每行讀取資料轉換的User物件
     * @param analysisContext
     */
    @Override
    public void invoke(User user, AnalysisContext analysisContext){
        log.info("excel資料行:{}",user.toString());
    }

    /**
     * 整個檔案解析完執行
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("檔案解析完畢");
    }
}

當解析每一條資料時都會呼叫invoke方法,當所有資料都解析完畢時最後會呼叫doAfterAllAnalysed方法。可以在監聽類內的方法中將每次讀取到的資料進行儲存或者其他操作處理。

4、介面使用easyExcel讀取excel檔案呼叫監聽器

/**
     * 上傳excel檔案並讀取其中內容
     *
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public String uploadExcel(MultipartFile file) {
        log.info("easyExcel上傳檔案:{}", file);
        try {
            InputStream inputStream = file.getInputStream();
            EasyExcel.read(inputStream, User.class, new UserExcelListener())
                    .sheet()
                    .doRead();
        } catch (Exception e) {

        }
        return "表格檔案上傳成功";
    }

三、使用EasyExcel 實現寫操作

寫操作有兩種寫法,一種是不建立物件的寫入,另一種是根據物件寫入。這裡主要介紹建立物件寫入

建立物件寫入

1、建立excel物件類

@Data
public class User {

    @ExcelProperty(index = 0)
    private Integer id;

    @ExcelProperty(index = 1)
    private String name;

    @ExcelProperty(index = 2)
    private Integer age;
}

注意@ExcelProperty(「使用者編號」) 會生成相應的列名為 使用者編號,如果不設定,則會直接將欄位名設定為excel的列名。

2、介面使用測試資料匯出(常規匯出不合並單元格)

/**
     * 輸出匯出excel
     */
    @PostMapping("/export")
    public void export() {
        ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setId(i);
            user.setName("測試使用者-" + i);
            user.setAge(20 + i);
            users.add(user);
        }
        log.info("匯出資料結果集:{}", users);
        String fileName = "C:\\Users\\pytho\\Desktop\\fsdownload\\使用者資訊表.xlsx";
        EasyExcel.write(fileName, User.class)
                .autoCloseStream(true)
                .sheet("sheet名稱")
                .doWrite(users);
    }

3、介面測試匯出(單列合併單元格)

/**
     * 輸出匯出excel
     */
    @PostMapping("/export1")
    public void export1() {
        ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setId(i);
            if (i == 3 || i == 4 || i == 5) {
                user.setName("測試使用者-3");
            } else {
                user.setName("測試使用者-" + i);
            }
            user.setAge(20 + i);
            users.add(user);
        }
        log.info("匯出資料結果集:{}", users);
        String fileName = "C:\\Users\\pytho\\Desktop\\fsdownload\\(單列相同內容合併單元格)使用者資訊表.xlsx";
        EasyExcel.write(fileName, User.class)
                .registerWriteHandler(new SimpleExcelMergeUtil())
                .autoCloseStream(true)
                .sheet("sheet名稱")
                .doWrite(users);
    }

如果要對匯出的excel進行處理,就需要自定義處理器類進行處理

自定義easyExcel處理器(單列合併:根據使用者id相同的列進行合併單元格):

/**
 * @version 1.0
 * @Package: com.stech.bms.buss.utils
 * @ClassName: ExcelMergeUtil
 * @Author: sgq
 * @Date: 2023/7/28 13:29
 * @Description: 僅處理單列資料相同合併單元格
 */
public class SimpleExcelMergeUtil implements CellWriteHandler {

    public SimpleExcelMergeUtil() {
    }

    /**
     * 建立每個單元格之前執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param row
     * @param head
     * @param columnIndex
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

    }

    /**
     * 建立每個單元格之後執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param cell
     * @param head
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    /**
     * 每個單元格資料內容渲染之後執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param cellData
     * @param cell
     * @param head
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    /**
     * 每個單元格完全建立完之後執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param cellDataList
     * @param cell
     * @param head
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 當前行
        int curRowIndex = cell.getRowIndex();
        // 當前列
        int curColIndex = cell.getColumnIndex();

        if (!isHead) {
            if (curRowIndex > 1 && curColIndex == 1) {
                // 從第二行資料行開始,獲取當前行第二列資料
                Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
                // 獲取上一行第二列資料
                Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
                Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
                if (curData.equals(preData)) {
                    Sheet sheet = writeSheetHolder.getSheet();
                    List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
                    boolean isMerged = false;
                    for (int i = 0; i < mergedRegions.size() && !isMerged; i++) {
                        CellRangeAddress cellRangeAddr = mergedRegions.get(i);
                        // 若上一個單元格已經被合併,則先移出原有的合併單元,再重新新增合併單元
                        if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
                            sheet.removeMergedRegion(i);
                            cellRangeAddr.setLastRow(curRowIndex);
                            sheet.addMergedRegion(cellRangeAddr);
                            isMerged = true;
                        }
                    }
                    // 若上一個單元格未被合併,則新增合併單元
                    if (!isMerged) {
                        CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
                        sheet.addMergedRegion(cellRangeAddress);
                    }
                }
            }
        }
    }
}

4、介面測試匯出(通用合併單元格)

/**
     * 輸出匯出excel
     */
    @PostMapping("/export2")
    public void export2() {
        ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setId(i);
            if (i == 3 || i == 4 || i == 5) {
                user.setName("測試使用者-3");
            } else {
                user.setName("測試使用者-" + i);
            }
            user.setAge(20 + i);
            users.add(user);
        }
        log.info("匯出資料結果集:{}", users);
        // 從第幾行開始合併
        int mergeStartRowIndex = 5;
        // 需要合併哪些列
        int[] mergeColumns = {1};
        String fileName = "C:\\Users\\pytho\\Desktop\\fsdownload\\(單列相同內容合併單元格-通用版)使用者資訊表.xlsx";
        EasyExcel.write(fileName, User.class)
                .registerWriteHandler(new SimpleCommonExcelMergeUtil(mergeStartRowIndex,mergeColumns))
                .autoCloseStream(true)
                .sheet("sheet名稱")
                .doWrite(users);
    }

excel處理器類:

/**
 * @version 1.0
 * @Package: com.stech.bms.buss.utils
 * @ClassName: ExcelMergeUtil
 * @Author: sgq
 * @Date: 2023/7/28 13:29
 * @Description: 僅處理單列資料相同合併單元格
 */
public class SimpleCommonExcelMergeUtil implements CellWriteHandler {

    private int mergeStartRowIndex;
    private int[] mergeColumns;
    private List<Integer> mergeColumnList;

    public SimpleCommonExcelMergeUtil() {
    }

    public SimpleCommonExcelMergeUtil(int mergeStartRowIndex, int[] mergeColumns) {
        this.mergeStartRowIndex = mergeStartRowIndex;
        this.mergeColumns = mergeColumns;
        mergeColumnList = new ArrayList<>();
        for (int i : mergeColumns) {
            mergeColumnList.add(i);
        }
    }

    /**
     * 建立每個單元格之前執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param row
     * @param head
     * @param columnIndex
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

    }

    /**
     * 建立每個單元格之後執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param cell
     * @param head
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    /**
     * 每個單元格資料內容渲染之後執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param cellData
     * @param cell
     * @param head
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    /**
     * 每個單元格完全建立完之後執行
     *
     * @param writeSheetHolder
     * @param writeTableHolder
     * @param cellDataList
     * @param cell
     * @param head
     * @param relativeRowIndex
     * @param isHead
     */
    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 當前行
        int curRowIndex = cell.getRowIndex();
        // 當前列
        int curColIndex = cell.getColumnIndex();

        if (!isHead) {
            if (curRowIndex > mergeStartRowIndex && mergeColumnList.contains(curColIndex)) {
                // 從第二行資料行開始,獲取當前行第二列資料
                Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
                // 獲取上一行第二列資料
                Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
                Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
                if (curData.equals(preData)) {
                    Sheet sheet = writeSheetHolder.getSheet();
                    List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
                    boolean isMerged = false;
                    for (int i = 0; i < mergedRegions.size() && !isMerged; i++) {
                        CellRangeAddress cellRangeAddr = mergedRegions.get(i);
                        // 若上一個單元格已經被合併,則先移出原有的合併單元,再重新新增合併單元
                        if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
                            sheet.removeMergedRegion(i);
                            cellRangeAddr.setLastRow(curRowIndex);
                            sheet.addMergedRegion(cellRangeAddr);
                            isMerged = true;
                        }
                    }
                    // 若上一個單元格未被合併,則新增合併單元
                    if (!isMerged) {
                        CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
                        sheet.addMergedRegion(cellRangeAddress);
                    }
                }
            }
        }
    }
}

這只是簡單的合併單元格例子,拋磚引玉的作用。工作中可能會遇到很多情況:合併單元格後第一列序列號也需要根據其他列進行合併單元格且序列號還必須保持連續,根據部分列合併單元格,隔行合併單元格等等情況,這就需要開發者對easyExcel的處理器類裡面的api比較瞭解才能完成。遇到的問題也可以留言,看到也會嘗試一起處理解決。