springboot 后端大文件分片上传(断点上传)
目录简介1. 数据库表创建2. 编写文件记录标识接口3. 编写分片是否存在判断接口4. 编写文件分片上传接口5. 编写文件分片合并接口简介什么是分片上传?分片上传就是把一个大的文件分成N个部分然后一部分一部分的进行上传。分片上传有什么好处分片上传可以实现断网、关浏览器、传输错误等情况发生后再次进行此文件的上传的时候已上传的部分无需继续上传减少上传文件的流量消耗与用户的等待时间。后端如何实现分片上传后端传统上传直接开发一个接口接受文件即可但是分片上传如何实现呢按照分片的解读理解咱们可以将上传接口进行拆分①. 首先咱们需要一个接口来记录标识一个文件这样才能区分不同文件②. 然后咱们还需要一个接口用来判断某个分片是否已经上传完成③. 再然后还需要一个接口用来进行分片文件的上传其实就是一个简单的上传功能将文件分片存放并且记录到文件分片上传记录中④. 最后还需要一个最重要的接口那当然就是分片的合并接口了调用此接口按照顺序将某个文件的所有分片合并成真实的对应的文件。那么废话不多说下面咱们就开始编码有问题可留言评论断点上传不是断点续传断点续传是特指下载这又是另外的知识了注ResponseVo为自定义统一返回格式自行定义接口中用到jackjson包;接口中用到CommonException为自定义异常自行定义或其他方式处理接口中用到FileExistsResponse为判断文件分片是否存在的返回数据结构自行定义接口中用到的filemapper等数据库层的内容自行实现数据库涉及到一张表file_uploadFileUpload为文件上传表file_upload的实体1. 数据库表创建create table file_upload ( id NUMBER not null, index NUMBER, parent NUMBER, name VARCHAR2(100 char) not null, path VARCHAR2(255 char), md5 VARCHAR2(200 char) not null, size NUMBER not null constraint PK_FILE_UPLOAD primary key (id) );2. 编写文件记录标识接口此接口每个文件调用一次调用完成后需要返回文件记录的信息主要是返回ID以便前端拿到后作为该文件所有分片的parent。接口入参public class FileNewRequest { private String index; private String parent; private String name; private String path; private String md5; private String size; public String getIndex() { return index; } public void setIndex(String index) { this.index index; } public String getParent() { return parent; } public void setParent(String parent) { this.parent parent; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getPath() { return path; } public void setPath(String path) { this.path path; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public String getSize() { return size; } public void setSize(String size) { this.size size; } }接口/** * 新创建一个文件标识记录 * author kevin * param request : * return com.liu.services.system.entity.FileUpload * date 2021/1/8 */ ApiOperation(value 文件标识记录, notes /file/new) PostMapping(/file/new) public ResponseVo uploadFile(Valid RequestBody FileNewRequest request) { String fileName request.getName(); String md5 request.getMd5(); try { //写入文件标识记录 FileUpload file (FileUpload) JSONObject.toBean(JSONObject.fromObject(request), FileUpload.class); //如果文件标识记录不存在则插入;如果存在则更新文件大小 FileUpload fileUpload fileMapper.findByMd5AndName(md5, fileName); if (ObjectUtils.isNotEmpty(fileUpload)) { fileMapper.updateSize(file); } else { fileMapper.insert(file); } fileUpload fileMapper.findByMd5AndName(md5, fileName); return new ResponseVo.Builder().ok().data(fileUpload).build(); }catch (Exception e){ return new ResponseVo.Builder().error().message(String.format(文件%1$s 上传错误, fileName)).build(); } }3. 编写分片是否存在判断接口此接口前端每个分片处理都需要调用判断跟第4点中的接口配合使用此处parent为第2点中接口返回的ID接口入参public class FileExistsRequest { private Long parent; private String md5; private Long size; public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public Long getSize() { return size; } public void setSize(Long size) { this.size size; } }接口/** * 文件分片上传判断文件记录或者文件分片是否已经存在 * * param request : * return com.liu.base.ResponseVo * author kevin * date 2021/1/9 */ PostMapping(/file/exists) public ResponseVo fileExists(Valid RequestBody FileExistsRequest request) { FileUpload file; if(null ! request.getParent()){ file fileMapper.findByParentAndMd5(String.valueOf(request.getParent()), request.getMd5()); }else{ ListFileUpload files fileMapper.findByMd5(request.getMd5()); //查找父文件ID不为空的记录即文件分片记录 files files.stream().filter(item - item.getParent() ! null).collect(Collectors.toList()); int count files.size(); if(count 1){ throw new CommonException(找到了不止一个分片/文件记录); }else if (count 1){ file null; }else { file files.get(0); } } //文件标识或文件片不存在 if(file null) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); } //无父文件ID则表示记录为文件标识记录判断文件是否存在 if(null file.getParent() || 0 file.getParent()){ ListFileUpload patchs fileMapper.findByParent(file.getId()); Long total patchs.stream().mapToLong(FileUpload::getSize).sum(); if(total.equals(request.getSize())) { return new ResponseVo.Builder().ok().data(FileExistsResponse.exists(file.getId())).build(); }else if(total 0) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); }else { return new ResponseVo.Builder().ok().data( FileExistsResponse.partExistent(file.getId(), fileMapper.findPatchIndexByParent(file.getId())) ).build(); } } //父文件ID存在需要判断的内容为文件片 //文件片存在且大小为正确的大小表示文件片已存在 if(file.getFileSize().equals(request.getFileSize())) { return new ResponseVo.Builder().ok().data(FileExistsResponse.exists(file.getId())).build(); } //文件片大小为0表示文件片还未上传 if(file.getFileSize() 0) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); } //其他文件片大小不为0又不等于需要上传的文件片大小表示文件片存在部分 return new ResponseVo.Builder().ok().data( FileExistsResponse.partExistent(file.getId(), fileMapper.findPatchIndexByParent(file.getId())) ).build(); }4. 编写文件分片上传接口此接口在前端调用文件分片是否存在判断分片不存在或只存在部分后调用此处parent为第2点中接口返回的ID接口入参public class FilePatchUploadRequest { private String name; private Long index; private Long parent; private String md5; private Long size; public String getName() { return name; } public void setName(String name) { this.name name; } public Long getIndex() { return index; } public void setIndex(Long index) { this.index index; } public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public Long getSize() { return size; } public void setSize(Long size) { this.size size; } }接口/** * 文件分片上传 * * param request : * param patch : * return com.liu.base.ResponseVo * author kevin * date 2021/1/7 */ PostMapping(/file/patch/upload) public ResponseVo filePatchUpload(Valid FilePatchUploadRequest request, RequestParam(patch) MultipartFile patch){ Long index request.getIndex(); //判断分片文件是否存在 if(patch.isEmpty()){ return new ResponseVo.Builder().error().message(第:%1$s 个分片未获取到分片文件).build(); } Long parent request.getParent(); Long size request.getSize(); String md5 request.getMd5(); String name request.getName(); //开始分片保存 FileUpload file fileMapper.findByParentAndMd5(String.valueOf(parent), md5); try { //判断数据库如果分片不存在或者分片大小与上传的分片的大小不一致则上传保存分片 if (file null || !file.getFileSize().equals(size)) { Optional.ofNullable(file).ifPresent(e - fileMapper.deleteByPk(e.getId())); //保存分片文件到服务器构建文件分片记录信息 FileUpload fileUpload new FileUpload(index, parent, name, saveFile(patch, size, d:/), md5, size); return new ResponseVo.Builder().ok().build(); } }catch (IOException e){ log.error(SystemConst.PATCH_UPLOAD_FILE_ERROR_LOG, name, index, e); throw new CommonException(ResponseState.REQUEST_ERROR.getCode(), String.format(SystemConst.UPLOAD_FILE_PATCH_ERROR, name, index)); } return new ResponseVo.Builder().ok().build() ; } /** * 保存文件块(此方法可提取为一个工具类中的工具方法) * author kevin * param source : 文件块 * param size : 文件块大小 * return java.lang.String * date 2021/1/7 */ private String saveFile(MultipartFile source, Long size, String path) throws IOException { String now LocalDateTime.now().format(DateTimeFormatter.ofPattern(SystemConst.DATE_ONLY_FORMATTER)); if(null source || source.isEmpty()) { throw new CommonException(未获取到需要保存的分片文件); } if(source.getSize() ! size) { throw new CommonException(分片文件大小与传入的分片大小不一致); } path path File.separator now; File file new File(path); if(!file.exists() !file.mkdirs()) { throw new CommonException(创建文件夹失败); } //根据文件名获取文件类型 String fileType (null fileName || !fileName.contains(.)) ? unknown : fileName.substring(fileName.lastIndexOf(.)); //保存文件块到磁盘 while(true) { String saveFileName UUID.randomUUID() fileType.toLowerCase() .tmp; File saveFilePath new File(path, saveFileName); //磁盘上块文件名如果已经存在则重新生成文件名 if(saveFilePath.exists()) { continue; } source.transferTo(saveFilePath); return saveFilePath.getAbsolutePath(); } }5. 编写文件分片合并接口此接口在前端确认所有分片已完成上传后调用。此处parent为第2点中接口返回的ID接口入参public class FileMergeRequest { private Long parent; private Long fileSize; public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public Long getFileSize() { return fileSize; } public void setFileSize(Long fileSize) { this.fileSize fileSize; } }接口/** * 文件分片合并 * author kevin * param request : * return com.liu.base.ResponseVo * date 2021/1/8 */ ApiOperation(value 文件分片合并, notes /file/merge) PostMapping(/file/merge) public ResponseVo filePatchMerge(Valid RequestBody FileMergeRequest request) { Long parent request.getParent(); Long size request.getFileSize(); //查询当前文件 FileUpload fileInfo fileMapper.findByPk(parent); String fileName null fileInfo ? : fileInfo.getName(); String md5 null fileInfo ? : fileInfo.getMd5(); try { //查询当前文件所有分块 ListFileUpload patchs fileMapper.findByParentPatchIndex(parent); Long total patchs.stream().mapToLong(FileUpload::getFileSize).sum(); if (fileInfo null || CollectionUtils.isEmpty(patchs) || !total.equals(size)) { fileMapper.deleteByParent(parent); log.error(String.format(文件合并失败大小不正确); return new ResponseVo.Builder().error().message(文件合并失败大小不正确).build(); } //合并文件 String path mergeFile(fileName, patchs.stream().map(FileUpload::getPath) .collect(Collectors.toList()), filePath, userName); fileMapper.updateByIdSetPathAndSize(String.valueOf(parent), path, String.valueOf(total)); fileMapper.deleteByParent(parent); }catch (Exception e){ fileMapper.deleteByParent(parent); e.printStackTrace(); return new ResponseVo.Builder().error().message(文件合并失败e.getMessage()).build(); } return new ResponseVo.Builder().ok().build(); } /** * 合并文件块(此方法可提取到工具类) * author kevin * param fileName : 文件名称 * param filePaths : 文件分片路径 * param path : 文件存放路径 * param directory : 文件存放目录 * return java.lang.String * date 2021/1/8 */ private String mergeFile(String fileName, ListString filePaths, String path, String directory) throws IOException { String now LocalDateTime.now().format(DateTimeFormatter.ofPattern(SystemConst.DATE_ONLY_FORMATTER)); path path File.separator (StringUtils.isEmpty(directory) ? : (directory File.separator)) now; File file new File(path); if(!file.exists() !file.mkdirs()) { throw new CommonException(创建文件夹失败); } //生成文件名 File saveFilePath new File(path, fileName); while (saveFilePath.exists()){ //生成文件名 Files.delete(saveFilePath.toPath()); } //创建文件输出流 try (FileOutputStream fos new FileOutputStream(saveFilePath)){ FileChannel out fos.getChannel(); //循环每一个文件块对应的文件 for (String filename : filePaths) { File patch new File(filename); //将每一个文件块的内容都追加到新文件中 try (FileChannel in new FileInputStream(patch).getChannel()) { in.transferTo(0, in.size(), out); } //追加完成后删除文件块 Files.delete(patch.toPath()); } out.close(); } return saveFilePath.getAbsolutePath(); }

相关新闻

Decnupaz(pivekimab sunirine-pvzy)中文说明书【海得康】

Decnupaz(pivekimab sunirine-pvzy)中文说明书【海得康】

药品基本信息通用名称‌:pivekimab sunirine-pvzy商品名称‌:Decnupaz药品类型‌:靶向CD123的抗体药物偶联物(ADC)适应症本品为处方药,适用于治疗成年母细胞性浆细胞样树突状细胞肿瘤(BPDCN&…

2026/11/13 15:00:50 阅读更多 →
终极Steam游戏数据提取指南:轻松获取DLC、成就和文件信息

终极Steam游戏数据提取指南:轻松获取DLC、成就和文件信息

终极Steam游戏数据提取指南:轻松获取DLC、成就和文件信息 【免费下载链接】GetDataFromSteam-SteamDB 项目地址: https://gitcode.com/gh_mirrors/ge/GetDataFromSteam-SteamDB 你是否曾经想要从Steam游戏中提取详细的DLC信息、成就数据或文件哈希值&#x…

2026/11/4 11:44:10 阅读更多 →
HarmonyOS应用实战-启示散页-57-题库颜色迁移别让旧数据失色:给 colorKey 做兼容映射

HarmonyOS应用实战-启示散页-57-题库颜色迁移别让旧数据失色:给 colorKey 做兼容映射

HarmonyOS 应用实战 57:题库颜色迁移别让旧数据失色,给 colorKey 做兼容映射 题库颜色看起来只是一个视觉字段,但它一旦写进 Preferences,就变成了需要迁移的持久数据。旧版本如果保存的是 #8AA7FF 或已经废弃的颜色名&#xff0c…

2026/11/5 1:34:21 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/13 5:13:25 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/13 5:13:25 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/13 5:13:25 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/12 18:22:26 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/12 18:22:25 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/13 5:13:25 阅读更多 →

日新闻

周新闻

月新闻