如何为SXSSFWorkbook实现对=开头单元格自动应用setQuotePrefixed(true)?
问题描述
我们项目代码库规模较大,需要实现一项安全特性:对内容以=开头的单元格自动应用setQuotePrefixed(true);样式标记——因为这类内容在Excel中打开后,无论修改单元格值还是直接按回车,都会被解析成公式,存在安全风险。
我们已经实现了针对XSSFWorkbook的处理逻辑(生成“干净”实例并自动修复工作簿),但这套逻辑对SXSSFWorkbook无效——因为流式工作簿SXSSFWorkbook在调用commit方法前就已经完成了流式输出,无法事后批量修改单元格样式。
现有XSSFWorkbook处理代码:
public static XSSFWorkbook createCleanXSSFWorkbook(InputStream is) throws IOException { XSSFWorkbook wb=new XSSFWorkbook(is){ @Override protected void commit() throws IOException { applySafeCellStyles(this); super.commit(); } }; cleanXSSFMetaInformation(wb); return wb; } private static void applySafeCellStyles(Workbook wb) { int sheets = wb.getNumberOfSheets(); Map<CellStyle, CellStyle> styleToSafeStyleMap = new HashMap<>(); for (int sheetNumber = 0; sheetNumber < sheets; sheetNumber++) { Sheet sheet = wb.getSheetAt(sheetNumber); for (Row row : sheet) { for (Cell cell : row) { if (cell.getCellType() == CellType.STRING) { String cellValue = cell.getStringCellValue(); if (cellValue.startsWith("=")) { CellStyle thisCellStyle = cell.getCellStyle(); CellStyle safeCellStyle = styleToSafeStyleMap.get(thisCellStyle); if (safeCellStyle == null) { safeCellStyle = wb.createCellStyle(); safeCellStyle.cloneStyleFrom(thisCellStyle); safeCellStyle.setQuotePrefixed(true); styleToSafeStyleMap.put(thisCellStyle, safeCellStyle); } cell.setCellStyle(safeCellStyle); } } } } } }
解决方案
由于SXSSFWorkbook的流式特性,所有行在达到内存阈值后会被刷入磁盘,无法像XSSFWorkbook那样在commit阶段批量修改样式。因此必须在单元格写入阶段就完成样式处理,以下提供两种可行方案:
方案1:小文件场景 - 先处理XSSF再转SXSSF
如果源Excel文件体积不大,可以先复用现有XSSFWorkbook的处理逻辑,再将其包装为SXSSFWorkbook,兼顾代码复用和流式输出:
public static SXSSFWorkbook createCleanSXSSFWorkbook(InputStream is, int rowAccessWindowSize) throws IOException { // 复用现有逻辑处理XSSFWorkbook XSSFWorkbook xssfWb = createCleanXSSFWorkbook(is); // 包装为SXSSFWorkbook,设置内存中行的缓存数量 SXSSFWorkbook sxssfWb = new SXSSFWorkbook(xssfWb, rowAccessWindowSize); return sxssfWb; }
注意:此方案会将整个源文件加载到内存,仅适合小体积Excel。
方案2:大文件场景 - 流式逐行处理
针对大体积Excel,采用逐行读取源文件、逐行写入SXSSFWorkbook的方式,在写入单元格时直接检查并应用安全样式:
public static SXSSFWorkbook createCleanSXSSFWorkbookStreaming(InputStream is, int rowAccessWindowSize) throws IOException { XSSFWorkbook xssfSource = new XSSFWorkbook(is); SXSSFWorkbook sxssfDest = new SXSSFWorkbook(rowAccessWindowSize); // 执行元信息清理(复用原cleanXSSFMetaInformation的逻辑,适配SXSSF) cleanSXSSFMetaInformation(sxssfDest); // 缓存样式,避免重复创建 Map<CellStyle, CellStyle> styleCache = new HashMap<>(); int sheetCount = xssfSource.getNumberOfSheets(); for (int sheetIdx = 0; sheetIdx < sheetCount; sheetIdx++) { XSSFSheet sourceSheet = xssfSource.getSheetAt(sheetIdx); SXSSFSheet destSheet = sxssfDest.createSheet(sourceSheet.getSheetName()); // 复制源工作表的列宽设置 int columnCount = sourceSheet.getRow(0) != null ? sourceSheet.getRow(0).getLastCellNum() : 0; for (int colIdx = 0; colIdx < columnCount; colIdx++) { destSheet.setColumnWidth(colIdx, sourceSheet.getColumnWidth(colIdx)); } // 逐行处理源数据 Iterator<Row> rowIterator = sourceSheet.iterator(); while (rowIterator.hasNext()) { XSSFRow sourceRow = (XSSFRow) rowIterator.next(); SXSSFRow destRow = destSheet.createRow(sourceRow.getRowNum()); // 逐单元格处理 Iterator<Cell> cellIterator = sourceRow.cellIterator(); while (cellIterator.hasNext()) { XSSFCell sourceCell = (XSSFCell) cellIterator.next(); SXSSFCell destCell = destRow.createCell(sourceCell.getColumnIndex(), sourceCell.getCellType()); // 复制单元格值并处理安全样式 switch (sourceCell.getCellType()) { case STRING: String cellValue = sourceCell.getStringCellValue(); destCell.setCellValue(cellValue); // 对=开头的字符串应用安全样式 if (cellValue.startsWith("=")) { CellStyle safeStyle = styleCache.computeIfAbsent(sourceCell.getCellStyle(), style -> { CellStyle newStyle = sxssfDest.createCellStyle(); newStyle.cloneStyleFrom(style); newStyle.setQuotePrefixed(true); return newStyle; }); destCell.setCellStyle(safeStyle); } else { // 复制原样式 CellStyle normalStyle = styleCache.computeIfAbsent(sourceCell.getCellStyle(), style -> { CellStyle newStyle = sxssfDest.createCellStyle(); newStyle.cloneStyleFrom(style); return newStyle; }); destCell.setCellStyle(normalStyle); } break; case NUMERIC: destCell.setCellValue(sourceCell.getNumericCellValue()); destCell.setCellStyle(getCachedStyle(sourceCell.getCellStyle(), sxssfDest, styleCache)); break; case BOOLEAN: destCell.setCellValue(sourceCell.getBooleanCellValue()); destCell.setCellStyle(getCachedStyle(sourceCell.getCellStyle(), sxssfDest, styleCache)); break; // 按需处理其他单元格类型(如公式、日期等) default: destCell.setCellStyle(getCachedStyle(sourceCell.getCellStyle(), sxssfDest, styleCache)); break; } } } } xssfSource.close(); return sxssfDest; } // 辅助方法:缓存并复制单元格样式 private static CellStyle getCachedStyle(CellStyle sourceStyle, SXSSFWorkbook wb, Map<CellStyle, CellStyle> cache) { return cache.computeIfAbsent(sourceStyle, style -> { CellStyle newStyle = wb.createCellStyle(); newStyle.cloneStyleFrom(style); return newStyle; }); } // 适配SXSSF的元信息清理方法 private static void cleanSXSSFMetaInformation(SXSSFWorkbook wb) { // 复用原cleanXSSFMetaInformation的逻辑,例如: wb.setCreator(""); wb.setLastModifiedBy(""); // 其他需要清空的核心属性... }
核心逻辑说明:
- 逐行读取源
XSSFWorkbook的内容,避免一次性加载整个文件到内存 - 使用
styleCache缓存样式,减少重复创建样式带来的内存开销 - 在写入字符串类型单元格时,直接检查值是否以
=开头,即时应用setQuotePrefixed(true)样式 - 复制源文件的列宽、单元格样式等格式信息,保证输出Excel与源文件一致
内容的提问来源于stack exchange,提问作者fiffy
相关产品推荐
相关产品推荐

