基于Apache POI生成Excel的性能优化技术问询
Apache POI生成Excel性能优化方案(10k行耗时5分钟问题修复)
我们基于指定模板生成Excel工作簿,源文件为制表符分隔文本,当前使用Java+Apache POI实现功能,但处理约10k行的源文件时耗时达5分钟,性能未达预期。以下是针对性的优化方案:
现有代码的核心性能瓶颈
- 重复字符串分割:循环中多次调用
MT940Text.split("\\r\\n"),每次都要重新分割整个文本,10k行数据会产生10k次重复分割操作,开销极大。 - 全量加载文件:一次性将整个文件读入字符串,不仅占用内存,还无法边读边处理,延迟了数据处理的启动时间。
- 未启用POI流式处理:使用默认的
XSSFWorkbook处理大数据量时,所有数据都驻留内存,导致内存占用高、写入速度慢。 - 单元格操作冗余:循环中逐行逐单元格创建和赋值,未做批量优化,且字符串分割使用正则表达式(
split("\\t"))累积开销。
具体优化措施
1. 一次性预处理所有行,避免重复分割
将文本分割操作提前执行一次,存储为数组后循环复用,彻底消除重复分割的开销:
// 原代码(低效) int _tableSize = MT940Text.split("\\r\\n").length; for (int j = 0; j < _tableSize; j++) { String _transactionInCtx = MT940Text.split("\\r\\n")[j]; // ... } // 优化后(高效) String[] allLines = MT940Text.split("\\r\\n"); int _tableSize = allLines.length; for (int j = 0; j < _tableSize; j++) { String _transactionInCtx = allLines[j]; // ... }
2. 流式读取文件,减少内存占用
替换全量读取为逐行流式读取,降低内存消耗的同时,可提前启动数据处理:
private static List<String> readFileLines(final String fileName) { List<String> lines = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader(fileName, StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) { lines.add(line); } } catch (IOException e) { log.error(printStackTraceToString("readFileLines", "", e)); return Collections.emptyList(); } return lines; } // 在generateMT940Excel中调用 List<String> allLines = readFileLines(inFileName); int _tableSize = allLines.size(); for (int j = 0; j < _tableSize; j++) { String _transactionInCtx = allLines.get(j); // ... }
3. 切换为SXSSF流式处理模式
SXSSFWorkbook是Apache POI专为大数据量设计的实现,它会将超出内存缓存的行写入临时磁盘文件,大幅降低内存占用并提升写入速度:
// 读取XSSF模板 XSSFWorkbook xssfTemplate = new XSSFWorkbook( ZDMT940FlatToExcel.class.getClassLoader().getResourceAsStream("EBS_Result_Template.xlsx")); // 克隆模板工作表并修改内容 XSSFSheet templateSheet = xssfTemplate.getSheet("##BankName##"); XSSFSheet clonedXssfSheet = xssfTemplate.cloneSheet(xssfTemplate.getSheetIndex(templateSheet), bankName); clonedXssfSheet.getRow(0).getCell(0).setCellValue( templateSheet.getRow(0).getCell(0).toString().replace("##BankName##", bankName).replace("##EBSType##", "MT940")); clonedXssfSheet.getRow(2).getCell(3).setCellValue( templateSheet.getRow(2).getCell(3).toString().replace("##TimeStamp##", _timeNow)); // 转换为SXSSFWorkbook,设置内存缓存行数(此处设为100,可根据内存调整) SXSSFWorkbook wb = new SXSSFWorkbook(xssfTemplate, 100); wb.setCompressTempFiles(true); // 压缩临时文件,减少磁盘IO // 获取流式工作表 SXSSFSheet clonedSheet = wb.getSheet(bankName);
4. 替换正则分割为高效的字符串拆分
用StringTokenizer替代split("\\t"),避免正则表达式的性能开销:
// 原代码 String[] _transactionInCtxSize = _transactionInCtx.split("\\t"); // 优化后 StringTokenizer tokenizer = new StringTokenizer(_transactionInCtx, "\t"); List<String> cellValues = new ArrayList<>(maxColumns); // 提前指定容量 while (tokenizer.hasMoreTokens()) { cellValues.add(tokenizer.nextToken()); } // 补全空列(保证与最大列数一致) while (cellValues.size() < maxColumns) { cellValues.add(" "); }
5. 预计算最大列数,批量创建单元格
提前遍历所有行计算最大列数,确保每行创建足够的单元格,避免样式缺失的同时减少单元格创建的冗余操作:
// 预计算最大列数 int maxColumns = 0; for (String line : allLines) { StringTokenizer tokenizer = new StringTokenizer(line, "\t"); maxColumns = Math.max(maxColumns, tokenizer.countTokens()); } // 批量创建单元格 for (int j = 0; j < allLines.size(); j++) { int _rowNum = baseRowForTransactions + j; Row _rowInCxt = clonedSheet.createRow(_rowNum); StringTokenizer tokenizer = new StringTokenizer(allLines.get(j), "\t"); int colIndex = 0; while (tokenizer.hasMoreTokens()) { Cell _cellInCxt = _rowInCxt.createCell(colIndex); _cellInCxt.setCellStyle(borderStyle); _cellInCxt.setCellValue(nvl(tokenizer.nextToken(), " ")); colIndex++; } // 补充剩余空单元格 while (colIndex < maxColumns) { Cell _cellInCxt = _rowInCxt.createCell(colIndex); _cellInCxt.setCellStyle(borderStyle); _cellInCxt.setCellValue(" "); colIndex++; } }
6. 优化输出流与POI版本
- 使用缓冲输出流减少磁盘IO次数:
fileOut = new BufferedOutputStream(new FileOutputStream(outFileName)); - 升级至最新稳定版Apache POI(如5.2.5+),新版本修复了大量性能问题。
内容的提问来源于stack exchange,提问作者Srii
相关产品推荐
相关产品推荐

