使用Apache POI的shiftRows按列排序Excel遇XmlValueDisconnectedException问题
解决Apache POI 3.17按第一列排序XSSFWorksheet的问题
嘿,我懂你遇到的糟心事了——shiftRows在POI 3.17版本里确实容易踩坑,行移动时经常因为行引用、样式关联或者隐藏行/合并单元格的问题抛出异常。与其死磕这个不稳定的方法,不如换个更可靠的思路:先把所有数据行提取出来排序,再重新写入工作表,既避免了移动行的各种幺蛾子,排序逻辑也更清晰可控。
下面是具体的实现步骤和代码:
核心思路
- 提取表头之外的所有数据行(包含单元格值和样式信息)
- 按第一列的值对数据行进行排序(适配不同数据类型)
- 清空原工作表的数据行(保留表头)
- 将排序后的行重新写入工作表
完整代码示例
首先需要两个辅助方法,用来处理不同类型单元格的读写:
private static Object getCellValue(XSSFCell cell) { if (cell == null) return ""; switch (cell.getCellTypeEnum()) { case STRING: return cell.getStringCellValue(); case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue(); } else { return cell.getNumericCellValue(); } case BOOLEAN: return cell.getBooleanCellValue(); case FORMULA: try { return cell.getStringCellValue(); } catch (Exception e) { return cell.getNumericCellValue(); } default: return ""; } } private static void setCellValue(XSSFCell cell, Object value) { if (value instanceof String) { cell.setCellValue((String)value); } else if (value instanceof Double) { cell.setCellValue((Double)value); } else if (value instanceof Date) { cell.setCellValue((Date)value); } else if (value instanceof Boolean) { cell.setCellValue((Boolean)value); } else { cell.setCellValue(value.toString()); } }
接下来是排序的核心逻辑:
// 获取目标工作表 XSSFSheet sheet = workbook.getSheetAt(0); int headerRowNum = 0; // 假设第0行是表头,可根据实际调整 int startDataRow = headerRowNum + 1; int lastRowNum = sheet.getLastRowNum(); // 1. 收集所有数据行的内容和样式 List<Map<String, Object>> rowDataList = new ArrayList<>(); for (int i = startDataRow; i <= lastRowNum; i++) { XSSFRow row = sheet.getRow(i); if (row == null) continue; List<Object> cellValues = new ArrayList<>(); int columnCount = sheet.getRow(headerRowNum).getLastCellNum(); // 遍历所有列,收集单元格值 for (int j = 0; j < columnCount; j++) { XSSFCell cell = row.getCell(j); cellValues.add(getCellValue(cell)); } // 保存行样式(如果有设置的话) rowDataList.add(Map.of( "values", cellValues, "rowStyle", row.getRowStyle() )); } // 2. 按第一列的值排序 rowDataList.sort((rowA, rowB) -> { Object valA = ((List<Object>)rowA.get("values")).get(0); Object valB = ((List<Object>)rowB.get("values")).get(0); // 根据第一列的实际数据类型调整排序逻辑 if (valA instanceof String && valB instanceof String) { return ((String)valA).compareToIgnoreCase((String)valB); } else if (valA instanceof Double && valB instanceof Double) { return ((Double)valA).compareTo((Double)valB); } else if (valA instanceof Date && valB instanceof Date) { return ((Date)valA).compareTo((Date)valB); } else { // 其他类型统一转字符串进行比较 return valA.toString().compareToIgnoreCase(valB.toString()); } }); // 3. 清空原数据行(从后往前删避免索引混乱) for (int i = lastRowNum; i >= startDataRow; i--) { XSSFRow row = sheet.getRow(i); if (row != null) { sheet.removeRow(row); } } // 4. 写入排序后的行 int currentRow = startDataRow; for (Map<String, Object> rowData : rowDataList) { XSSFRow newRow = sheet.createRow(currentRow++); // 复制原行的样式 newRow.setRowStyle((XSSFCellStyle)rowData.get("rowStyle")); List<Object> values = (List<Object>)rowData.get("values"); int columnCount = values.size(); for (int j = 0; j < columnCount; j++) { XSSFCell newCell = newRow.createCell(j); setCellValue(newCell, values.get(j)); // 如果需要保留单元格单独样式,可以在这里添加复制逻辑 // 比如从原行单元格获取样式,设置到新单元格 } }
额外注意事项
- 如果工作表存在合并单元格,需要先记录所有合并区域的位置和内容,排序完成后再重新创建合并区域,否则合并单元格会丢失或错位。
- 若需要保留单元格的单独样式(而非行样式),可以在收集数据时一并保存每个单元格的样式,写入时复制到新单元格中。
- 排序逻辑可以根据需求灵活调整,比如改成倒序、多列组合排序,只需要修改
sort方法里的比较器即可。
这种方法比直接调用shiftRows稳定得多,在POI 3.17版本中可以完全正常运行,而且扩展性更强,后续调整排序规则也很方便。
内容的提问来源于stack exchange,提问作者Leo
相关产品推荐
相关产品推荐

