简介这是一份面向Java全栈开发者的Excel导入功能实战资料结合SpringBoot后端与AntDesignVue前端讲解如何利用upload组件实现.xlsx/.xls文件上传、状态监控与后端解析处理。资源以PDF格式呈现共1个文件压缩包大小198KB内容紧凑适合已有SpringBoot基础、希望快速掌握前后端文件交互的中级开发者。目前已吸引2437人学习浏览说明其在相关技术社区具有较高参考价值。文档不仅展示前端accept、customRequest、change等关键属性用法还给出后端Controller通过MultipartFile接收文件、Service层解析学生信息表的具体示例并封装了导入中按钮状态切换与错误处理逻辑可直接迁移到实际项目中。1. SpringBoot 和 AntDesignVue 的 Excel 导入先搞清楚这份资源能帮你省什么刚接手一个后台管理系统最烦的就是给用户做“导入”功能。SpringBoot 加 AntDesignVue 的项目里Excel 导入看着简单真要落地会遇到一堆细节前端怎么限制文件类型、上传状态怎么切换、后台怎么安全解析、日期怎么变成一串数字……这里把完整实现拆给你看用 Ant Design Vue 的 upload 组件配合 SpringBoot 的 POI 解析实现一个学生信息的导入功能。这套代码直接抄进项目里能跑通适合刚接触这两个框架的开发者也适合想找踩坑清单的老手。2. 前端实现用 upload 组件接管文件上传而不是默认 action2.1 为什么不用默认 action要写 customRequesta-upload 的默认行为是只要给了 action 地址选择文件后它会自动发一个 multipart/form-data 请求。这个默认路径看起来省事但实际项目里基本没法用。原因有三第一后端接口通常不是简单接收文件就完事还要顺带传业务参数第二项目里的请求都封装了一套 axios 实例会自动带 token、统一处理错误码直接用组件默认的 XMLHttpRequest 会绕过这些逻辑第三我们希望在上传前做格式校验比如文件大小超过 10MB 就提前拦截而不是等请求发出去了再报错。所以用 customRequest 完全接管上传动作是更可控的做法。这里还要提一下 accept 属性。accept 的值是逗号分隔的 MIME 类型它只是让文件选择器默认只显示这些类型的文件用户依然可以切换到“所有文件”强行选择别的类型。所以前端限制只是体验优化真正的校验必须放在后端。常见写法是把 xlsx 对应的 MIME 类型 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 和 xls 对应的 application/vnd.ms-excel 都写上让两种格式都能被选到。2.2 模板代码与属性定义先看模板片段。a-upload 的 disabled 和按钮的 disabled 绑的是同一个 uploadDisabled这样在导入过程中整个入口都被锁住不会出现按钮点了没反应但组件还能继续选文件的错位感。a-upload acceptapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel :customRequestcustomRequest changecustomChange :disableduploadDisabled a-button typeprimary :iconuploadIcon :disableduploadDisabled导入/a-button /a-upload这段模板里有几个关键属性accept 只负责弹窗筛选文件类型customRequest 是自定义上传函数组件不会发默认请求change 在文件状态变化时触发包括选中、上传中、成功、失败、移除。需要特别注意的是change 事件返回的 data 参数里有一个 file 对象它的 status 字段用来区分当前所处阶段。对应 data 定义export default { data() { return { // 导入 Excel 时的 icon 图标 uploadIcon: to-top, // 导入过程中是否禁用上传按钮 uploadDisabled: false } } }这两个属性一个控制图标to-top 表示上传/导出loading 表示正在处理一个控制按钮禁用。不要小看这个状态切换它是防止重复提交的关键。如果用户导入的 Excel 有几万行后台解析可能要几秒甚至十几秒这期间如果没有禁用反馈用户会一直点按钮最后落库出现好几份重复数据。以下是这几个属性的速查表方便对照属性作用备注accept过滤器选择器里可选的 MIME 类型非硬性校验customRequest覆盖默认上传行为自定义请求逻辑change文件状态变化时触发处理 uploading / done / error / removed:disabled禁用整个上传组件入参和按钮同步绑定:icon按钮图标使用 Ant Design 图标库2.3 customRequest 与 change 事件的分工customRequest 接收一个 data 参数里面包含 file、onProgress、onSuccess、onError 等回调。我们这里主要用 file 和 onProgress。核心逻辑是构造 FormData然后调用项目封装的 axios 请求方法。/** * 自定义导入文件方法 * param data 上传的 Excel 文件信息 */ customRequest(data) { const file data.file; // 需要上传的 Excel 文件 const formData new FormData(); formData.append(file, file); data.onProgress(); // 标记上传状态为进行中 batchImportGetRecord(formData) .then(res { if (res) { this.$message.success(导入成功); } else { this.$message.error(导入失败); } }) .finally(() { this.switchIconAndStatus(false); }); }代码里 formData.append(file, file) 的前端字段名必须和后端 RequestParam(file) 里的名称完全一致否则后端一直接收不到文件。batchImportGetRecord 是项目里基于 axios 封装好的方法你也可以用 ajax但重点是成功和失败的提示要在 then 里按需弹出。finally 块不管请求成功还是失败都会执行用来把按钮恢复成可点击状态。这里有一个很多人忽略的细节data.onProgress() 需要在请求发出前调用否则 upload 组件内部状态可能一直停留在 pending导致后续 change 事件不触发。另外建议在 then 里再显式调用 data.onSuccess()让组件彻底认为上传流程已结束否则按钮虽然被你恢复了组件内部还是处于中间态。再看 change 事件/** * 导入功能的 change 事件 * param data 上传文件过程中的文件状态信息 */ customChange(data) { if (data.file.status uploading) { this.switchIconAndStatus(true); } else if (data.file.status done) { this.switchIconAndStatus(false); } else if (data.file.status error) { this.switchIconAndStatus(false); } }这个事件的重点是监听 file.status 的变化。Ant Design Vue 的 upload 组件会暴露这些状态uploading 表示上传中done 表示上传成功error 表示上传失败removed 表示用户主动移除文件。由于我们在 customRequest 里完全接管了请求如果是调用后端接口返回 false组件层面得到的基建状态其实是 done而不是 error。所以真正判断导入成功还是失败要看请求返回的布尔值而不是 file.status。这里的 switchIconAndStatus 只负责按钮画面切换。/** * 导入按钮的图标和状态切换 * param flag 是否正在导入 */ switchIconAndStatus(flag) { if (flag) { this.uploadIcon loading; this.uploadDisabled true; } else { this.uploadIcon to-top; this.uploadDisabled false; } }这个方法逻辑很直白但设计意图是当后台解析大批量 Excel 时耗时可能几秒甚至几十秒如果没有禁用按钮用户会下意识地连续点击轻则重复导入重则把数据库写进成倍的脏数据。loading 图标加禁用按钮本质上是给用户一个“正在处理请稍候”的视觉反馈。这里提醒一句如果后端处理时间真的特别长前端这个 loading 状态撑不住用户会怀疑页面卡死所以更高级的做法会在后面第 6 章提到。3. 后端接收与解析从 MultipartFile 到 List3.1 Controller 层RequestParam(file) 的名称必须一致后端接口接收上传文件标准做法是用 MultipartFile 类型。这个接口默认只接受 POST 请求不能是 GET。Controller 代码很简单但正是因为简单反而容易在参数名上翻车。PostMapping(value import) public Boolean importExcelData(RequestParam(file) MultipartFile file) { return studentService.importExcelData(file); }这里最关键的就是 RequestParam(file) 里的名称。前端 FormData 里 append 的是 file这里就必须是 file。如果前端写的是 excelFile而后端写的是 fileSpring 要么抛出 MissingServletRequestPartException要么 file 直接为 null。很多第一次做上传联调的新手都死在这两个字上。有同学会问改成 RequestParam(value file) 是不是更保险其实默认不写 value 也会用参数名去匹配但显式写出来更明确也能防止 IDE 自动重命名参数时踩坑。另外要区分这里不能加 RequestBody因为文件是 multipart/form-data 格式加了 RequestBody 后 Spring 会尝试把整个请求体包装成一个对象文件流就取不到了。还有如果项目里配置了全局的拦截器给请求体做 JSON 解析也要特别注意放行这个上传接口。3.2 Service 层XSSFWorkbook 逐行解析的完整逻辑这一节是核心。以下代码以导入学生信息表为例表格的列依次是姓名、年龄、爱好、家庭地址、出生日期。这里用的是 Apache POI 的 XSSFWorkbook只处理 xlsx 格式。如果要兼容 xls需要使用 HSSFWorkbook或者直接用 WorkbookFactory.create 统一创建后者能自动识别 xls 和 xlsx。Override public boolean importExcelData(MultipartFile file) { ListStudent list new ArrayList(); Student student null; try { // 通过文件输入流读取到对应的 workbook 工作簿 XSSFWorkbook workbook new XSSFWorkbook(file.getInputStream()); // 只解析第一张 sheet 工作表 XSSFSheet sheet workbook.getSheetAt(0); // 遍历第一个工作表的所有行 for (int i 0; i sheet.getPhysicalNumberOfRows(); i) { if (i 0) continue; // 跳过标题行 XSSFRow row sheet.getRow(i); if (row null) continue; // 跳过空行 student new Student(); // 遍历一行中的所有列 for (int j 0; j row.getPhysicalNumberOfCells(); j) { XSSFCell cell row.getCell(j); if (cell null) continue; // 跳过空单元格 String cellValue cell.getStringCellValue(); double cellValue2 cell.getNumericCellValue(); switch (j) { case 0: student.setName(cellValue); break; case 1: student.setAge(cellValue2); break; case 2: student.setHobby(cellValue); break; case 3: student.setAddress(cellValue); break; case 4: SimpleDateFormat sdf new SimpleDateFormat(yyyy年M月d日HH:mm:ss); Date date sdf.parse(cellValue); student.setBirthday(date); break; } } list.add(student); } // 用 MyBatis-Plus 的 saveBatch 批量插入 studentService.saveBatch(list); return true; } catch (Exception e) { e.printStackTrace(); System.err.println(导入失败); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return false; } }逐段拆一下。XSSFWorkbook 构造时传的是 file.getInputStream()这一步会读取整个文件内容所以文件大小会直接影响内存占用。getSheetAt(0) 只取第一张工作表如果你的业务要处理多 sheet就要改成循环遍历。getPhysicalNumberOfRows() 返回的是物理存在的行数而不是逻辑上的行数这两者在某些场景下差异很大后面第 5 章会专门讲。这里的遍历里i 0 表示第一行通常是表头直接跳过。row.getPhysicalNumberOfCells() 同理也可能与实际列数不一致有的程序员喜欢用 getLastCellNum()那个返回的是索引用的时候要小心。单元格取值部分这里同时用了 getStringCellValue 和 getNumericCellValue但其实是有隐患的。POI 会把单元格分成多种类型字符串、数字、日期、布尔等如果你对一个数字单元格调用 getStringCellValue会直接抛 IllegalStateException。所以这段代码只是为了演示整个流程真正放在生产环境前必须按第 5 章的方法改造。最后 studentService.saveBatch(list) 是 MyBatis-Plus 的 IService 自带方法它能把整个 list 一次性发到数据库执行比 for 循环里单条 insert 高效得多。3.3 为什么用 saveBatch 而不是循环 insert很多人第一次写导入习惯在 for 循环里每读一行就执行一次 insert。如果一个 Excel 有 5000 行就要发送 5000 次 SQL数据库连接往返的开销会直接把接口拖到不可用。saveBatch 是 MyBatis-Plus 提供的批量插入方法内部会拼装成一条多 VALUES 的 insert或者按配置的批处理大小分批次执行网络往返次数从 5000 次降到几十次。saveBatch 还可以传第二个参数指定批次大小比如studentService.saveBatch(list, 1000);这个参数的意思是每批插入 1000 条。默认值是 1000但如果你的 Excel 行数特别多或者这一批对象里字段特别大可以适当调小比如 500避免单条 SQL 过大。需要注意不是所有数据库都对大批量 insert 友好比如某些版本对单条 SQL 的占位符数量有上限MySQL 一般没问题但在别的数据库上要提前测一下。如果项目没有用 MyBatis-Plus也可以用原生 JDBC 的批量模式或者 MyBatis 的 ExecutorType.BATCH。核心思路是一样的减少数据库往返次数。4. 参数细节MIME 类型、日期单元格、事务回滚与性能边界4.1 accept 的 MIME 类型和文件扩展名校验前端 accept 里我们写了两个 MIME 类型但这俩类型并不是绝对可靠。xlsx 的标准 MIME 是 application/vnd.openxmlformats-officedocument.spreadsheetml.sheetxls 的 MIME 则可能是 application/vnd.ms-excel也可能是 application/octet-stream取决于浏览器和操作系统。换句话说同一个 xlsx 在不同浏览器下拿到的 MIME 可能不一样所以后端必须再做一层校验。常见做法是检查原始文件名后缀String originalFilename file.getOriginalFilename(); if (!originalFilename.endsWith(.xlsx) !originalFilename.endsWith(.xls)) { return false; }更严格的项目会直接尝试用 WorkbookFactory.create 去创建工作簿创建失败就说明文件损坏或者根本不是 Excel。POI 的 WorkbookFactory 能自动识别 xls 和 xlsx比单纯判断扩展名更可靠。但要注意WorkbookFactory.create 需要传入 InputStream这个流用完后要关闭否则会占着文件句柄Windows 上尤其明显。另外Spring Boot 的文件大小限制也要关注。默认单文件上传最大是 1MB超过会直接报错。一般在 application.yml 里配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MBmax-file-size 是单个文件大小max-request-size 是整个请求的大小。如果导入文件可能超过 10MB记得同步调大。前端也要做一层大小校验比如在 customRequest 里判断 file.size超过后提前提示用户不发给后端避免浪费流量。4.2 日期单元格getNumericCellValue 拿到的是 double这是 Excel 导入最经典的坑。Excel 内部把日期存储为一个数字表示从 1900 年 1 月 0 日以来的天数。如果你直接 getNumericCellValue拿到的是 44814 这样的浮点数而不是 2023 年 9 月 1 日。所以要判断这个单元格是不是日期格式。POI 提供了 DateUtil.isCellDateFormatted(Cell cell) 方法。示例if (DateUtil.isCellDateFormatted(cell)) { Date date cell.getDateCellValue(); student.setBirthday(date); } else { // 如果不是日期格式可能就是字符串或者数字 String cellValue cell.getStringCellValue(); SimpleDateFormat sdf new SimpleDateFormat(yyyy年M月d日HH:mm:ss); student.setBirthday(sdf.parse(cellValue)); }注意isCellDateFormatted 的判断依赖单元格的格式样式如果用户在 Excel 里手动把日期列设置为文本格式这个方法会返回 false。所以稳妥的方案是先按单元格类型分支处理再兜底解析字符串。在真正的项目里我一般会直接使用 com.opencsv 的 CSV 导入或者让用户先下载模板严格按模板格式填写从源头减少日期格式的多样性。4.3 事务回滚TransactionAspectSupport 手动回滚的适用场景Service 方法加了 Transactional 以后运行时异常会自动触发回滚。但在这个 demo 里异常被 catch 住了Spring 不会自动回滚所以要在 catch 块里手动调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()把当前事务标记为 rollback-only。这样做的原因是我们希望把成功与否转换成 Boolean 返回给前端而不是让异常直接抛出去。如果方法没有事务或者因为内部自调用导致事务代理失效调 currentTransactionStatus() 会抛出 NoTransactionException。所以有两个前提方法必须是 public且类是 Spring 管理的 Bean方法必须从外部调用。这里还要补一个注解细节Transactional(rollbackFor Exception.class) public boolean importExcelData(MultipartFile file) { ... }默认情况下Spring 只对 RuntimeException 和 Error 回滚如果方法里抛的是 IOException 这类受检异常默认不会回滚。所以一定要显式指定 rollbackFor Exception.class让所有异常都触发回滚。关于 rollback-only 还有一个容易踩的坑如果你在 catch 块里调用 setRollbackOnly但方法后面又正常返回了 true此时事务提交时会被强制回滚最终结果是数据库没有数据但接口返回成功。这是因为 setRollbackOnly 会标记当前事务为只能回滚。所以手动回滚后return false 必须放在同一分支别做出“标记回滚再 return true”的操作。5. 避坑指南Excel 导入最常见的 5 个坑5.1 参数名不一致、onSuccess 未调用与重复提交现象 1前端点击导入后后端接口报 MissingServletRequestPartException或者后台收到的 file 为 null。原因前端 formData.append(file, file) 里的字段名是 file但后端 RequestParam(file) 写成了别的名字或者前端 append 时用了别的名字。解决统一字段名前后端都叫 file并且后端显式写 RequestParam(file)。现象 2文件上传成功后按钮一直转圈无法再次点击。原因customRequest 里的请求结束后没有调用 data.onSuccess()组件内部状态一直停留在 uploading而你自己的 switchIconAndStatus 虽然在 finally 里恢复了但组件的内部状态钩子没有通知到。解决在 then 里调用 data.onSuccess()在 catch 里调用 data.onError()让组件状态真正走到 done 或 error。现象 3用户重复点击导入按钮导致同一条数据被插入两次。原因上传过程中按钮没有禁用或者后端请求发出到响应之间的窗口期太长。解决前端在 customChange 里检测到 uploading 时立即把 uploadDisabled 置为 true在 done/error 恢复。但前端禁用只是第一道防线后端还需要对数据做唯一性校验比如给业务字段加唯一索引否则无论前端怎么防恶意请求都能绕过。5.2 单元格类型误判、日期序列号与空行现象 4cell.getStringCellValue() 直接抛 IllegalStateException提示 Cannot get a STRING value from a NUMERIC cell。原因POI 把所有单元格按类型区分数字单元格必须用 getNumericCellValue字符串单元格才能用 getStringCellValue混用就会报错。解决不要一上来就 getStringCellValue先判断 cell.getCellType()或者直接用 DataFormatter 统一格式化。DataFormatter 的用法如下DataFormatter formatter new DataFormatter(); String value formatter.formatCellValue(cell);这样无论单元格是什么类型拿到的都是字符串。数字会变成 18日期会变成 2023-09-01 这样的字符串但日期格式依赖于 Excel 单元格的显示格式不一定可靠需要结合业务场景决定是否用这种方式。现象 5出生日期解析出来是一串数字 44814。原因Excel 日期本质是数字序列号直接 getNumericCellValue 拿到的是天数而不是日期对象。解决用 DateUtil.isCellDateFormatted 判断后再调用 getDateCellValue或者统一在模板里规定日期列用文本格式。如果用户填的是 2023 年 9 月 1 日这种带年月日的文本那就要用 SimpleDateFormat 解析这里注意格式串要和单元格内容完全一致比如 yyyy年M月d日 和 yyyy-MM-dd 解析不了同一个字符串。现象 6sheet.getPhysicalNumberOfRows() 返回 100但 sheet.getRow(i) 可能为 null或者漏读了最后几行。原因物理行数按行记录统计如果中间有整行被删除但没压缩或者行的样式残留会出现行稀疏。解决遍历前先判断 row null并且用 getLastRowNum() 遍历逻辑行号再判断实际行是否存在。getPhysicalNumberOfCells 也有类似问题最后一列没有值时物理列数和逻辑列数可能不一致遍历时要注意边界条件。6. 进阶把导入做成一个可复用的模板工具6.1 提供模板下载接口导入功能上线后永远有用户不知道该填哪些列。与其一遍遍解释不如直接在页面上放一个“下载模板”按钮。后端用 POI 生成一个带表头和示例数据的空 Excel交给前端下载。注意模板里的日期列要设置成真正的日期格式避免用户复制示例时被解析成数字。生成模板的代码不复杂但表头和后台解析的列顺序必须完全对应否则用户按模板填完解析出来全是错位数据。6.2 导入结果返回与错误定位后端返回 Boolean 最大的问题是导入失败时用户不知道哪行错了。更好的做法是返回一个结果对象包含成功条数、失败条数和错误明细。这样前端可以把错误行号直接展示给用户省去在日志里找原因的过程。结果对象可以设计得简单一些successCount、failCount、message、errorRows其中 errorRows 是 List 每项是“第 x 行原因”。解析时每行单独 try-catch记录错误信息最后统一返回。你会发现当用户拿到“第 15 行日期格式错误”时他的第一反应是去改模板而不是来找你。这个体验差异值不值得多写几十行代码我觉得值得。6.3 长时间导入的进度反馈如果 Excel 有几万行前端那个 loading 图标只能告诉用户“正在等待”无法区分是卡死还是在跑。我一般会先在后端把文件存储下来然后异步解析前端用 taskId 轮询进度。这个方案改动很大但对于真正的生产系统很有价值。具体做法是上传接口先把文件落地返回 taskId解析线程独立跑把进度写入缓存前端每 2 秒轮询一次进度接口拿到百分比后更新进度条。不过对于大多数管理系统来说几千行的导入量用同步请求加 loading 图标就够了。如果非要上异步记住一点异步任务一定要带状态存储否则刷新页面就丢了。从那以后我每次给后台加导入功能都会强制走一遍“模板下载 → 格式校验 → 后台解析 → 失败回执”这条链路把用户能踩的坑都堵在前面。希望帮到你。本文还有配套的精品资源点击获取