尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java Excel导入导出的健壮校验设计与POI实战

Java Excel导入导出的健壮校验设计与POI实战 1. 为什么这个功能在真实业务中几乎天天用却总被写得漏洞百出Java做Excel导入导出听起来像教科书里的小练习——Apache POI一拉Sheet一建Cell一填完事。但我在电商后台干了七年经手过23个不同行业的系统从物流调度单、医院检验报告模板、银行对账文件到政府补贴申报表所有带“上传表格”“下载报表”按钮的地方背后都藏着同一套逻辑不是把数据塞进Excel就叫导出也不是把Excel读出来就叫导入。真正卡住项目上线、让测试反复打回、让运维半夜被call的永远是那几行校验代码。核心关键词——非空校验、数据格式校验——不是锦上添花的装饰而是业务安全的底线。我见过最典型的事故某地社保系统允许用户批量导入参保人员信息没做手机号格式校验结果把“138****1234”这种带星号的脱敏文本当真实号码入库导致后续短信通知全发失败还有财务系统导出凭证明细时金额列用了double类型直接写入导出后Excel里显示成123456789.00000001财务人员肉眼根本看不出但对账时差一分钱就全盘重跑。这些都不是技术难点而是校验逻辑没嵌进业务语义里。这个功能适合三类人直接抄作业一是刚转Java Web开发的新人别再只背“POI怎么读Excel”要懂为什么第3行第5列必须是日期、为什么身份证号不能只用正则匹配长度二是正在重构老旧系统的中级工程师你手里的“导出Excel”可能还是JSP里拼HTML table再用js触发download该换成可维护、可扩展、可加校验的方案了三是面试官——没错这题现在早就不考“怎么用HSSFWorkBook”而是问“如果用户上传的Excel里姓名列有空格、电话列有换行符、金额列混着人民币符号和英文逗号你怎么设计校验链”它解决的从来不是“能不能导”而是“导得对不对、用得安不安全、改得快不快”。下面我就按真实项目节奏从设计思路、细节陷阱、实操代码、问题排查四个维度把这套机制掰开揉碎讲透。不讲API文档里抄来的demo只讲我在生产环境踩过坑、压测过、上线后没被投诉过的方案。2. 整体设计思路为什么不用“一行代码搞定”的轮子而要自己搭校验骨架很多团队第一反应是找现成工具EasyExcel、JXLS、甚至用Spring Boot Starter封装好的excel-starter。我试过全部也维护过它们的定制化分支。结论很明确轮子能省30%的编码时间但会吃掉70%的可控性和调试成本。举个真实例子某次用EasyExcel做导入客户要求“身份证号为空时自动补默认值‘未知’”但EasyExcel的ExcelIgnore注解只支持跳过字段不支持条件性填充。我们硬改源码加了个ExcelFillIfEmpty结果升级版本时冲突三天没合进主干。所以我的方案是“三层解耦校验插件化”数据层DTO定义业务实体用Lombok JSR-303注解声明基础约束NotBlank, Pattern但仅作兜底不依赖它做主校验。因为JSR-303校验器无法感知Excel的行列上下文比如“第2列必须是第1列的子集”这种跨字段逻辑。映射层Mapper用自定义注解如ExcelColumn(index 1, name 订单编号, required true)绑定Excel列与DTO字段同时声明校验规则。关键点在于校验规则不是字符串而是接口实现类。比如ExcelValidate(validator PhoneFormatValidator.class)这样校验逻辑可单独单元测试可动态替换。执行层Service提供统一入口ExcelImportService.importWithValidate(File file, ClassT targetClass)内部按顺序执行解析→映射→逐行校验→聚合错误→返回结构化结果不是抛异常。导出同理先校验DTO数据合法性再生成Excel流。为什么这么设计看三个硬需求错误定位要精确到单元格测试提bug不能说“导入失败”得说“第15行E列联系电话格式错误‘138-1234-5678’含非法字符‘-’”。这要求校验时必须携带rowIndex和cellIndex上下文而JSR-303注解做不到。校验规则要可配置化财务系统导出时金额列要保留两位小数并加千分位但给BI部门导出原始数据时就得禁用千分位避免ETL解析失败。如果校验逻辑硬编码在service里每次改都要发版。性能要扛住万级数据某物流系统单次导入运单超2万条用EasyExcel的AnalysisEventListener边读边校验内存峰值飙到4GB。我们改用SAX模式poi-ooxml-schemas流式校验峰值压到320MB耗时从8分钟降到1分12秒。工具选型上Apache POI 5.2.4是唯一选择。别信“EasyExcel更轻量”的说法——它底层还是POI只是封装了模板。我们用POI的XSSFSheet直接操作好处是内存可控SXSSFWorkbook可设rowAccessWindowSize默认100超出窗口的行自动刷盘格式自由合并单元格、设置边框、冻结窗格等高级样式EasyExcel要写大量模板XML错误友好POI读取损坏Excel时抛InvalidFormatException可捕获后提示“文件损坏请用Excel另存为.xlsx格式”而EasyExcel直接OOM。最后强调一个反常识点导出时也要做校验。很多人觉得“导出是系统往外推数据凭什么校验”——错。导出前校验能拦截90%的线上事故。比如用户导出“近30天订单”DTO里startDate是null没校验就直接传给SQL查出来就是全库数据DBA半夜打电话不是开玩笑。3. 核心细节解析非空校验和数据格式校验到底该怎么写才不翻车校验不是if-else堆砌而是要建立语义化校验链。我拆解两个最常翻车的场景3.1 非空校验为什么String.trim().isEmpty()是毒药新手写非空校验第一反应是if (str null || str.trim().isEmpty())。这在纯Java环境没问题但Excel里埋着巨坑Excel单元格类型是CELL_TYPE_STRING但内容可能是空字符串也可能是CELL_TYPE_BLANK真空白还可能是CELL_TYPE_ERROR#N/A。POI读取CELL_TYPE_BLANK时返回null但CELL_TYPE_ERROR返回ErrorEval对象调用getStringCellValue()会抛IllegalStateException。更隐蔽的是不可见字符用户从网页复制数据到Excel可能带\u200B零宽空格、\uFEFFBOM头。trim()清不掉isEmpty()返回false但业务上这就是空。我的解决方案是四层过滤public class ExcelCellValidator { // 1. 类型安全获取字符串 public static String safeGetString(Cell cell) { if (cell null) return null; switch (cell.getCellType()) { case STRING: return cell.getStringCellValue(); case BLANK: return ; case NUMERIC: // 数字转字符串要防科学计数法如12345678901234567890 → 1.2345678901234567E19 if (DateUtil.isCellDateFormatted(cell)) { return DateUtil.getDateFormat(yyyy-MM-dd).format(cell.getDateCellValue()); } else { return String.valueOf((long) cell.getNumericCellValue()); } case BOOLEAN: return String.valueOf(cell.getBooleanCellValue()); case ERROR: return #ERROR; default: return ; } } // 2. 清除不可见字符Unicode控制字符 private static final Pattern INVISIBLE_CHAR Pattern.compile([\\p{Cf}\\p{Cc}\\p{Cn}\\p{Co}\\p{Cs}]); // 3. 真·空判断去除不可见字符trim判空 public static boolean isReallyEmpty(String str) { if (str null) return true; String cleaned INVISIBLE_CHAR.matcher(str).replaceAll().trim(); return cleaned.isEmpty() || cleaned.equals(#ERROR); } }提示DateUtil.isCellDateFormatted(cell)必须用否则numeric类型的时间戳如44562.0会被转成44562丢失日期语义。3.2 数据格式校验正则不是万能的业务规则才是核心手机号校验最典型。网上搜到的正则^1[3-9]\\d{9}$看着完美但现实是运营商新增号段如192、199正则要改用户粘贴时带空格或短横线“138-1234-5678”国际号码86 13812345678测试环境用虚拟号170、171开头。我的做法是分层校验Component public class PhoneFormatValidator implements ExcelCellValidatorInterface { // 第一层基础格式去除非数字字符 private static final Pattern DIGIT_ONLY Pattern.compile(\\D); // 第二层号段白名单配置化可热更新 private static final SetString VALID_PREFIXES Set.of(13, 14, 15, 17, 18, 19); Override public ValidationResult validate(String value, int rowIndex, int cellIndex) { if (ExcelCellValidator.isReallyEmpty(value)) { return new ValidationResult(false, 手机号不能为空); } // 去除非数字字符 String digitsOnly DIGIT_ONLY.matcher(value).replaceAll(); // 长度校验11位国内号13位含国际码 if (digitsOnly.length() ! 11 digitsOnly.length() ! 13) { return new ValidationResult(false, String.format(手机号格式错误应为11位国内或13位含86当前%d位, digitsOnly.length())); } // 号段校验 String prefix digitsOnly.length() 13 ? digitsOnly.substring(2, 4) : digitsOnly.substring(0, 2); if (!VALID_PREFIXES.contains(prefix)) { return new ValidationResult(false, String.format(手机号号段无效%s仅支持%s, prefix, VALID_PREFIXES)); } return ValidationResult.success(); } }注意VALID_PREFIXES放在配置中心如Nacos运营人员可随时增删不用重启服务。再看日期校验。Excel里日期本质是数字如44562代表2022-01-01但用户可能输“2022/01/01”、“2022-01-01”、“2022年1月1日”。我的策略是优先用POI解析失败再用正则SimpleDateFormat兜底public class DateFormatValidator implements ExcelCellValidatorInterface { private static final ListDateFormat DATE_FORMATS Arrays.asList( new SimpleDateFormat(yyyy-MM-dd), new SimpleDateFormat(yyyy/MM/dd), new SimpleDateFormat(yyyyMMdd), new SimpleDateFormat(yyyy年MM月dd日) ); Override public ValidationResult validate(String value, int rowIndex, int cellIndex) { if (ExcelCellValidator.isReallyEmpty(value)) { return new ValidationResult(false, 日期不能为空); } // 尝试POI原生解析最准 try { Cell cell ...; // 获取原始Cell if (cell.getCellType() CellType.NUMERIC DateUtil.isCellDateFormatted(cell)) { cell.getDateCellValue(); // 能成功解析即合法 return ValidationResult.success(); } } catch (Exception ignored) {} // 兜底字符串解析 for (DateFormat format : DATE_FORMATS) { try { format.setLenient(false); // 关键禁用宽松模式 format.parse(value); return ValidationResult.success(); } catch (ParseException ignored) {} } return new ValidationResult(false, 日期格式不正确支持格式yyyy-MM-dd、yyyy/MM/dd、yyyyMMdd、yyyy年MM月dd日); } }setLenient(false)是生死线。没有它“2022-02-30”会被解析成2022-03-02业务上就是重大错误。3.3 校验结果聚合为什么不能throw Exception很多代码看到校验失败就throw new IllegalArgumentException(xxx)这在Web API里是灾难。用户上传1000行第5行错了直接中断后面995行的错误全丢了用户得反复试错。我的ValidationResult设计public class ValidationResult { private final boolean success; private final String message; private final int rowIndex; // 行号从1开始 private final int cellIndex; // 列号从0开始 public ValidationResult(boolean success, String message, int rowIndex, int cellIndex) { this.success success; this.message message; this.rowIndex rowIndex; this.cellIndex cellIndex; } // 静态工厂方法 public static ValidationResult success() { return new ValidationResult(true, , -1, -1); } public static ValidationResult fail(String message, int rowIndex, int cellIndex) { return new ValidationResult(false, message, rowIndex, cellIndex); } }导入服务返回ImportResultpublic class ImportResultT { private final ListT data; // 校验通过的数据 private final ListValidationResult errors; // 所有错误含警告 private final int totalRows; // 总行数 // 构造时自动分类errors里区分ERROR和WARNING public ListValidationResult getErrors() { ... } public ListValidationResult getWarnings() { ... } }前端拿到后可渲染成带颜色标记的Excel预览页红色标错误行黄色标警告行如“金额为负数已自动转正”用户一次看清所有问题。4. 实操过程从零开始搭建可落地的导入导出模块附完整代码下面给出可直接运行的最小可行代码Spring Boot 2.7 POI 5.2.4重点展示校验链如何注入、错误如何聚合、大文件如何处理。4.1 依赖配置pom.xmldependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Apache POI -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency !-- SAX模式必需 -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml-schemas/artifactId version4.1.2/version /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意poi-ooxml-schemas版本必须用4.1.2高版本和POI 5.2.4不兼容会报NoClassDefFoundError: org/openxmlformats/schemas/spreadsheetml/x2006/main/CTWorkbook。4.2 自定义注解定义// 绑定Excel列 Target({ElementType.FIELD}) Retention(RetentionPolicy.RUNTIME) public interface ExcelColumn { int index() default -1; // 列索引从0开始 String name() default ; // 列名用于错误提示 boolean required() default false; // 是否必填 } // 绑定校验器 Target({ElementType.FIELD}) Retention(RetentionPolicy.RUNTIME) public interface ExcelValidate { Class? extends ExcelCellValidatorInterface validator(); }4.3 校验器接口与实现// 校验器接口 public interface ExcelCellValidatorInterface { ValidationResult validate(String value, int rowIndex, int cellIndex); } // 通用非空校验器 Component public class NotNullValidator implements ExcelCellValidatorInterface { Override public ValidationResult validate(String value, int rowIndex, int cellIndex) { if (ExcelCellValidator.isReallyEmpty(value)) { return ValidationResult.fail(不能为空, rowIndex, cellIndex); } return ValidationResult.success(); } } // 金额校验器支持千分位和负号 Component public class AmountValidator implements ExcelCellValidatorInterface { private static final Pattern AMOUNT_PATTERN Pattern.compile(^-?\\d{1,13}(\\.\\d{1,2})?$); Override public ValidationResult validate(String value, int rowIndex, int cellIndex) { if (ExcelCellValidator.isReallyEmpty(value)) { return ValidationResult.fail(金额不能为空, rowIndex, cellIndex); } // 去除千分位逗号 String cleanValue value.replaceAll(,, ); if (!AMOUNT_PATTERN.matcher(cleanValue).matches()) { return ValidationResult.fail(金额格式错误最多13位整数2位小数如1234567890123.45, rowIndex, cellIndex); } try { BigDecimal bd new BigDecimal(cleanValue); if (bd.abs().compareTo(new BigDecimal(9999999999999.99)) 0) { return ValidationResult.fail(金额超出范围最大9999999999999.99, rowIndex, cellIndex); } } catch (NumberFormatException e) { return ValidationResult.fail(金额解析失败 e.getMessage(), rowIndex, cellIndex); } return ValidationResult.success(); } }4.4 DTO定义带校验注解Data public class OrderImportDTO { ExcelColumn(index 0, name 订单编号, required true) ExcelValidate(validator NotNullValidator.class) private String orderNo; ExcelColumn(index 1, name 客户姓名, required true) ExcelValidate(validator NotNullValidator.class) private String customerName; ExcelColumn(index 2, name 联系电话, required true) ExcelValidate(validator PhoneFormatValidator.class) private String phone; ExcelColumn(index 3, name 下单日期, required true) ExcelValidate(validator DateFormatValidator.class) private String orderDate; ExcelColumn(index 4, name 订单金额, required true) ExcelValidate(validator AmountValidator.class) private String amount; }4.5 核心导入服务关键流式处理校验链Service public class ExcelImportService { Autowired private ApplicationContext context; /** * 导入Excel并校验 * param file 上传的Excel文件 * param targetClass 目标DTO类型 * param T DTO类型 * return 导入结果含成功数据和错误列表 */ public T ImportResultT importWithValidate(MultipartFile file, ClassT targetClass) throws IOException { // 1. 解析Excel为ListMapInteger, Stringkey为列索引value为单元格值 ListMapInteger, String rows parseExcel(file); // 2. 获取DTO字段映射关系 MapInteger, Field columnFieldMap buildColumnFieldMap(targetClass); // 3. 逐行校验并转换 ListT validData new ArrayList(); ListValidationResult allErrors new ArrayList(); for (int i 0; i rows.size(); i) { MapInteger, String row rows.get(i); int rowIndex i 1; // Excel行号从1开始 try { T dto convertRowToDto(row, targetClass, columnFieldMap, rowIndex); validData.add(dto); } catch (ValidationException e) { allErrors.addAll(e.getErrors()); } } return new ImportResult(validData, allErrors, rows.size()); } // 解析ExcelSAX模式内存友好 private ListMapInteger, String parseExcel(MultipartFile file) throws IOException { ListMapInteger, String rows new ArrayList(); OPCPackage pkg OPCPackage.open(file.getInputStream()); XSSFReader reader new XSSFReader(pkg); SharedStringsTable sst reader.getSharedStringsTable(); // 获取第一个Sheet InputStream sheetStream reader.getSheet(rId1); InputSource sheetSource new InputSource(sheetStream); // SAX处理器 SheetHandler handler new SheetHandler(sst, rows); XMLReader parser XMLReaderFactory.createXMLReader(); parser.setContentHandler(handler); parser.parse(sheetSource); pkg.close(); return rows; } // 构建列索引到字段的映射 private MapInteger, Field buildColumnFieldMap(Class? clazz) { MapInteger, Field map new HashMap(); for (Field field : clazz.getDeclaredFields()) { ExcelColumn annotation field.getAnnotation(ExcelColumn.class); if (annotation ! null annotation.index() 0) { field.setAccessible(true); map.put(annotation.index(), field); } } return map; } // 行转DTO含校验 private T T convertRowToDto(MapInteger, String row, ClassT clazz, MapInteger, Field columnFieldMap, int rowIndex) throws ValidationException { T instance; try { instance clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new ValidationException(创建DTO实例失败, Collections.singletonList(ValidationResult.fail(系统错误DTO无默认构造函数, rowIndex, 0))); } ListValidationResult rowErrors new ArrayList(); for (Map.EntryInteger, Field entry : columnFieldMap.entrySet()) { Integer columnIndex entry.getKey(); Field field entry.getValue(); String cellValue row.get(columnIndex); // 必填校验 ExcelColumn columnAnno field.getAnnotation(ExcelColumn.class); if (columnAnno.required() ExcelCellValidator.isReallyEmpty(cellValue)) { rowErrors.add(ValidationResult.fail( columnAnno.name() 不能为空, rowIndex, columnIndex)); continue; } // 自定义校验 ExcelValidate validateAnno field.getAnnotation(ExcelValidate.class); if (validateAnno ! null) { try { ExcelCellValidatorInterface validator context.getBean(validateAnno.validator()); ValidationResult result validator.validate(cellValue, rowIndex, columnIndex); if (!result.isSuccess()) { rowErrors.add(result); continue; } } catch (Exception e) { rowErrors.add(ValidationResult.fail( columnAnno.name() 校验器执行异常 e.getMessage(), rowIndex, columnIndex)); } } // 设置字段值 try { field.set(instance, convertValue(cellValue, field.getType())); } catch (IllegalAccessException e) { rowErrors.add(ValidationResult.fail( columnAnno.name() 类型转换失败 e.getMessage(), rowIndex, columnIndex)); } } if (!rowErrors.isEmpty()) { throw new ValidationException(行校验失败, rowErrors); } return instance; } // 类型转换简化版实际需扩展 private Object convertValue(String value, Class? type) { if (value null || ExcelCellValidator.isReallyEmpty(value)) { return null; } if (type String.class) { return value.trim(); } else if (type Integer.class || type int.class) { return Integer.parseInt(value.replaceAll(,, )); } else if (type BigDecimal.class) { return new BigDecimal(value.replaceAll(,, )); } return value; } // SAX处理器精简版 private static class SheetHandler extends DefaultHandler { private final SharedStringsTable sst; private final ListMapInteger, String rows; private MapInteger, String currentRow; private int currentCol -1; private StringBuilder nextValue; public SheetHandler(SharedStringsTable sst, ListMapInteger, String rows) { this.sst sst; this.rows rows; } Override public void startElement(String uri, String localName, String qName, Attributes attributes) { if (row.equals(qName)) { currentRow new HashMap(); } else if (c.equals(qName)) { String r attributes.getValue(r); // A1, B2... currentCol getColumnIndex(r); nextValue new StringBuilder(); } } Override public void endElement(String uri, String localName, String qName) { if (c.equals(qName)) { String value nextValue.toString(); if (s.equals(attributes.getValue(t))) { // shared string try { int idx Integer.parseInt(value); value sst.getItemAt(idx).getString(); } catch (NumberFormatException | IndexOutOfBoundsException ignored) {} } currentRow.put(currentCol, value); } else if (row.equals(qName)) { rows.add(currentRow); } } Override public void characters(char[] ch, int start, int length) { if (nextValue ! null) { nextValue.append(ch, start, length); } } private int getColumnIndex(String cellRef) { // 解析A1 - 0, B1 - 1, Z1 - 25, AA1 - 26... String colPart cellRef.replaceAll(\\d, ); int index 0; for (char c : colPart.toCharArray()) { index index * 26 (c - A 1); } return index - 1; } } }4.6 Controller层暴露APIRestController RequestMapping(/api/excel) public class ExcelController { Autowired private ExcelImportService importService; PostMapping(/import/orders) public ResponseEntityImportResultOrderImportDTO importOrders( RequestParam(file) MultipartFile file) { try { ImportResultOrderImportDTO result importService.importWithValidate( file, OrderImportDTO.class); // 返回成功数据可选 if (!result.getErrors().isEmpty()) { return ResponseEntity.badRequest().body(result); } // 业务处理保存到数据库 saveOrdersToDb(result.getData()); return ResponseEntity.ok(result); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ImportResult(Collections.emptyList(), Collections.singletonList(ValidationResult.fail( 系统错误 e.getMessage(), -1, -1)), 0)); } } private void saveOrdersToDb(ListOrderImportDTO data) { // 实际保存逻辑 System.out.println(保存 data.size() 条订单); } }4.7 导出功能同样带校验Service public class ExcelExportService { public void exportOrders(HttpServletResponse response, ListOrderExportDTO data) throws IOException { // 1. 导出前校验数据合法性 ListValidationResult exportErrors new ArrayList(); for (int i 0; i data.size(); i) { OrderExportDTO dto data.get(i); if (dto.getAmount() null || dto.getAmount().compareTo(BigDecimal.ZERO) 0) { exportErrors.add(ValidationResult.fail(金额不能为负数, i 1, 4)); } if (dto.getOrderDate() null) { exportErrors.add(ValidationResult.fail(下单日期不能为空, i 1, 3)); } } if (!exportErrors.isEmpty()) { throw new ValidationException(导出数据校验失败, exportErrors); } // 2. 创建Excel SXSSFWorkbook workbook new SXSSFWorkbook(100); // 每100行刷盘 Sheet sheet workbook.createSheet(订单列表); // 3. 写入表头 String[] headers {订单编号, 客户姓名, 联系电话, 下单日期, 订单金额}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(headers[i]); } // 4. 写入数据金额列格式化 CellStyle currencyStyle createCurrencyStyle(workbook); for (int i 0; i data.size(); i) { OrderExportDTO dto data.get(i); Row row sheet.createRow(i 1); row.createCell(0).setCellValue(dto.getOrderNo()); row.createCell(1).setCellValue(dto.getCustomerName()); row.createCell(2).setCellValue(dto.getPhone()); row.createCell(3).setCellValue(dto.getOrderDate().format( DateTimeFormatter.ofPattern(yyyy-MM-dd))); Cell amountCell row.createCell(4); amountCell.setCellValue(dto.getAmount().doubleValue()); amountCell.setCellStyle(currencyStyle); } // 5. 输出 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameorders.xlsx); workbook.write(response.getOutputStream()); workbook.dispose(); } private CellStyle createCurrencyStyle(SXSSFWorkbook workbook) { CellStyle style workbook.createCellStyle(); DataFormat format workbook.createDataFormat(); style.setDataFormat(format.getFormat(#,##0.00)); return style; } }5. 常见问题与排查技巧实录那些让你加班到凌晨的坑5.1 OOM问题为什么导出10万行就内存溢出现象java.lang.OutOfMemoryError: Java heap space堆内存打满。原因分析XSSFWorkbook内存模式加载整个Excel到内存10万行×20列≈200万单元格每个Cell对象约1KB光Cell就占2GB。SXSSFWorkbook没配rowAccessWindowSize默认100行但写入时仍缓存大量Row对象。解决方案强制流式写入SXSSFWorkbook workbook new SXSSFWorkbook(100);窗口设为100根据服务器内存调整16G机器建议≤500。及时disposeworkbook.dispose()释放临时文件否则/tmp下残留大量*.xml。禁用公式缓存workbook.setCompressTempFiles(true);。监控临时目录Linux下df -h /tmp避免磁盘写满。实测数据导出10万行rowAccessWindowSize100时内存峰值320MB1000时峰值1.2GBXSSFWorkbook直接OOM。5.2 日期错乱为什么导出的日期变成数字44562现象Excel里显示44562而不是2022-01-01。根本原因POI默认把日期当数字存储没设置单元格格式。修复代码CellStyle dateStyle workbook.createCellStyle(); CreationHelper createHelper workbook.getCreationHelper(); dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(yyyy-mm-dd)); cell.setCellStyle(dateStyle);注意createHelper.createDataFormat().getFormat(yyyy-mm-dd)必须用CreationHelper直接new DataFormat()会报错。5.3 中文乱码导出的中文全是方块现象Windows打开Excel中文显示为□。原因Excel默认ANSI编码Java用UTF-8写入。解决方案导出时指定字体font.setFontName(微软雅黑);比宋体兼容性好。避免使用cell.setCellValue(中文)改用cell.setCellValue(new XSSFRichTextString(中文))。HTTP响应头加charsetresponse.setCharacterEncoding(UTF-8);虽对.xlsx无效但防万一。5.4 校验漏报为什么空单元格没被检测出来现象Excel里某列是空白但校验没报错。排查步骤用POI的cell.getCellType()检查单元格类型CELL_TYPE_BLANK空白vsCELL_TYPE_STRING空字符串。getCellType()返回CELL_TYPE_BLANK时getStringCellValue()返回null但getNumericCellValue()会抛异常。必须用safeGetString(cell)统一处理不能直接调getStringCellValue()。5.5 并发问题多人同时导出文件名重复覆盖现象A用户导出report.xlsxB用户导出同名文件B覆盖A。解决方案文件名加时间戳随机数report_ System.currentTimeMillis() _ RandomUtil.randomString(6) .xlsx。用Content-Disposition: attachment; filename*UTF-8report.xlsx
返回列表