自定义单元格合并策略:
/*** 自定义单元格合并策略** @create: 2023-11-15 13:41**/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class EasyExcelCustomMergeStrategy implements RowWriteHandler {/*** 总数*/private Integer totalNum;//合并行计数private int count;//要合并的列 从0开始private int[] mergeColumnIndex;//已合并单元数private int mergedTotalCount = 0;public EasyExcelCustomMergeStrategy(int[] mergeColumnIndex) {this.mergeColumnIndex = mergeColumnIndex;}// 每写入一行会执行一次afterRowDispose,存在合并行时进行合并方法mergeSameRow,否则什么也不做@Overridepublic void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {//当前行索引int curRowNum = row.getRowNum();if (mergeColumnIndex != null && mergeColumnIndex.length > 0 && !isHead) {//当前行第一列单元格Cell curA1Cell = row.getCell(0);Object curA1Data = curA1Cell.getCellTypeEnum() == CellType.STRING ? curA1Cell.getStringCellValue() : curA1Cell.getNumericCellValue();//上一行第一列单元格Cell preA1Cell = row.getSheet().getRow(curRowNum - 1).getCell(0);Object preA1Data = preA1Cell.getCellTypeEnum() == CellType.STRING ? preA1Cell.getStringCellValue() : preA1Cell.getNumericCellValue();if (curA1Data.equals(preA1Data)) {count++;} else {if (count > 0) {for (int columnIndex : mergeColumnIndex) {mergeSameRow(writeSheetHolder, curRowNum, count, columnIndex);}//for (int i = 0; i < mergeColumnIndex.length; i++) {////}count = 0;}}// 最后一行存在合并时,需要单独调用合并,否则不会执行mergeSameRowif (curRowNum == totalNum && count > 0) {for (int columnIndex : mergeColumnIndex) {mergeSameRow(writeSheetHolder, curRowNum + 1, count, columnIndex);}}}}/*** 按列合并单元格** @param writeSheetHolder 写出处理* @param curRowIndex 当前行索引,有n行固定行就加n* @param needMergeNum 需要合并的行* @param curColIndex 需要合并的列*/private void mergeSameRow(WriteSheetHolder writeSheetHolder, int curRowIndex, int needMergeNum, int curColIndex) {Sheet sheet = writeSheetHolder.getSheet();try {Field sh = sheet.getClass().getDeclaredField("_sh");sh.setAccessible(true);XSSFSheet shSheet = (XSSFSheet) sh.get(sheet);CTWorksheet worksheet = shSheet.getCTWorksheet();CTMergeCells ctMergeCells = mergedTotalCount > 0 ? worksheet.getMergeCells() : worksheet.addNewMergeCells();CTMergeCell ctMergeCell = ctMergeCells.addNewMergeCell();CellRangeAddress cellAddresses = new CellRangeAddress(curRowIndex - needMergeNum - 1, curRowIndex - 1, curColIndex, curColIndex);ctMergeCell.setRef(cellAddresses.formatAsString());mergedTotalCount++;} catch (Exception e) {log.error("导出出错!", e);throw new BusinessException("导出出错!\n");}}
}
easyExcel提供了两种合并单元格方法:addMergedRegion和addMergedRegionUnsafe,在使用中发现会遇到不同的问题:
如果将需要合并的数据一次性传入afterRowDispose方法进行合并,在合并过程中使用addMergedRegionUnsafe方法合并单元格可能会导致工作簿损坏,而使用addMergedRegion会进行单元格是否重复合并的校验,会直接抛出异常。
使用addMergedRegionUnsafe方法合并单元格导致的问题:
使用addMergedRegion进行的校验:
/*** adds a merged region of cells (hence those cells form one)** @param region (rowfrom/colfrom-rowto/colto) to merge* @param validate whether to validate merged region* @return index of this region* @throws IllegalArgumentException if region contains fewer than 2 cells* @throws IllegalStateException if region intersects with an existing merged region* or multi-cell array formula on this sheet*/
private int addMergedRegion(CellRangeAddress region, boolean validate) {if (region.getNumberOfCells() < 2) {// 区域包含少于 2 个单元格throw new IllegalArgumentException("Merged region " + region.formatAsString() + " must contain 2 or more cells");}region.validate(SpreadsheetVersion.EXCEL97);if (validate) {// throw IllegalStateException if the argument CellRangeAddress intersects with// a multi-cell array formula defined in this sheet// 此工作表上的多单元格数组公式validateArrayFormulas(region);// Throw IllegalStateException if the argument CellRangeAddress intersects with// a merged region already in this sheet// 区域与现有合并区域相交validateMergedRegions(region);}return _sheet.addMergedRegion(region.getFirstRow(),region.getFirstColumn(),region.getLastRow(),region.getLastColumn());
}
导致错误产生的代码:
public class EasyExcelCustomMergeStrategy implements RowWriteHandler {//合并坐标集合private List<CellRangeAddress> cellRangeAddress;//从哪行开始private int beginRow;//合并行数private int mergeRows;public PiFillMergeStrategy(List<CellRangeAddress> cellRangeAddress, int beginRow, int mergeRows) {this.cellRangeAddress = cellRangeAddress;this.beginRow = beginRow;this.mergeRows = mergeRows;}@Overridepublic void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {if (CollectionUtil.isNotEmpty(cellRangeAddress)) {if (row.getRowNum() >= beginRow && row.getRowNum() <= beginRow + mergeRows) {for (CellRangeAddress item : cellRangeAddress) {writeSheetHolder.getSheet().addMergedRegionUnsafe(item);}}}}}
因此合并时循环每一条数据判断是否与上一条数据是否相同,相同时再逐条进行合并,写法使用XSSFSheet写法或使用easyExcel自带的两种合并方法:
private void mergeSameRow(WriteSheetHolder writeSheetHolder, int curRowIndex, int needMergeNum, int curColIndex) {Sheet sheet = writeSheetHolder.getSheet();CellRangeAddress cellAddresses = new CellRangeAddress(curRowIndex - needMergeNum - 1, curRowIndex - 1, curColIndex, curColIndex);sheet.addMergedRegionUnsafe(cellAddresses);// 或使用// sheet.addMergedRegion(cellAddresses);
}
合并策略的使用:
public void download(HttpServletResponse response) {/*** 此处为业务数据****/String subject = "XXX";String fileName = subject.concat(".xlsx");//定义多级表头List<List<String>> headList = new ArrayList<>();//定义数据体List<List<Object>> dataList = new ArrayList<>();this.getHeadAndData(headList, dataList);// 定义合并单元格 (根据业务需求自定义)List<Integer> mergeColumnIndex = new ArrayList<>();int mergeCol = headList.size() - offerVOList.size() - 3;for (int i = 0; i < mergeCol; i++) {mergeColumnIndex.add(i);}for (int j = mergeCol + 3; j < headList.size(); j++) {mergeColumnIndex.add(j);}// 生成自定义策略EasyExcelCustomMergeStrategy customMergeStrategy = new EasyExcelCustomMergeStrategy(mergeColumnIndex.stream().filter(Objects::nonNull).mapToInt(i->i).toArray());customMergeStrategy.setTotalNum(dataList.size());try {response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));EasyExcel.write(response.getOutputStream()).excelType(ExcelTypeEnum.XLSX).head(headList).sheet(subject).registerWriteHandler(customMergeStrategy)//调用策略.doWrite(dataList);} catch (Exception e) {throw new Exception(String.format("生成文件(%s)失败", fileName), e);}
}