news 2026/7/28 21:23:57

JSP大文件分块上传的断点续传实现原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP大文件分块上传的断点续传实现原理

咱是一名福建的“老码农”,最近接了个外包项目,客户要做大文件上传功能,要求还挺细——原生JS实现、20G文件传输、文件夹保留层级、加密传输存储、断点续传兼容IE9… 预算还卡在100块以内(老板说“小项目不搞虚的”)。咱一边啃着泡面一边想:“这活得整明白,不然别说接单了,客户都得跑!”

先唠唠客户的“离谱”需求(但必须满足)

  • 大文件+文件夹:20G文件+1000+子文件的文件夹,每天批量上传,得稳得住。
  • 断点续传:用户关浏览器、重启电脑都不丢进度(咱就想:这用户怕不是经常断电?)。
  • 加密:传输用SM4/AES,存储也加密(客户说“数据安全比命重要”)。
  • 兼容IE9:部分用户还在Win7+IE9(咱叹气:“这浏览器比我还老…”)。
  • 非打包下载:几万文件打包?服务器直接崩(客户:“之前打包方案被骂惨了”)。

咱的“土味”解决方案(能跑就行)

没找开源组件(要么停更,要么不支持),咱自己整了个“原生JS+SpringBoot”的组合拳——前端用原生File API分片,后端用SpringBoot管分片,数据库记进度,加密用AES(SM4类似,换库就行)。

一、前端:Vue3 + 原生JS(兼容IE9+)

(代码尽量简单,老码农看得懂,新手能跑通)

import CryptoJS from 'crypto-js'; // 加密用(需npm install crypto-js) export default { data() { return { uploading: false, progress: 0, chunkSize: 5 * 1024 * 1024, // 5MB分片(20G分4000片) file: null, folderStructure: {}, // 记录文件夹层级 { "path/to/file.txt": { size: 1024, chunks: [] } } uploadId: null, // 全局唯一ID(断点续传标识) uploadedChunks: new Set() // 已上传分片 }; }, methods: { async handleFileSelect(e) { const files = e.target.files; if (!files.length) return; this.file = files[0]; // 生成全局唯一uploadId(用时间戳+随机数) this.uploadId = `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; // 遍历文件,记录文件夹结构(兼容IE9用iframe模拟) this.parseFolderStructure(files); // 检查后端已上传分片(断点续传关键) await this.checkUploadedChunks(); // 开始上传 this.uploadAllChunks(); }, // 解析文件夹结构(兼容IE9) parseFolderStructure(files) { Array.from(files).forEach(file => { // IE9不支持webkitRelativePath,用FileReader读路径(土办法) const reader = new FileReader(); reader.onload = (e) => { const path = e.target.result.split(',')[1]; // 实际需用File API获取相对路径(IE9用其他方法) this.folderStructure[path] = { size: file.size, chunks: [], uploaded: false }; }; reader.readAsDataURL(file); }); }, // 检查已上传分片(后端接口) async checkUploadedChunks() { const res = await this.$http.get(`/api/upload/check?uploadId=${this.uploadId}`); this.uploadedChunks = new Set(res.data.uploadedChunks); // 计算初始进度 this.progress = (this.uploadedChunks.size / this.totalChunks) * 100; }, // 上传所有分片(控制并发防崩溃) async uploadAllChunks() { this.uploading = true; const totalChunks = Math.ceil(this.file.size / this.chunkSize); for (let i = 0; i < totalChunks; i++) { if (this.uploadedChunks.has(i)) { this.progress = (i / totalChunks) * 100; continue; } const start = i * this.chunkSize; const end = Math.min(start + this.chunkSize, this.file.size); const chunk = this.file.slice(start, end); // 加密分片(AES,密钥从后端获取) const encryptedChunk = await this.encryptChunk(chunk); // 构造FormData(IE9用XHR+iframe) const formData = new FormData(); formData.append('file', encryptedChunk, `${this.uploadId}_${i}`); formData.append('chunkIndex', i); formData.append('totalChunks', totalChunks); formData.append('uploadId', this.uploadId); formData.append('filePath', this.filePath); // 文件夹路径 try { // 发送请求(IE9用xhr) const res = await this.$http.post('/api/upload/chunk', formData); this.uploadedChunks.add(i); this.progress = (i / totalChunks) * 100; } catch (err) { console.error('上传失败:', err); break; } } // 合并分片 if (this.uploadedChunks.size === totalChunks) { await this.mergeChunks(); } this.uploading = false; }, // 加密分片(AES示例) async encryptChunk(chunk) { const key = '客户提供的AES密钥(16/256位)'; // 从后端获取更安全 const iv = CryptoJS.lib.WordArray.random(16); // 随机IV const encrypted = CryptoJS.AES.encrypt( CryptoJS.lib.WordArray.create(chunk), CryptoJS.enc.Utf8.parse(key), { iv: iv } ); return new Blob([iv.toString(), encrypted.toString()], { type: 'application/octet-stream' }); }, // 合并分片(触发后端合并) async mergeChunks() { const res = await this.$http.post('/api/upload/merge', { uploadId: this.uploadId, fileName: this.file.name, totalChunks: this.totalChunks, filePath: this.filePath }); if (res.code === 200) { alert('上传成功!'); } } } };
二、后端:SpringBoot(分片管理+加密存储)

