JAVA百万文件分块上传性能优化策略

大文件上传系统开发指南(基于原生JS+SpringBoot)

项目概述

大家好,我是一个在浙江奋斗的Java程序员,最近接了个"刺激"的外包项目 - 开发一个支持20G大文件上传下载的系统,还要兼容IE9这种上古浏览器。客户要求使用原生JS实现(不能用jQuery等库),支持文件夹上传(保留层级结构),还要加密传输和断点续传。预算只有100元,但需要提供完整技术支持和源代码。

这活儿听着就刺激,但咱是谁?是打不死的小强!下面我就分享一下我的解决方案和部分代码实现。

技术选型

  • 前端:Vue3 CLI + 原生JavaScript(兼容IE9)
  • 后端:SpringBoot + Tomcat
  • 数据库:MySQL(主要存用户信息和文件元数据)
  • 文件存储:服务器本地文件系统
  • 加密:SM4(国密)和AES

系统架构

浏览器(IE9+) ←HTTP→ SpringBoot后端 ←本地IO→ 文件存储 ↑ MySQL(元数据)

前端实现(关键代码)

1. 文件夹上传组件(兼容IE9)

export default { data() { return { fileList: [], chunkSize: 5 * 1024 * 1024, // 5MB分片 concurrent: 3 // 并发上传数 } }, methods: { triggerFileInput() { document.getElementById('fileInput').click(); }, handleFileChange(e) { const files = e.target.files; if (!files.length) return; // 处理文件夹结构 const fileTree = this.buildFileTree(files); this.prepareUpload(fileTree); }, // 构建文件树结构(保留文件夹层级) buildFileTree(files) { const tree = {}; for (let i = 0; i < files.length; i++) { const file = files[i]; const path = file.webkitRelativePath || file.relativePath || file.name; const parts = path.split('/'); // 添加文件信息 const fileName = parts[parts.length - 1]; current[fileName] = { file: file, relativePath: path, size: file.size, chunks: Math.ceil(file.size / this.chunkSize), uploadedChunks: 0, progress: 0, status: '等待上传' }; } return tree; }, // 准备上传队列 prepareUpload(tree) { const flattenFiles = []; const traverse = (node, path = '') => { for (const key in node) { if (key === '__files__' || key === '__dirs__') continue; const newPath = path ? `${path}/${key}` : key; if (node[key].file) { // 是文件 flattenFiles.push({ ...node[key], relativePath: newPath }); } else { // 是目录,继续遍历 traverse(node[key].__dirs__, newPath); } } }; traverse(tree); this.fileList = flattenFiles; this.startUpload(); }, // 开始上传 startUpload() { const activeUploads = 0; const uploadNext = () => { if (activeUploads >= this.concurrent) return; const file = this.fileList.find(f => f.status === '等待上传'); if (!file) { if (this.fileList.every(f => f.status === '上传完成')) { this.$emit('upload-complete'); } return; } }; uploadNext(); }, // 分片上传文件 async uploadFile(fileEntry) { const file = fileEntry.file; const fileId = this.generateFileId(file); // 检查断点续传信息 const resumeInfo = this.getResumeInfo(fileId); let startChunk = resumeInfo ? resumeInfo.uploadedChunks : 0; for (let i = startChunk; i < fileEntry.chunks; i++) { const start = i * this.chunkSize; const end = Math.min(start + this.chunkSize, file.size); const chunk = file.slice(start, end); // 读取分片内容(兼容IE9) const chunkData = await this.readFileAsArrayBuffer(chunk); // 加密处理(这里简化,实际应该用Web Crypto API或polyfill) const encryptedData = this.encryptData(chunkData, 'AES'); // 实际应该用SM4 const formData = new FormData(); formData.append('fileId', fileId); formData.append('chunkIndex', i); formData.append('totalChunks', fileEntry.chunks); formData.append('relativePath', fileEntry.relativePath); formData.append('fileSize', file.size); formData.append('chunk', new Blob([encryptedData])); formData.append('fileName', file.name); } // 所有分片上传完成,通知服务器合并 await this.mergeFile(fileId, fileEntry.relativePath, file.size, file.name); fileEntry.status = '上传完成'; this.clearResumeInfo(fileId); }, // 以下是兼容IE9的工具方法 readFileAsArrayBuffer(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.readAsArrayBuffer(file); }); }, encryptData(data, algorithm) { // 实际项目中应该使用Web Crypto API或polyfill // 这里简化处理,实际加密代码会更复杂 if (typeof data === 'string') { return btoa(data); // 简单base64模拟加密 } else { const bytes = new Uint8Array(data); let result = ''; for (let i = 0; i < bytes.length; i++) { result += String.fromCharCode(bytes[i]); } return btoa(result); } }, async uploadChunk(formData) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload/chunk', true); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error('上传失败')); } }; xhr.onerror = () => reject(new Error('网络错误')); xhr.send(formData); }); }, async mergeFile(fileId, relativePath, fileSize, fileName) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload/merge', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error('合并失败')); } }; xhr.onerror = () => reject(new Error('网络错误')); xhr.send(JSON.stringify({ fileId, relativePath, fileSize, fileName })); }); }, // 断点续传相关方法(使用localStorage存储) generateFileId(file) { // 简单生成文件ID,实际应该更可靠 return `${file.name}-${file.size}-${file.lastModified}`; }, getResumeInfo(fileId) { const info = localStorage.getItem(`upload_resume_${fileId}`); return info ? JSON.parse(info) : null; }, saveResumeInfo(fileId, fileEntry) { localStorage.setItem(`upload_resume_${fileId}`, JSON.stringify({ uploadedChunks: fileEntry.uploadedChunks, relativePath: fileEntry.relativePath, fileSize: fileEntry.size, fileName: fileEntry.file.name })); }, clearResumeInfo(fileId) { localStorage.removeItem(`upload_resume_${fileId}`); }, formatSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } } }

后端实现(SpringBoot关键代码)

1. 文件上传控制器

@RestController@RequestMapping("/api/upload")publicclassFileUploadController{privatestaticfinalStringUPLOAD_DIR="/var/bigfileupload/";// 实际应该从配置读取privatestaticfinalintCHUNK_SIZE=5*1024*1024;// 5MB@PostMapping("/chunk")publicResponseEntityuploadChunk(@RequestParam("fileId")StringfileId,@RequestParam("chunkIndex")intchunkIndex,@RequestParam("totalChunks")inttotalChunks,@RequestParam("relativePath")StringrelativePath,@RequestParam("fileSize")longfileSize,@RequestParam("fileName")StringfileName,@RequestParam("chunk")MultipartFilechunk)throwsIOException{// 创建临时目录StringtempDir=UPLOAD_DIR+"temp/"+fileId+"/";FiletempDirFile=newFile(tempDir);if(!tempDirFile.exists()){tempDirFile.mkdirs();}// 保存分片(实际应该先解密)StringchunkPath=tempDir+chunkIndex;chunk.transferTo(newFile(chunkPath));// 记录上传进度(实际应该用数据库)UploadProgressprogress=newUploadProgress();progress.setFileId(fileId);progress.setUploadedChunks(chunkIndex+1);progress.setTotalChunks(totalChunks);progress.setRelativePath(relativePath);progress.setFileSize(fileSize);progress.setFileName(fileName);// saveToDatabase(progress); // 实际应该存数据库returnResponseEntity.ok().body(Map.of("status","success","chunkIndex",chunkIndex,"fileId",fileId));}@PostMapping("/merge")publicResponseEntitymergeFile(@RequestBodyMergeRequestrequest)throwsIOException,NoSuchAlgorithmException{StringfileId=request.getFileId();StringtempDir=UPLOAD_DIR+"temp/"+fileId+"/";FiletempDirFile=newFile(tempDir);if(!tempDirFile.exists()){returnResponseEntity.badRequest().body(Map.of("error","临时文件不存在"));}// 创建目标目录结构StringrelativePath=request.getRelativePath();StringtargetPath=UPLOAD_DIR+relativePath;FiletargetFile=newFile(targetPath);}// 下载文件接口(非打包方式)@GetMapping("/download")publicResponseEntitydownloadFile(@RequestParamStringfilePath,HttpServletResponseresponse)throwsIOException{// 设置响应头HttpHeadersheaders=newHttpHeaders();headers.add(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename=\""+file.getName()+"\"");headers.add(HttpHeaders.CONTENT_TYPE,Files.probeContentType(file.toPath()));headers.add(HttpHeaders.CONTENT_LENGTH,String.valueOf(file.length()));returnResponseEntity.ok().headers(headers).body(resource);}// 文件夹下载(递归下载)@GetMapping("/download/folder")publicvoiddownloadFolder(@RequestParamStringfolderPath,HttpServletResponseresponse)throwsIOException{// 实际实现应该递归遍历文件夹,生成zip或逐个文件下载// 这里简化处理,实际项目中需要更复杂的实现response.setContentType("application/zip");response.setHeader("Content-Disposition","attachment; filename=\""+newFile(folderPath).getName()+".zip\"");// 实际应该使用ZipOutputStream打包// 这里只是示例,实际不会这样实现try(ServletOutputStreamout=response.getOutputStream()){out.write("这不是真正的zip文件,实际应该递归打包文件夹".getBytes());}}}

2. 数据库实体类

@Entity@Table(name="file_metadata")publicclassFileMetadata{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateLongid;privateStringfileId;privateStringfilePath;privateStringfileName;privatelongfileSize;privateDateuploadTime;// getters and setters}

兼容IE9的注意事项

  1. XMLHttpRequest:IE9不支持FormData,但支持XMLHttpRequest上传文件
  2. FileReader:IE10+才完全支持,IE9需要polyfill
  3. Blob:IE10+支持,IE9需要使用BlobBuilder
  4. 加密:Web Crypto API在IE9不可用,需要使用第三方库如CryptoJS

IE9兼容的加密方案示例

// 在index.html中引入CryptoJS//// 修改encryptData方法encryptData(data,algorithm){if(algorithm==='AES'){// 使用CryptoJS进行AES加密constkey=CryptoJS.enc.Utf8.parse('1234567890123456');// 实际应该从安全配置读取constiv=CryptoJS.enc.Utf8.parse('1234567890123456');letdataToEncrypt;if(typeofdata==='string'){dataToEncrypt=data;}else{// 如果是ArrayBuffer,转换为字符串constbytes=newUint8Array(data);letstr='';for(leti=0;i<bytes.length;i++){str+=String.fromCharCode(bytes[i]);}dataToEncrypt=str;}constencrypted=CryptoJS.AES.encrypt(dataToEncrypt,key,{iv:iv,mode:CryptoJS.mode.CBC,padding:CryptoJS.pad.Pkcs7});returnencrypted.toString();}// 其他算法...returndata;// 未加密}

部署说明

  1. 前端构建

    npminstallnpmrun build

    将生成的dist目录内容部署到Tomcat的webapps/ROOT目录

  2. 后端配置

    • 修改application.properties
      server.port=8080 spring.servlet.multipart.max-file-size=21GB spring.servlet.multipart.max-request-size=21GB file.upload-dir=/var/bigfileupload/
    • 确保上传目录存在且有写入权限:
      mkdir-p /var/bigfileupload/tempchmod777/var/bigfileupload
  3. 数据库初始化

    • 创建MySQL数据库并执行SQL脚本:
      CREATETABLEfile_metadata(idBIGINTAUTO_INCREMENTPRIMARYKEY,file_idVARCHAR(255)NOTNULL,file_pathTEXTNOTNULL,file_nameVARCHAR(255)NOTNULL,file_sizeBIGINTNOTNULL,upload_timeDATETIMENOTNULL);

开发文档要点

  1. 系统功能

    • 大文件分片上传(支持20GB+)
    • 文件夹上传(保留层级结构)
    • 断点续传(基于localStorage)
    • 加密传输和存储(AES/SM4)
    • 兼容IE9+等主流浏览器
  2. API文档

    • POST /api/upload/chunk- 上传文件分片
    • POST /api/upload/merge- 合并文件分片
    • GET /api/upload/download- 下载单个文件
    • GET /api/upload/download/folder- 下载整个文件夹
  3. 部署文档

    • 环境要求:JDK 8+, Node.js, MySQL, Tomcat 8+
    • 配置文件说明
    • 初始化脚本

总结

这个项目确实挑战不小,但通过合理的分片上传、断点续传机制和兼容性处理,我们还是能够实现客户的需求。关键点在于:

  1. 前端使用原生JS实现文件夹结构解析和上传队列管理
  2. 后端提供分片上传和合并接口
  3. 使用localStorage存储上传进度实现断点续传
  4. 通过CryptoJS等库实现兼容IE9的加密

由于预算有限,我省略了一些高级功能如:

  • 完整的SM4加密实现(实际需要引入Bouncy Castle等库)
  • 分布式存储支持
  • 详细的权限控制
  • 完善的错误处理和日志

如果需要更完整的实现,建议考虑:

  1. 使用WebUploader等成熟库(但需要处理兼容性问题)
  2. 增加预算购买商业组件
  3. 分阶段开发,先实现核心功能

最后,欢迎加入我们的QQ群374992201,一起交流技术,合作接单!群里经常有红包和技术分享,还有项目合作机会哦!

导入项目

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

工程

NOSQL

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

创建数据表

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

修改数据库连接信息

访问页面进行测试

文件存储路径

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

效果预览

文件上传

文件刷新续传

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

文件夹上传

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

下载示例

点击下载完整示例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1130726.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图像文字识别与比对系统设计与实现

图像文字识别与比对系统设计与实现 1. 系统总体设计 1.1 系统架构 本系统采用C/S架构,前端使用WPF框架,后端检测功能使用C#实现。系统模块主要包括: 用户界面模块:WPF前端界面 图像采集模块:海康相机控制与触发 图像处理模块:OpenCV图像预处理 文字识别模块:基于OCR技…

企业多场景应用:客服、文档、邮件自动翻译系统搭建指南

企业多场景应用&#xff1a;客服、文档、邮件自动翻译系统搭建指南 在跨国协作日益频繁的今天&#xff0c;语言障碍成为企业运营中不可忽视的挑战。无论是客户服务响应、内部文档流转&#xff0c;还是跨区域邮件沟通&#xff0c;高效、准确的中英翻译能力已成为企业数字化基础设…

教育科技融合案例:在线课程字幕实时翻译实现路径

教育科技融合案例&#xff1a;在线课程字幕实时翻译实现路径 &#x1f4cc; 引言&#xff1a;AI 赋能教育&#xff0c;打破语言壁垒 在全球化加速的今天&#xff0c;优质教育资源的跨语言传播成为教育科技发展的重要方向。大量高质量的中文在线课程因缺乏多语言支持&#xff0c…

自动化部署:用Docker封装M2FP服务

自动化部署&#xff1a;用Docker封装M2FP服务 &#x1f9e9; M2FP 多人人体解析服务&#xff08;WebUI API&#xff09; 项目背景与技术痛点 在计算机视觉领域&#xff0c;人体解析&#xff08;Human Parsing&#xff09;是语义分割的一个重要子任务&#xff0c;目标是对图像中…

避开CUDA地狱:MGeo地址匹配模型的最佳实践

避开CUDA地狱&#xff1a;MGeo地址匹配模型的最佳实践 作为一名长期被CUDA版本和依赖冲突折磨的开发者&#xff0c;我深知在本地搭建MGeo地址匹配模型环境有多痛苦。本文将分享如何通过预置镜像快速部署MGeo模型&#xff0c;完全避开环境配置的坑&#xff0c;让你专注于地址匹…

告别环境配置噩梦:一键部署阿里通义Z-Image-Turbo的终极指南

告别环境配置噩梦&#xff1a;一键部署阿里通义Z-Image-Turbo的终极指南 作为独立开发者&#xff0c;你是否也遇到过这样的困境&#xff1a;想为电商平台添加AI生成商品图功能&#xff0c;却被各种依赖冲突和显存限制搞得焦头烂额&#xff1f;阿里通义Z-Image-Turbo正是为解决这…

GESP2025年12月认证C++八级真题与解析(判断题4-7)

&#x1f9e0; 判断题第 4 题1、&#x1f4cc; 题目原文使用 math.h 或 cmath 头文件中的函数&#xff0c;表达式 sqrt(4) 的结果类型为 double。✅ 判断结果&#xff1a;正确&#xff08;√&#xff09;2、&#x1f4d6; 故事讲解&#xff1a;&#xff08;1&#xff09;&#…

M2FP模型多任务学习:结合姿态估计的改进

M2FP模型多任务学习&#xff1a;结合姿态估计的改进 &#x1f4cc; 引言&#xff1a;从人体解析到多任务协同的演进 在计算机视觉领域&#xff0c;多人人体解析&#xff08;Human Parsing&#xff09; 是一项极具挑战性的任务&#xff0c;其目标是对图像中每个个体的身体部位进…

Z-Image-Turbo模型压缩技术解析:快速复现实验环境

Z-Image-Turbo模型压缩技术解析&#xff1a;快速复现实验环境 为什么需要预配置的研究环境 作为一名研究生&#xff0c;想要复现Z-Image-Turbo论文中的实验结果&#xff0c;最大的挑战往往不是理解算法本身&#xff0c;而是搭建一个能够运行实验的环境。Z-Image-Turbo作为阿里巴…

阿里通义Z-Image-Turbo WebUI与个性化推荐:如何生成符合用户偏好的图像

阿里通义Z-Image-Turbo WebUI与个性化推荐&#xff1a;如何生成符合用户偏好的图像 在电商领域&#xff0c;个性化产品展示图能显著提升用户转化率。阿里通义Z-Image-Turbo WebUI 是一款基于AI的图像生成工具&#xff0c;可帮助电商平台根据用户偏好快速生成定制化的产品展示图…

如何评估翻译质量?BLEU之外的人工评测标准

如何评估翻译质量&#xff1f;BLEU之外的人工评测标准 &#x1f4cc; 引言&#xff1a;AI 智能中英翻译服务的兴起与挑战 随着全球化进程加速&#xff0c;跨语言沟通需求激增&#xff0c;AI 驱动的智能翻译服务正成为企业、开发者乃至个人用户的刚需工具。当前市场上的中英翻…

如何扩展M2FP模型支持更多身体部位识别?

如何扩展M2FP模型支持更多身体部位识别&#xff1f; &#x1f9e9; M2FP 多人人体解析服务&#xff1a;从基础到进阶 在当前计算机视觉领域&#xff0c;人体解析&#xff08;Human Parsing&#xff09; 正成为智能交互、虚拟试衣、动作分析等应用的核心技术。其中&#xff0c;M…

机械臂视觉抓取系统:从仿真到部署的完整实现

机械臂视觉抓取系统:从仿真到部署的完整实现 摘要 本文详细阐述了一个完整的机械臂视觉抓取系统的设计与实现,涵盖从仿真环境搭建到实物部署的全过程。系统基于ROS(Robot Operating System)框架,整合了Intel RealSense D435i深度相机、六自由度机械臂、DH夹爪以及自主开…

金融报告翻译实战:数字与单位的精确转换策略

金融报告翻译实战&#xff1a;数字与单位的精确转换策略 在金融、投资和跨国企业沟通中&#xff0c;高质量的中英翻译服务是确保信息准确传递的关键。尤其在处理财务报表、年度报告、市场分析等专业文档时&#xff0c;不仅要求语言通顺自然&#xff0c;更对数字表达、计量单位、…

灾备恢复计划:镜像备份与快速重建流程

灾备恢复计划&#xff1a;镜像备份与快速重建流程 &#x1f310; AI 智能中英翻译服务 (WebUI API) 在现代软件开发与AI服务部署中&#xff0c;系统的高可用性与快速灾备恢复能力已成为衡量服务稳定性的关键指标。本文以“AI 智能中英翻译服务”为实际案例&#xff0c;深入探讨…

M2FP模型边缘计算部署:树莓派实战案例

M2FP模型边缘计算部署&#xff1a;树莓派实战案例 &#x1f310; 项目背景与技术选型动因 随着智能安防、体感交互和虚拟试衣等应用的兴起&#xff0c;多人人体解析&#xff08;Multi-person Human Parsing&#xff09;作为细粒度语义分割的重要分支&#xff0c;正逐步从云端走…

如何用M2FP优化安防系统中的人体检测?

如何用M2FP优化安防系统中的人体检测&#xff1f; &#x1f9e9; M2FP 多人人体解析服务&#xff1a;重新定义精细化人体理解 在现代智能安防系统中&#xff0c;传统的人体检测技术&#xff08;如YOLO、SSD等&#xff09;虽然能够快速定位画面中是否有人&#xff0c;但其输出…

M2FP模型在时尚行业的7个创新应用场景

M2FP模型在时尚行业的7个创新应用场景 &#x1f310; 技术背景与行业痛点 随着个性化消费和数字化体验的兴起&#xff0c;时尚行业正加速向智能化转型。传统服装设计、虚拟试穿、搭配推荐等环节高度依赖人工经验&#xff0c;效率低、成本高&#xff0c;且难以满足用户对“千人…

基于M2FP的智能广告系统:精准人群画像生成

基于M2FP的智能广告系统&#xff1a;精准人群画像生成 在数字广告日益追求“千人千面”的今天&#xff0c;用户画像的粒度决定了广告投放的精度。传统基于行为数据和基础属性的人群标签已难以满足精细化运营需求。而视觉信息——尤其是用户在真实场景中的穿着、姿态与外貌特征…

Python 迷失录

下面记录Python的奇怪行为round&#xff08;&#xff09;系统提升的round()说明Help on built-in function round in module builtins:round(number, ndigitsNone)Round a number to a given precision in decimal digits.The return value is an integer if ndigits is omitte…