Excel 批量导入实战:当 EasyExcel 遇上单元格嵌入附件

发布时间:2026/6/23 20:02:01

Excel 批量导入实战:当 EasyExcel 遇上单元格嵌入附件 Excel 批量导入实战:当 EasyExcel 遇上单元格嵌入附件适用场景:后端需要解析用户上传的 Excel,其中不仅包含常规文本数据,还包含直接嵌入单元格的附件(zip、docx、图片等),要求一次性完成数据入库、附件落盘、关联业务实体。一、场景与痛点在常见的 B 端后台中,运营/产品同学往往通过一份 Excel 模板批量录入业务数据。模板里除了常规字段,还经常出现“需求附件”列——用户把文件直接拖进 Excel 单元格里。这对后端提出了两个核心挑战:MultipartFile 的流只能读一次:EasyExcel 读完之后,如果再用 POI 读,流已经耗尽。Excel 里的附件不是超链接,而是二进制对象:需要精确解析 OLE、OOXML、图片三种嵌入形态,并按行号对应回业务数据。Excel中的附件类型,名称获取问题:excel对实体附件的数据和名称存储在不同的位置,需要根据特定的格式进行解析,且部分格式如:.xlsx和.docx格式的名称并无存储,本文暂时只找到对.zip的名称支持。本文以“设计需求批量导入”模块为实例,给出一条可复用的工程化路径。二、技术栈选型组件用途版本参考EasyExcel快速读取文本数据、类型转换、注解映射2.x / 3.xApache POI (XSSF)解析底层 OOXML 结构,提取单元格嵌入对象5.xHutool字符串、集合、空值工具5.xSpring Boot事务控制、Service 层编排3.x自定义 OSS 工具附件上传、路径管理—为什么不只用 EasyExcel?EasyExcel 专注于高性能的文本/数值读取,对单元格内嵌 OLE/图片对象只提供有限支持。对于“需要把附件捞出来并上传 OSS”的场景,必须下沉到 Apache POI 的XSSFWorkbook+XSSFDrawing层做原生解析。三、整体架构流程用户上传 Excel (.xlsx) │ ▼ ┌───────────────────┐ │ 1. 读取字节数组 │ ← 关键:先复制 bytes,解决流不可重复读 │ byte[] fileBytes │ └───────────────────┘ │ ┌───┴───┐ ▼ ▼ EasyExcel Apache POI (文本数据) (嵌入附件) │ │ ▼ ▼ ListVO ListEmbeddingDesc │ │ └───┬───┘ ▼ ┌───────────────────┐ │ 2. 按行号关联 │ ← 把附件挂到对应 VO 上 │ vo.setAttachments()│ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 3. 逐行校验 │ ← 字典、用户、日期、多选字段、岗位-类型联动 │ validate/convert │ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 4. 分批收集 │ ← 主表PO、用户绑定、Code映射、附件信息 │ 4 个临时集合 │ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 5. 批量写入 │ ← 先插主表,再插关联表,最后上传附件 │ insert + OSS upload│ └───────────────────┘ │ ▼ ┌───────────────────┐ │ 6. 后置业务 │ ← 操作历史、审批流、待办、消息通知 └───────────────────┘四、关键实现拆解4.1 文件字节复制:解决流不可重复读MultipartFile.getInputStream()只能消费一次。EasyExcel 和 POI 各自需要独立流,因此第一步就是把文件读成字节数组。byte[]fileBytes=file.getBytes();// 1. EasyExcel 读取ListDesignDemandImportVoimportVos=EasyExcel.read(newByteArrayInputStream(fileBytes)).head(DesignDemandImportVo.class).sheet("DesignDemand").doReadSync();// 2. POI 解析附件(同样基于 fileBytes)ListExcelAttachmentParser.EmbeddingDescembeddings=ExcelAttachmentParser.resolve(fileBytes);通用经验:凡是要用两套工具解析同一个上传文件,先读 bytes,再各自 new ByteArrayInputStream,不要直接传file.getInputStream()。4.2 双管道解析:文本 + 附件文本管道:EasyExcel@DatapublicclassDesignDemandImportVo{@ExcelProperty("品类")privateStringcategoryType_dictText;@ExcelProperty("希望交付时间")privateStringhopeDeliveryDate;@ExcelProperty("需求附件")privateStringattachmentZip;// 纯占位,不需要填内容@ExcelIgnoreprivateListCellAttachmentattachments=newArrayList();// 真正存附件数据}用@ExcelProperty做列名映射;@ExcelIgnore字段不映射列,由代码手动填充。日期在 Excel 中可能是文本格式,所以先按String接收,再手动解析为LocalDate,避免格式错乱。附件管道:Apache POI 原生解析ExcelAttachmentParser.resolve(byte[] xlsxBytes)是核心工具类。它遍历XSSFDrawing,识别三种嵌入形态:OLE 对象:zip、docx、xlsx 等,通常以\u0001Ole10Native或package条目存储在POIFSFileSystem中。OOXML 原生嵌入:无 OLE2 包装的直接嵌入,直接读PackagePart的流。粘贴图片:XSSFPicture,通过XSSFPictureData获取 PNG/JPEG 字节。publicstaticListEmbeddingDescresolve(byte[]xlsxBytes){try(XSSFWorkbookworkbook=newXSSFWorkbook(newByteArrayInputStream(xlsxBytes))){for(inti=0;iworkbook.getNumberOfSheets();i++){XSSFSheetsheet=workbook.getSheetAt(i);XSSFDrawingdrawing=sheet.getDrawingPatriarch();if(drawing==null)continue;for(XSSFShapeshape:drawing.getShapes()){XSSFClientAnchoranchor=(XSSFClientAnchor)shape.getAnchor();introw=anchor.getRow1();// 获取附件所在行号if(shapeinstanceofXSSFObjectData){// OLE / OOXML 嵌入对象...}elseif(shapeinstanceofXSSFPicture){// 图片...}}}}}OLE 文件名乱码是重灾区。Windows 中文系统默认用 GBK 编码文件名,但 Office 2016+ 可能用 UTF-8。解析器做了多编码尝试 + 启发式乱码检测:优先 GBK → GB18030 → UTF-8 → 兜底 ISO-8859-1通过looksLikeMojibake()检测典型乱码特征(Â、Ã、¿等),反向选择正确编码。下面是ExcelAttachmentParser完整代码importlombok.extern.slf4j.Slf4j;importorg.apache.commons.io.IOUtils;importorg.apache.poi.openxml4j.opc.PackagePart;importorg.apache.poi.poifs.filesystem.*;importorg.apache.poi.xssf.usermodel.*;importjava.io.ByteArrayInputStream;importjava.io.IOException;importjava.io.InputStream;importjava.nio.ByteBuffer;importjava.nio.ByteOrder;importjava.nio.charset.StandardCharsets;importjava.util.*;/** * Excel单元格嵌入附件解析器 */@Slf4jpublicclassExcelAttachmentParser{publicstaticListEmbeddingDescresolve(byte[]xlsxBytes){ListEmbeddingDescresult=newArrayList();try(XSSFWorkbookworkbook=newXSSFWorkbook(newByteArrayInputStream(xlsxBytes))){// 获取所有嵌入式文件for(inti=0;iworkbook.getNumberOfSheets();i++){XSSFSheetsheet=workbook.getSheetAt(i);XSSFDrawingdrawing=sheet.getDrawingPatriarch();if(drawing==null)continue

相关新闻