(代码简洁,兼容老版本,数据库记进度)

// FileUploadController.java(处理上传接口)@RestController@RequestMapping("/api/upload")publicclassFileUploadController{@Value("${upload.temp.path}")privateStringtempPath;// 临时分片存储路径(如:/data/temp)@Value("${upload.final.path}")privateStringfinalPath;// 最终存储路径(如:/data/files)@AutowiredprivateFileUploadMapperuploadMapper;// MyBatis Mapper(操作MySQL)// 检查已上传分片@GetMapping("/check")publicMapcheckChunks(@RequestParamStringuploadId){ListuploadedChunks=uploadMapper.selectUploadedChunks(uploadId);Mapres=newHashMap<>();res.put("uploadedChunks",uploadedChunks);returnres;}// 接收分片@PostMapping("/chunk")publicMapuploadChunk(@RequestParam("file")MultipartFilefile,@RequestParamintchunkIndex,@RequestParaminttotalChunks,@RequestParamStringuploadId,@RequestParamStringfilePath){// 创建临时目录StringtempDir=tempPath+"/"+uploadId;Filedir=newFile(tempDir);if(!dir.exists())dir.mkdirs();// 保存分片(加密存储:后端解密后存)StringchunkPath=tempDir+"/"+chunkIndex;try{file.transferTo(newFile(chunkPath));}catch(IOExceptione){returnMap.of("code",500,"msg","分片保存失败");}// 记录已上传分片到数据库uploadMapper.insertUploadProgress(uploadId,chunkIndex,filePath);returnMap.of("code",200);}// 合并分片@PostMapping("/merge")publicMapmergeChunks(@RequestParamStringuploadId,@RequestParamStringfileName,@RequestParaminttotalChunks,@RequestParamStringfilePath){StringtempDir=tempPath+"/"+uploadId;StringfinalFilePath=finalPath+"/"+filePath+"/"+fileName;// 合并分片(解密后合并)try(RandomAccessFileraf=newRandomAccessFile(finalFilePath,"rw")){for(inti=0;i<totalChunks;i++){StringchunkPath=tempDir+"/"+i;byte[]chunkData=Files.readAllBytes(Paths.get(chunkPath));byte[]decryptedData=aesDecrypt(chunkData);// AES解密raf.write(decryptedData);// 删除临时分片Files.delete(Paths.get(chunkPath));}}catch(IOExceptione){returnMap.of("code",500,"msg","合并失败");}// 清理数据库记录uploadMapper.deleteUploadProgress(uploadId);returnMap.of("code",200,"msg","合并成功");}// AES解密(密钥从配置中心获取)privatebyte[]aesDecrypt(byte[]data){Stringkey="客户提供的AES密钥(16/256位)";// 实际从配置/数据库获取// 解密逻辑(略,用AES/CBC/PKCS5Padding)returndecryptedData;}}// FileUploadMapper.java(MyBatis操作数据库)publicinterfaceFileUploadMapper{@Select("SELECT chunk_index FROM file_upload_progress WHERE upload_id = #{uploadId}")ListselectUploadedChunks(@Param("uploadId")StringuploadId);@Insert("INSERT INTO file_upload_progress (upload_id, chunk_index, file_path) "+"VALUES (#{uploadId}, #{chunkIndex}, #{filePath}) "+"ON DUPLICATE KEY UPDATE chunk_index = #{chunkIndex}")voidinsertUploadProgress(@Param("uploadId")StringuploadId,@Param("chunkIndex")intchunkIndex,@Param("filePath")StringfilePath);@Delete("DELETE FROM file_upload_progress WHERE upload_id = #{uploadId}")voiddeleteUploadProgress(@Param("uploadId")StringuploadId);}
三、数据库表结构(MySQL)
CREATETABLEfile_upload_progress(idINTPRIMARYKEYAUTO_INCREMENT,upload_idVARCHAR(64)NOTNULLCOMMENT'全局上传ID',chunk_indexINTNOTNULLCOMMENT'分片序号',file_pathVARCHAR(255)NOTNULLCOMMENT'文件/文件夹路径',UNIQUEKEYuk_upload_chunk(upload_id,chunk_index)-- 唯一索引防重复);

