1. 需求:將一個源excel的多個sheet頁,合並到目標excel的單個sheet頁中, 源excel可能需要內部自身的子sheet頁合並,合並的時候做到將源excel中的單元格是公式的值賦值到目標單元格中,同時完整的合並源excel中出現的合並單元格。
2. 代碼實現
2.1 業務table設計
主表:源excel中需要合並到目標excel的sheet信息

CREATE TABLE `sheet_message` ( `id` int(8) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `model_name` varchar(255) NOT NULL COMMENT 'excel名稱', `sheet_name` varchar(255) NOT NULL COMMENT 'sheet頁名稱', `need_merge` varchar(255) NOT NULL COMMENT '是否需要合並,需要合並為yes', `order_no` int(11) DEFAULT NULL COMMENT '合並的時候排序,決定哪個sheet頁需要排在最前面', `last_sheet_interval` int(8) DEFAULT NULL COMMENT '當前合並的sheet,與上一個sheet頁之間的間隔', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
子表:源excel中sheet需要內部合並的sheet信息

CREATE TABLE `child_sheet_message` ( `id` bigint(8) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `parent_sheet_name` varchar(255) NOT NULL COMMENT '父sheet頁名稱,即需要將子sheet頁合並到的位置', `sheet_name` varchar(255) NOT NULL COMMENT '子sheet頁名稱', `order_no` int(8) NOT NULL COMMENT '子sheet頁合並到父sheet頁的順序', `title_length` int(8) DEFAULT NULL COMMENT '父子合並的表頭處理,如果合並不需要去掉表頭,填0,需要去掉填寫sheet表頭長度', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
2.2 代碼service層
2.2.1 主方法mergeExcelBySheetMessage, 中間用到的mapper層省略, 用到的sql如下
2.2.1.1 取出所有需要合並sheet頁 select * from sheet_message s where s.model_name = #{fileName} and s.need_merge = 'yes' order by s.order_no
/** * 傳遞 源workbook, 和 源文件名fileName * * @param sourceWorkBook 源workbook 用來獲取excel數據 * @param fileName 源文件名稱,用來獲取數據庫中的sheet頁信息 * @return * @throws IllegalAccessException * @throws InstantiationException */ @Override public Workbook mergeExcelBySheetMessage(Workbook sourceWorkBook, String fileName) throws IllegalAccessException, InstantiationException { // 創建和目標workbook相同類型的workbook Workbook targetWorkBook = sourceWorkBook.getClass().newInstance(); // 創建目標sheet Sheet targetSheet = targetWorkBook.createSheet(); // 取出所有的需要合並的sheet頁合集 List<SheetMessage> needMergeSheetList = sheetMessageMapper.selectByFileName(fileName); // 當不需要合並的時候,直接返回源workbook if (needMergeSheetList == null || needMergeSheetList.isEmpty()) { return sourceWorkBook; } // 記錄已經合並過的sheet頁,防止重復合並 List<String> sheetNameList = new ArrayList<>(); // 將合並完之后的sourceWorkbook合並到targetWorkbook中 for (SheetMessage sheetMessage : needMergeSheetList) { String sheetName = sheetMessage.getSheetName(); // 與上一個合並的sheet之間的間隔 Integer lastSheetInterval = sheetMessage.getLastSheetInterval(); if (lastSheetInterval > 0) { targetSheet.createRow(targetSheet.getLastRowNum() + lastSheetInterval); } // 源sheet合並需要合並的子項 mergeChildSheetMessage(sheetName, sourceWorkBook, sheetNameList); Sheet sourceSheet = sourceWorkBook.getSheet(sheetName); moveSourceSheetIntoTargetSheet(targetWorkBook, sourceSheet, targetSheet, 0); } return targetWorkBook; }
2.2.2 源sheet需要自身合並的子項(源excel中有的sheet頁是需要同過自身的其他sheet頁合並而來)
中間用到的mapper層,省略,用到的sql語句如下
2.2.2.1 select * from child_sheet_message c where c.parent_sheet_name = #{parentSheetName} order by c.order_no
/** * 源excel中sheet頁合並需要合並的子sheet * * @param parentSheetName 需要合並的sheet頁名稱 * @param sourceWorkBook 源excel * @param sheetNameList 已經合並過的sheet名稱的list集合,用來防止重復合並 */ private void mergeChildSheetMessage(String parentSheetName, Workbook sourceWorkBook, List<String> sheetNameList) { if (sheetNameList.contains(parentSheetName)) { return; } List<ChildSheetMessage> childSheetList = childSheetMessageMapper.selectByParentSheetMessage(parentSheetName); if (childSheetList == null || childSheetList.isEmpty()) { sheetNameList.add(parentSheetName); return; } // 合並 Sheet targetSheet = sourceWorkBook.getSheet(parentSheetName); for (ChildSheetMessage childSheetMessage : childSheetList) { String sheetName = childSheetMessage.getSheetName(); Integer titleLength = childSheetMessage.getTitleLength(); // 遞歸合並子項的子項sheet mergeChildSheetMessage(sheetName, sourceWorkBook, sheetNameList); Sheet sourceSheet = sourceWorkBook.getSheet(sheetName); // 合並sheet頁 moveSourceSheetIntoTargetSheet(sourceWorkBook, sourceSheet, targetSheet, titleLength); } // 記錄已經合並了的sheet頁名稱 sheetNameList.add(parentSheetName); }
2.2.3 moveSourceSheetIntoTargetSheet 合並源sheet到目標sheet中
/** * sheet頁合並 * * @param targetWorkBook 目標workbook,該對象主要用來創建單元格格式 * @param sourceSheet 源sheet * @param targetSheet 目標sheet * @param removeTitleLength 源sheet頁合並到目標sheet中需要去掉的表頭長度,如果不去掉傳遞0 */ private static void moveSourceSheetIntoTargetSheet(Workbook targetWorkBook, Sheet sourceSheet, Sheet targetSheet, int removeTitleLength) { if (sourceSheet == null) { return; } // 獲取目標sheet最后一行的下一行 int targetRowNums = targetSheet.getLastRowNum(); int physicalNumberOfRows = targetSheet.getPhysicalNumberOfRows(); targetRowNums = physicalNumberOfRows == 0 ? 0 : targetRowNums + 1; // 移動 源sheet頁中的 合並單元格區域 到目標sheet頁中 moveSourceSheetAllMergedRegionToTargetSheet(sourceSheet, targetSheet, targetRowNums, removeTitleLength); int sourceRowNums = sourceSheet.getLastRowNum(); for (int i = removeTitleLength; i <= sourceRowNums; i++) { Row targetRow = targetSheet.createRow(targetRowNums++); Row sourceRow = sourceSheet.getRow(i); // 復制行 copySourceRowToTargetRow(targetWorkBook, sourceRow, targetRow); } }
2.2.4 合並sheet的時候,先處理sheet頁中的合並單元格區域問題
/** * 合並sheet頁中,處理源sheet頁中可能存在的 合並單元格部分; * 當源sheet頁在合並單元格的時候可能去掉表頭,所以也需去掉表頭的合並單元格部分 * * @param sourceSheet 源sheet * @param targetSheet 目標sheet * @param targetRowNums 目標sheet的最后一行(源合並單元格的位置,需要變化到目標單元格區域,需要提供一個位置角標) * @param removeTitleLength 需要移除的表頭長度 */ private static void moveSourceSheetAllMergedRegionToTargetSheet(Sheet sourceSheet, Sheet targetSheet, int targetRowNums, int removeTitleLength) { int numMergedRegions = sourceSheet.getNumMergedRegions(); for (int i = 0; i < numMergedRegions; i++) { CellRangeAddress mergedRegion = sourceSheet.getMergedRegion(i); int firstRow = mergedRegion.getFirstRow(); // 去掉表頭的 單元格合並 if (firstRow < removeTitleLength) { continue; } int lastRow = mergedRegion.getLastRow(); int firstColumn = mergedRegion.getFirstColumn(); int lastColumn = mergedRegion.getLastColumn(); // 合並單元格的行需要跟隨當前單元格的行數下移 firstRow = firstRow + targetRowNums; lastRow = lastRow + targetRowNums; CellRangeAddress cellRangeAddress = new CellRangeAddress(firstRow, lastRow, firstColumn, lastColumn); targetSheet.addMergedRegion(cellRangeAddress); } }
2.2.5 移動行copySourceRowToTargetRow
/** * 將源行復制到目標行 * @param targetWorkBook 目標workbook,主要用來創建單元格樣式 * @param sourceRow 源行 * @param targetRow 目標行 */ private static void copySourceRowToTargetRow(Workbook targetWorkBook, Row sourceRow, Row targetRow) { if (sourceRow == null) { return; } // 行高 targetRow.setHeight(sourceRow.getHeight()); int sourceCellNums = sourceRow.getLastCellNum(); for (int i = 0; i < sourceCellNums; i++) { Cell targetCell = targetRow.createCell(i); Cell sourceCell = sourceRow.getCell(i); // 復制單元格 copySourceCellToTargetCell(targetWorkBook, targetCell, sourceCell); } }
2.2.5 移動單元格
/** * 移動單元格 * * @param targetWorkBook 目標workbook,用來在本方法中創建單元格樣式 * @param targetCell 目標單元格 * @param sourceCell 源單元格 */ private static void copySourceCellToTargetCell(Workbook targetWorkBook, Cell targetCell, Cell sourceCell) { if (sourceCell == null) { return; } // 將源單元格的格式 賦值到 目標單元格中 CellStyle sourceCellStyle = sourceCell.getCellStyle(); /* 此處由於是新建了workbook對象,只能新建 CellStyle對象,然后clone,再賦值; 直接賦值 源CellStyle對象 會報不是同源異常 */ CellStyle targetCellStyle = targetWorkBook.createCellStyle(); targetCellStyle.cloneStyleFrom(sourceCellStyle); targetCell.setCellStyle(targetCellStyle); CellType cellTypeEnum = sourceCell.getCellTypeEnum(); switch (cellTypeEnum) { case STRING: targetCell.setCellValue(sourceCell.getStringCellValue()); break; case NUMERIC: if (DateUtil.isCellDateFormatted(sourceCell)) { // 日期格式的值 targetCell.setCellValue(sourceCell.getDateCellValue()); } else { targetCell.setCellValue(sourceCell.getNumericCellValue()); } break; case BOOLEAN: targetCell.setCellValue(sourceCell.getBooleanCellValue()); break; case FORMULA: // ***為公式的情況下獲取的是單元格的數值 targetCell.setCellValue(sourceCell.getNumericCellValue()); break; case BLANK: break; case ERROR: targetCell.setCellValue(sourceCell.getErrorCellValue()); break; case _NONE: break; default: } }
如果有意見可以留言,謝謝~