
前端后端实现文件上传 依旧打个比方你前端打包文件FormData→ 填写快递单请求头→ 寄出POST请求→ 快递员后端→ 商家后端收到.上次讲了下载的实现是二进制写出。那么反过来就是读取请求体二进制写入。书接上回前端:vue前端上传文件必须设置请求头// 前端代码constformDatanewFormData()formData.append(file,file)fetch(/api/upload,{method:POST,body:formData,headers:{Content-Type:multipart/form-data// ← 告诉后端我传的是文件}})为什么必须设置Content-Type: multipart/form-dataHTTP 请求本质HTTP 请求 请求行 请求头 请求体 请求头Header告诉服务器我是什么类型 请求体Body实际的数据内容服务器需要知道怎么解析请求体三种常见的 Content-TypeContent-Type请求体格式用途application/json{name:张三}传 JSON 数据application/x-www-form-urlencodedname张三age18传表单数据URL 编码multipart/form-data二进制数据文件传文件后端如何识别PostMapping(/upload)publicAjaxResultupload(RequestParam(file)MultipartFilefile){// Spring 根据请求头 Content-Type 决定如何解析// 如果是 multipart/form-data → 解析成 MultipartFile// 如果是 application/json → 解析成 RequestBody}请求头后端解析方式结果multipart/form-data解析成MultipartFile✅ 成功接收application/json尝试解析成 JSON❌ 报错Current request is not a multipart request请求头完整示例POST /api/upload HTTP/1.1 Host: localhost:8080 Content-Type: multipart/form-data; boundary----WebKitFormBoundaryxxx Content-Length: 12345 ------WebKitFormBoundaryxxx Content-Disposition: form-data; namefile; filenametest.xlsx Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet [文件二进制数据] ------WebKitFormBoundaryxxx--关键部分参数作用multipart/form-data告诉后端这是文件上传boundaryxxx分隔符用于分割多个文件/字段如果没设置会怎样不设置或设错// ❌ 错误用 application/json 上传文件fetch(/api/upload,{method:POST,body:JSON.stringify({file:fileData}),headers:{Content-Type:application/json}})后端报错org.springframework.web.multipart.MultipartException: Current request is not a multipart request因为后端期望multipart收到的却是json格式不匹配浏览器自动设置// ✅ 用 FormData 时浏览器会自动设置 Content-TypeconstformDatanewFormData()formData.append(file,file)fetch(/api/upload,{method:POST,body:formData// 不需要手动设置 headers浏览器会自动加上// Content-Type: multipart/form-data; boundaryxxx})// 浏览器自动发送的请求头 Content-Type: multipart/form-data; boundary----WebKitFormBoundary7MA4YWxkTrZu0gW所以一般情况下不需要手动设置Content-Type但是 axios 可能会覆盖// axios 拦截器统一设置了 Content-Typeaxios.interceptors.request.use(config{config.headers[Content-Type]application/json;charsetutf-8// ❌ 会覆盖returnconfig})// 上传文件时被覆盖了后端就识别不了解决方案直接判断类型不是就用默认设置的请求头。if(!(config.datainstanceofFormData)){config.headers[Content-Type]application/json;charsetutf-8}总结问题答案为什么要设置告诉后端这是文件后端才知道怎么解析设成什么multipart/form-data不设会怎样后端报错Current request is not a multipart request谁负责设置浏览器自动设置但 axios 可能会覆盖解决方案上传文件时不要手动设置或判断 FormData 时删除一句话Content-Type: multipart/form-data告诉后端我传的是文件请用 MultipartFile 接收后端Java下载后端 OutputStream → 前端读 Blob 上传前端 FormData → 后端读 MultipartFile(本质是Blob)本质上还是读取请求体里面的二进制文件。MultipartFile本质上还是实现了InputStreamSource可以看源码。publicinterfaceMultipartFileextendsInputStreamSource{StringgetName();NullableStringgetOriginalFilename();// 原始文件名NullableStringgetContentType();// 文件类型booleanisEmpty();// 是否为空longgetSize();// 文件大小byte[]getBytes()throwsIOException;InputStreamgetInputStream()throwsIOException;// 获取输入流defaultResourcegetResource(){returnnewMultipartFileResource(this);}voidtransferTo(Filedest)throwsIOException,IllegalStateException;// 保存到磁盘defaultvoidtransferTo(Pathdest)throwsIOException,IllegalStateException{FileCopyUtils.copy(this.getInputStream(),Files.newOutputStream(dest));}}那我们就要搞懂明白MultipartFile是二进制文件。知道他的方法即可。本质上就是InputStream的封装代表上传文件的二进制数据流。HTTP 请求体二进制 ↓ ServletRequest.getInputStream() ↓ Spring 解析 multipart/form-data ↓ MultipartFile 对象封装了 InputStream ↓ 你的业务代码常用方法对照方法作用本质getBytes()获取文件字节数组把流读成 byte[]getInputStream()获取输入流直接读流transferTo(File)保存到磁盘InputStream → FileOutputStreamgetOriginalFilename()原始文件名从请求头解析getSize()文件大小流的长度isEmpty()是否为空没文件就是空知道这个就好办了我们只需要处理MultipartFile这个类 第一步读取请求体的参数用MultipartFile接收。getOriginalFilename()直接读取文件名然后用EasyExcel去读取上期传的excel。然后就能快速处理表格这就简单多了读取固定的行、列、值。构建好对象这样就能操作excel处理业务逻辑。 上期业务收集好学生信息放到excel模板上传给系统自动添加账号密码这个模板里面有很多条数据。后端读前端上传的Excel。封装成User对象添加到数据库即可。file.getInputStream()获取输入流本质直接读流 我们从输入流中读取到这个对象转存为UserImportDto对象。然后用EasyExcel读取转换成我们要的类即可/** * 批量导入用户 */PostMapping(/import)publicAjaxResultimportUsers(RequestParam(file)MultipartFilefile){// 1. 文件非空校验if(file.isEmpty()){returnAjaxResult.error(请选择要导入的文件);}// 2. 文件格式校验StringfileNamefile.getOriginalFilename();if(fileNamenull||!(fileName.endsWith(.xlsx)||fileName.endsWith(.xls))){returnAjaxResult.error(请上传 .xlsx 或 .xls 格式的文件);}try{ListUserImportDtoimportListnewArrayList();EasyExcel.read(file.getInputStream(),UserImportDto.class,newPageReadListenerUserImportDto(dataList-{importList.addAll(dataList);})).sheet().doRead();// 调用 Service 处理直接返回结果字符串StringresultMsguserService.batchImport(importList);// 判断是否有成功记录if(resultMsg.contains(成功0条)){returnAjaxResult.error(resultMsg);}else{returnAjaxResult.success(resultMsg);}}catch(IOExceptione){returnAjaxResult.error(导入失败e.getMessage());}}EasyExcel.read、PageReadListener接口重点我们看一下四种不同的写法EasyExcel.read注意PageReadListener是个接口// 写法1Lambda简洁EasyExcel.read(file.getInputStream(),UserImportDto.class,newPageReadListener(dataList-{importList.addAll(dataList);})).sheet().doRead();// 写法2拆开写易理解PageReadListenerUserImportDtolistenernewPageReadListener(dataList-{importList.addAll(dataList);});EasyExcel.read(file.getInputStream(),UserImportDto.class,listener).sheet().doRead();// 写法3匿名内部类不用 Lambda接口不能创建接口接口创建匿名内部实现类PageReadListenerUserImportDtolistenernewPageReadListenerUserImportDto(){Overridepublicvoidinvoke(ListUserImportDtodataList,AnalysisContextcontext){importList.addAll(dataList);}};EasyExcel.read(file.getInputStream(),UserImportDto.class,listener).sheet().doRead();// 写法4接口创建实现类// 先写一个实现类publicclassMyPageReadListenerimplementsPageReadListenerUserImportDto{privateListUserImportDtoimportList;publicMyPageReadListener(ListUserImportDtoimportList){this.importListimportList;}Overridepublicvoidinvoke(ListUserImportDtodataList,AnalysisContextcontext){importList.addAll(dataList);}}// 使用时MyPageReadListenerlistenernewMyPageReadListener(importList);EasyExcel.read(file.getInputStream(),UserImportDto.class,listener).sheet().doRead();读取页EasyExcel.read(file.getInputStream(),UserImportDto.class,listener).sheet()// ① 选择要读哪个Sheet.doRead();// ② 开始真正读取// 什么都不写默认读取第一个 Sheet.sheet()// 读取第 0 个 Sheet也是第一个.sheet(0)// 读取第 2 个 Sheet从0开始.sheet(2)// 读取指定名称的 Sheet.sheet(学生信息)// 读取第0页从第2行开始.sheet(0).headRowNumber(2)// 执行真正开始读取reader.doRead();// ← 只有执行这行才会读取数据读取完成后ListUserImportDtoimportListnewArrayList();// 空列表 size 0第1次调用 listener.invoke()→ importList 加了100条 第2次调用 listener.invoke()→ importList 又加了100条 第3次调用 listener.invoke()→ importList 又加了50条最后一页// importList有了UserImportDto类的数据后把这些对象给业务层处理比如存数据库存盘等等都可以。// 调用 Service 处理直接返回结果字符串StringresultMsguserService.batchImport(importList);业务层处理我这里做了处理看插入多少条失败多少条。最后结果返回给前端。OverridepublicStringbatchImport(ListUserImportDtoimportList){ListUserDOusersnewArrayList();StringBuildererrorsnewStringBuilder();intfailCount0;for(inti0;iimportList.size();i){UserImportDtodtoimportList.get(i);introwNumi2;// Excel 行号从2开始因为第1行是表头// 数据校验 // 1. 登录账号if(StringUtils.isBlank(dto.getUserName())){errors.append(第).append(rowNum).append(行登录账号不能为空);failCount;continue;}// 检查账号是否已存在跳过已存在的用户UserDOexistUseruserMapper.selectByUserName(dto.getUserName());if(existUser!null){errors.append(第).append(rowNum).append(行账号 ).append(dto.getUserName()).append( 已存在);failCount;continue;}// 2. 真实姓名if(StringUtils.isBlank(dto.getRealName())){errors.append(第).append(rowNum).append(行真实姓名不能为空);failCount;continue;}// 3. 角色if(StringUtils.isBlank(dto.getRole())){errors.append(第).append(rowNum).append(行角色不能为空);failCount;continue;}IntegerroleCodeconvertRole(dto.getRole());// 4. 班级学生必填教师/管理员可选if(roleCode1StringUtils.isBlank(dto.getClassName())){errors.append(第).append(rowNum).append(行学生必须填写班级);failCount;continue;}// 构建 UserDO UserDOusernewUserDO();user.setUserName(dto.getUserName());user.setRealName(dto.getRealName());user.setRole(roleCode);user.setClassName(dto.getClassName());user.setPassword(123456);// 默认状态启用user.setStatus(1);// 创建人/更新人可以从上下文获取当前登录用户user.setCreateBy(admin_import);users.add(user);}// 批量插入成功的记录intsuccessCount0;if(!users.isEmpty()){successCountuserMapper.insertBatch(users);}// 构建返回结果字符串StringBuilderresultnewStringBuilder();result.append(导入完成成功).append(successCount).append(条);if(failCount0){result.append(失败).append(failCount).append(条);result.append(\n).append(errors.toString());}returnresult.toString();}/** * 角色名称转代码 */privateIntegerconvertRole(StringroleName){switch(roleName.trim()){case学生:return1;case教师:return2;case管理员:return3;default:return1;}}这是个通用的业务逻辑当然现在封装的很完善不需要自己手写输入输出流这些东西都是现成工具本质上还是输入输出流。最后还不知道上传下载怎么实现的赶紧翻我主页业务流程都是完善的。记得收藏后期可能会用这些都封装好了成了通用方法。