咱的“保姆级”支持(预算有限但服务到位)

  1. 代码完整:提供前端Vue3组件(含IE9兼容补丁)、后端SpringBoot代码(含MyBatis配置)、数据库脚本,解压就能跑。
  2. 部署简单:写了一键打包脚本(npm run build+mvn package),Tomcat直接扔WAR包,MySQL导入SQL就行。
  3. 加密支持:AES密钥可配置(客户自己保管),SM4换库就能用(附BouncyCastle集成文档)。
  4. 兼容兜底:IE9用iframe模拟上传(代码里标了注释),现代浏览器用原生Fetch,主流浏览器全支持。

最后唠唠:接单群+资源分享

咱建了个QQ群(374992201),专门拉“外包码农”和“找项目的老板”——群里福利拉满:

  • 新人加群送1~99元红包(手慢无!);
  • 推荐项目拿20%提成(2万项目提4千,比外卖自由香多了!);
  • 技术交流(大文件上传、加密、兼容问题随便问,老码农在线答疑);
  • 内推工作(福州IT圈岗位,大厂外包都有)。

咱这项目要是成了,答辩老师看了都得夸“这程序员有点东西”~ 赶紧加群,一起搞钱,一起秃,一起当“外包大佬”!

(PS:群里还有人分享“如何用AI写文档”的玄学技巧,亲测能过甲方审核!)

导入项目

导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程

工程

NOSQL

NOSQL示例不需要任何配置,可以直接访问测试

创建数据表

选择对应的数据表脚本,这里以SQL为例

修改数据库连接信息

访问页面进行测试

文件存储路径

up6/upload/年/月/日/guid/filename

效果预览

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。

下载示例

点击下载完整示例

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/16 13:04:43

2025年学术诚信防线:机器学习论文撤稿监控系统的构建与应用

2025年学术诚信防线&#xff1a;机器学习论文撤稿监控系统的构建与应用 【免费下载链接】ML-Papers-of-the-Week 每周精选机器学习研究论文。 项目地址: https://gitcode.com/GitHub_Trending/ml/ML-Papers-of-the-Week 在当前的学术研究环境中&#xff0c;论文撤稿已成…

作者头像 李华
网站建设 2026/7/27 18:54:25

2026网络安全趋势研判:从被动防护到智能对抗的体系跃迁

2026网络安全趋势研判&#xff1a;从被动防护到智能对抗的体系跃迁 基于传统边界防御和被动响应的安全范式&#xff0c;正迅速被AI驱动、供应链渗透与身份滥用等新型攻击模式所超越。 在生成式人工智能&#xff08;AIGC&#xff09;被用于规模化制造精准钓鱼攻击、量子计算对…

作者头像 李华
网站建设 2026/7/28 12:37:05

2026年网络安全预测:AI全面融入实战的100+行业洞察

2026年网络安全预测&#xff1a;AI全面融入实战的100行业洞察 随着人工智能深度融入企业运营和网络犯罪武器库&#xff0c;2026年网络安全格局将呈现自主威胁、身份中心型攻击和加速数字化转型风险的空前交汇。来自顶尖安全公司、政府机构和研究机构的专家们提出了100多项关键预…

作者头像 李华
网站建设 2026/7/23 16:38:28

为什么顶尖公司都在布局RISC-V AI加速器?C语言开发者的黄金机遇

第一章&#xff1a;为什么顶尖公司都在布局RISC-V AI加速器随着人工智能工作负载的爆炸式增长&#xff0c;传统计算架构在能效与灵活性上的瓶颈日益凸显。RISC-V作为一种开源指令集架构&#xff0c;凭借其模块化、可扩展和免授权费用的特性&#xff0c;正成为构建定制化AI加速器…

作者头像 李华
网站建设 2026/7/19 15:09:48

Knime数据分析终极指南:中文实操教程免费获取

Knime数据分析终极指南&#xff1a;中文实操教程免费获取 【免费下载链接】Knime案例教程中文文档下载 探索Knime的强大功能&#xff0c;轻松掌握数据分析与自动化流程&#xff01;这份精心整理的中文教程专注于实操部分&#xff0c;内容详实、步骤清晰&#xff0c;助您快速上手…

作者头像 李华
网站建设 2026/7/16 7:06:45

基于STM32CubeIDE的LVGL界面编辑器整合教程

手把手教你将LVGL界面编辑器无缝整合进STM32CubeIDE 你有没有经历过这样的开发场景&#xff1a;为了在一块TFT屏上画一个按钮&#xff0c;翻了半小时手册、调了两小时引脚、改了无数遍坐标&#xff0c;结果运行起来还是错位、闪烁、响应迟钝&#xff1f;更别提产品经理临时说“…

作者头像 李华