Excel 批量导入实战:当 EasyExcel 遇上单元格嵌入附件
适用场景:后端需要解析用户上传的 Excel,其中不仅包含常规文本数据,还包含直接嵌入单元格的附件(zip、docx、图片等),要求一次性完成数据入库、附件落盘、关联业务实体。
一、场景与痛点
在常见的 B 端后台中,运营/产品同学往往通过一份 Excel 模板批量录入业务数据。模板里除了常规字段,还经常出现“需求附件”列——用户把文件直接拖进 Excel 单元格里。这对后端提出了两个核心挑战:
- MultipartFile 的流只能读一次:EasyExcel 读完之后,如果再用 POI 读,流已经耗尽。
- Excel 里的附件不是超链接,而是二进制对象:需要精确解析 OLE、OOXML、图片三种嵌入形态,并按行号对应回业务数据。
- Excel中的附件类型,名称获取问题:excel对实体附件的数据和名称存储在不同的位置,需要根据特定的格式进行解析,且部分格式如:.xlsx和.docx格式的名称并无存储,本文暂时只找到对.zip的名称支持。
本文以“设计需求批量导入”模块为实例,给出一条可复用的工程化路径。
二、技术栈选型
| 组件 | 用途 | 版本参考 |
|---|---|---|
| EasyExcel | 快速读取文本数据、类型转换、注解映射 | 2.x / 3.x |
| Apache POI (XSSF) | 解析底层 OOXML 结构,提取单元格嵌入对象 | 5.x |
| Hutool | 字符串、集合、空值工具 | 5.x |
| Spring Boot | 事务控制、Service 层编排 | 3.x |
| 自定义 OSS 工具 | 附件上传、路径管理 | — |
为什么不只用 EasyExcel?
EasyExcel 专注于高性能的文本/数值读取,对单元格内嵌 OLE/图片对象只提供有限支持。对于“需要把附件捞出来并上传 OSS”的场景,必须下沉到 Apache POI 的 XSSFWorkbook + XSSFDrawing 层做原生解析。
三、整体架构流程
用户上传 Excel (.xlsx)
│
▼
┌───────────────────┐
│ 1. 读取字节数组 │ ← 关键:先复制 bytes,解决流不可重复读
│ byte[] fileBytes │
└───────────────────┘
│
┌───┴───┐
▼ ▼
EasyExcel Apache POI
(文本数据) (嵌入附件)
│ │
▼ ▼
List<VO> List<EmbeddingDesc>
│ │
└───┬───┘
▼
┌───────────────────┐
│ 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 读取
List<DesignDemandImportVo> importVos = EasyExcel.read(new ByteArrayInputStream(fileBytes))
.head(DesignDemandImportVo.class)
.sheet("DesignDemand")
.doReadSync();
// 2. POI 解析附件(同样基于 fileBytes)
List<ExcelAttachmentParser.EmbeddingDesc> embeddings = ExcelAttachmentParser.resolve(fileBytes);
通用经验:凡是要用两套工具解析同一个上传文件,先读 bytes,再各自 new ByteArrayInputStream,不要直接传
file.getInputStream()。
4.2 双管道解析:文本 + 附件
文本管道:EasyExcel
@Data
public class DesignDemandImportVo {
@ExcelProperty("品类")
private String categoryType_dictText;
@ExcelProperty("希望交付时间")
private String hopeDeliveryDate;
@ExcelProperty("需求附件")
private String attachmentZip; // 纯占位,不需要填内容
@ExcelIgnore
private List<CellAttachment> attachments = new ArrayList<>(); // 真正存附件数据
}
- 用
@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 字节。
public static List<EmbeddingDesc> resolve(byte[] xlsxBytes) {
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(xlsxBytes))) {
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
XSSFSheet sheet = workbook.getSheetAt(i);
XSSFDrawing drawing = sheet.getDrawingPatriarch();
if (drawing == null) continue;
for (XSSFShape shape : drawing.getShapes()) {
XSSFClientAnchor anchor = (XSSFClientAnchor) shape.getAnchor();
int row = anchor.getRow1(); // 获取附件所在行号
if (shape instanceof XSSFObjectData) {
// OLE / OOXML 嵌入对象
...
} else if (shape instanceof XSSFPicture) {
// 图片
...
}
}
}
}
}
OLE 文件名乱码是重灾区。Windows 中文系统默认用 GBK 编码文件名,但 Office 2016+ 可能用 UTF-8。解析器做了多编码尝试 + 启发式乱码检测:
- 优先 GBK → GB18030 → UTF-8 → 兜底 ISO-8859-1
- 通过
looksLikeMojibake()检测典型乱码特征(Â、Ã、¿等),反向选择正确编码。
下面是ExcelAttachmentParser完整代码
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.poifs.filesystem.*;
import org.apache.poi.xssf.usermodel.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Excel单元格嵌入附件解析器
*/
@Slf4j
public class ExcelAttachmentParser {
public static List<EmbeddingDesc> resolve(byte[] xlsxBytes) {
List<EmbeddingDesc> result = new ArrayList<>();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(xlsxBytes))) {
// 获取所有嵌入式文件
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
XSSFSheet sheet = workbook.getSheetAt(i);
XSSFDrawing drawing = sheet.getDrawingPatriarch();
if (drawing == null) continue
941



被折叠的 条评论
为什么被折叠?



