news 2026/7/28 2:43:16

SpringBoot如何处理100万文件上传的性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot如何处理100万文件上传的性能优化

咱福州软件工程狗实锤了!最近为了毕设焦头烂额——要做个能打的大文件管理系统,还要支持10G上传、断点续传、加密啥的,关键是得兼容IE8这种“古董”浏览器(学校机房那台Win7+IE9的老机器,点个按钮都像在蹦迪)。找了一圈开源代码,不是缺胳膊少腿就是“仅示例”,出了问题连个搭话的人都没有……今天必须把压箱底的方案掏出来,顺便求大神救救孩子!


需求拆解:我要的到底是个啥?

简单说就仨核心:

  1. 大文件上传:10G起步,断点续传(关浏览器/重启电脑都不怕),支持文件夹(保留层级,比如/部门/2024报告/数据.xlsx)。
  2. 加密:传输过程HTTPS,存储到OSS前AES加密(密钥自己管,阿里云也偷不走)。
  3. 兼容:IE8+(包括龙芯、红莲花这些信创浏览器)、Win7+IE9,学校老机器也得跑起来。

前端:Vue3 + 原生JS + WebUploader(兼容IE8的救命稻草)

IE8不支持H5的File API,只能靠Flash补全。WebUploader是百度开源的,支持Flash/H5双模式,正好适配老浏览器。前端核心逻辑:分片上传、断点查询、文件夹遍历、加密预处理。

1. 前端关键代码(Vue3组件)
import SparkMD5 from 'spark-md5'; // 用于生成文件唯一标识(MD5) import WebUploader from 'webuploader'; // 引入WebUploader export default { data() { return { uploader: null, progress: 0, fileMd5: '', // 当前文件的MD5(用于断点续传) chunkSize: 2 * 1024 * 1024, // 分片大小2MB(10G文件分5000片) uploadedChunks: [] // 已上传的分片序号 }; }, mounted() { this.initUploader(); }, methods: { initUploader() { // 初始化WebUploader(兼容IE8的Flash模式) this.uploader = WebUploader.create({ swf: '/static/webuploader/Uploader.swf', // Flash路径(IE8必须) server: '/api/upload/init', // 初始化接口(获取文件MD5) pick: '#filePicker', dnd: '#dndArea', paste: document.body, chunked: true, // 开启分片 chunkSize: this.chunkSize, compress: false, // 不压缩(大文件必须) fileNumLimit: 1000, // 最大文件数 fileSizeLimit: 10 * 1024 * 1024 * 1024, // 10G限制 accept: { title: 'All Files', extensions: '*' } }); // 文件加入队列时生成MD5(用于断点续传) this.uploader.on('fileQueued', (file) => { this.fileMd5 = ''; // 重置MD5 this.uploadedChunks = []; // 重置已上传分片 this.calculateFileMd5(file); // 计算文件MD5 }); // 分片上传前查询已上传的分片 this.uploader.on('uploadBeforeSend', (obj, data) => { data.md5 = this.fileMd5; // 传递MD5给服务端 data.chunk = obj.chunk; // 当前分片序号 data.chunks = obj.chunks; // 总分片数 }); // 上传进度更新 this.uploader.on('uploadProgress', (file, percentage) => { this.progress = Math.round(percentage * 100); }); // 上传完成回调 this.uploader.on('uploadFinished', (file) => { this.$message.success('上传完成!'); }); }, // 计算文件MD5(关键:断点续传的唯一标识) calculateFileMd5(file) { const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; const chunkSize = this.chunkSize; const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); fileReader.onload = (e) => { spark.append(e.target.result); // 读取分片内容并计算MD5 currentChunk++; if (currentChunk < chunks) { loadNext(); } else { this.fileMd5 = spark.end(); // 所有分片读取完成,得到最终MD5 // 查询服务端已上传的分片 this.queryUploadedChunks(this.fileMd5); } }; const loadNext = () => { const start = currentChunk * chunkSize; const end = Math.min(start + chunkSize, file.size); fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); }; loadNext(); }, // 查询服务端已上传的分片(断点续传核心) async queryUploadedChunks(md5) { try { const res = await this.$http.get(`/api/upload/chunks?md5=${md5}`); this.uploadedChunks = res.data.uploadedChunks; // 服务端返回已上传的分片序号(如[0,1,2]) // 告诉WebUploader跳过已上传的分片 this.uploader.options.chunkRetry = 0; // 失败不重试(已上传的分片跳过) this.uploader.upload(); // 开始上传 } catch (err) { console.error('查询已上传分片失败', err); } }, startUpload() { this.uploader.upload(); }, pauseUpload() { this.uploader.stop(); } } };
2. 前端兼容性处理
  • IE8:依赖Flash(Uploader.swf),需在index.html中引入flash.js解决跨域问题。
  • 文件夹上传:IE9+支持webkitdirectory属性(WebUploader自动处理),IE8只能逐个上传文件,需手动记录路径(比如让用户输入文件夹结构,或者在上传时让用户在文件名前加路径前缀,如部门/2024报告/文件.txt)。
  • 加密预处理:敏感文件可在前端用CryptoJS.AES.encrypt加密后再上传(密钥由用户输入,后端存储时二次加密)。

后端:SpringBoot + 分片合并 + 加密存储

后端核心逻辑:接收分片、记录进度、合并文件、加密存储到OSS。

1. 数据库设计(MySQL)

需要两张表:file_info(文件元信息)和file_chunk(分片记录)。

-- 文件元信息表CREATETABLEfile_info(idBIGINTPRIMARYKEYAUTO_INCREMENT,file_nameVARCHAR(255)NOTNULL,-- 文件名(含路径,如"部门/2024报告/数据.xlsx")file_sizeBIGINTNOTNULL,-- 文件大小(字节)md5VARCHAR(32)NOTNULL,-- 文件MD5(唯一标识)oss_pathVARCHAR(255),-- OSS存储路径(加密后的文件)encrypt_keyVARCHAR(255),-- 加密密钥(AES密钥,Base64编码)create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP);-- 文件分片记录表CREATETABLEfile_chunk(idBIGINTPRIMARYKEYAUTO_INCREMENT,file_md5VARCHAR(32)NOTNULL,-- 关联file_info.md5chunk_numberINTNOTNULL,-- 分片序号(0开始)chunk_pathVARCHAR(255)NOTNULL,-- 分片临时存储路径is_uploadedTINYINTDEFAULT0,-- 是否已上传(0未上传,1已上传)create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP,UNIQUEKEY(file_md5,chunk_number)-- 防止重复分片);
2. 后端关键接口(SpringBoot)
@RestController@RequestMapping("/api/upload")publicclassUploadController{@AutowiredprivateFileServicefileService;// 初始化上传(生成文件MD5并查询已上传分片)@PostMapping("/init")publicResultinitUpload(@RequestParam("md5")Stringmd5){FileRecordfileRecord=fileService.getByMd5(md5);if(fileRecord!=null){// 文件已存在(秒传)returnResult.success("文件已存在",fileRecord.getOssPath());}// 创建分片记录表(初始化所有分片为未上传)fileService.initChunks(md5);returnResult.success();}// 查询已上传的分片@GetMapping("/chunks")publicResultqueryUploadedChunks(@RequestParam("md5")Stringmd5){ListuploadedChunks=fileService.getUploadedChunks(md5);returnResult.success("查询成功",Map.of("uploadedChunks",uploadedChunks));}// 接收分片上传@PostMapping("/chunk")publicResultuploadChunk(@RequestParam("file")MultipartFilefile,@RequestParam("md5")Stringmd5,@RequestParam("chunk")IntegerchunkNumber,@RequestParam("chunks")IntegertotalChunks)throwsIOException{// 保存分片到临时目录(本地或OSS临时空间)StringtempDir="/tmp/upload/"+md5;Filedir=newFile(tempDir);if(!dir.exists())dir.mkdirs();StringchunkPath=tempDir+"/chunk_"+chunkNumber;file.transferTo(newFile(chunkPath));// 记录分片已上传fileService.markChunkUploaded(md5,chunkNumber);// 检查是否所有分片已上传(触发合并)if(fileService.isAllChunksUploaded(md5,totalChunks)){fileService.mergeChunks(md5,totalChunks);}returnResult.success();}// 合并分片并加密存储到OSSpublicvoidmergeChunks(Stringmd5,IntegertotalChunks)throwsIOException{FileRecordfileRecord=fileService.getByMd5(md5);if(fileRecord==null)thrownewRuntimeException("文件不存在");// 合并分片(按顺序读取临时文件,写入最终文件)StringtempDir="/tmp/upload/"+md5;FileoutputFile=newFile(tempDir+"/merged_"+System.currentTimeMillis());try(RandomAccessFileraf=newRandomAccessFile(outputFile,"rw")){for(inti=0;i<totalChunks;i++){FilechunkFile=newFile(tempDir+"/chunk_"+i);byte[]bytes=Files.readAllBytes(chunkFile.toPath());raf.write(bytes);chunkFile.delete();// 删除临时分片}}// 加密文件(AES加密)StringencryptKey=AESUtil.generateKey();// 生成随机密钥(或用户传入)byte[]encryptedBytes=AESUtil.encrypt(Files.readAllBytes(outputFile.toPath()),encryptKey);// 上传加密后的文件到OSS(阿里云OSS SDK)StringossPath="encrypted_files/"+fileRecord.getFileName();OSSClientossClient=newOSSClient("oss-cn-hangzhou.aliyuncs.com","accessKeyId","accessKeySecret");ossClient.putObject("your-bucket",ossPath,newByteArrayInputStream(encryptedBytes));// 更新文件元信息(删除临时目录)fileRecord.setOssPath(ossPath);fileRecord.setEncryptKey(encryptKey);fileRecord.setFileSize(outputFile.length());fileRecordMapper.updateById(fileRecord);FileUtils.deleteDirectory(newFile(tempDir));}}
3. 加密工具类(AES)
publicclassAESUtil{privatestaticfinalStringALGORITHM="AES";privatestaticfinalStringKEY_ALGORITHM="AES";privatestaticfinalStringDEFAULT_CIPHER_ALGORITHM="AES/ECB/PKCS5Padding";// 生成随机密钥(Base64编码)publicstaticStringgenerateKey(){KeyGeneratorkeyGen=KeyGenerator.getInstance(KEY_ALGORITHM);keyGen.init(256);// 256位密钥(更安全)SecretKeysecretKey=keyGen.generateKey();returnBase64.getEncoder().encodeToString(secretKey.getEncoded());}// 加密文件内容publicstaticbyte[]encrypt(byte[]data,Stringkey)throwsException{KeysecretKey=newSecretKeySpec(Base64.getDecoder().decode(key),KEY_ALGORITHM);Ciphercipher=Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE,secretKey);returncipher.doFinal(data);}// 解密文件内容(下载时用)publicstaticbyte[]decrypt(byte[]data,Stringkey)throwsException{KeysecretKey=newSecretKeySpec(Base64.getDecoder().decode(key),KEY_ALGORITHM);Ciphercipher=Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);cipher.init(Cipher.DECRYPT_MODE,secretKey);returncipher.doFinal(data);}}

兼容性兜底方案(IE8/IE9)

  • Flash上传:WebUploader的Flash模式在IE8下必须,需确保swf文件路径正确,且浏览器允许Flash运行(学校机房可能需要调整安全设置)。
  • 分片大小:IE8对分片支持一般,建议分片大小设为2MB(太小会增加请求次数,太大容易超时)。
  • 文件夹上传:IE8不支持webkitdirectory,只能让用户手动输入文件夹路径(比如在文件名前加部门/2024报告/前缀),后端根据路径创建OSS目录。

毕设演示注意事项

  1. 本地测试:用Win7+IE9虚拟机测试上传流程(断点续传:上传到50%时关闭浏览器,重新打开继续上传)。
  2. OSS演示:展示加密后的文件在OSS中的存储(密钥单独管理,不展示给用户)。
  3. 兼容性截图:拍IE8/龙芯浏览器的上传界面,证明“真·全浏览器支持”。

求救!大神在哪里?

现在代码框架搭好了,但有几个坑还没填:

  • IE8下Flash上传的跨域问题(crossdomain.xml配置)。
  • 大文件分片合并时的内存溢出(10G文件合并时,Java内存不够怎么办?可能需要流式合并)。
  • 加密密钥的安全存储(用户忘记密钥怎么办?需要设计密钥找回功能)。

群里加了个374992201,新人有红包!求大神来群里指导,或者直接甩个DEMO链接!毕设答辩就靠这个了,要是能上线,说不定还能接点外包(群里说推荐项目提20%提成,10万项目提2万,比打工香多了!)

(PS:老师说“能跑起来就行”,但我想做得更完美——毕竟这是大学最后作业了,冲就完事了!)

SQL示例

创建数据库

配置数据库连接

自动下载maven依赖

启动项目

启动成功

访问及测试

默认页面接口定义

在浏览器中访问

数据表中的数据

效果预览

文件上传

文件刷新续传

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

文件夹上传

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

示例下载

下载完整示例

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

PHP构建智能家居温控中心(从零到上线全流程)

第一章&#xff1a;PHP构建智能家居温控中心概述在物联网快速发展的背景下&#xff0c;智能家居系统逐渐成为现代家庭的重要组成部分。其中&#xff0c;温度控制作为环境调节的核心功能之一&#xff0c;直接影响居住的舒适性与能源效率。利用PHP这一广泛应用于Web开发的服务器端…

作者头像 李华
网站建设 2026/7/26 11:12:36

要实现“新建需求”功能

要实现“新建需求”功能&#xff0c;我们可以基于 Vue&#xff08;Element UI&#xff09; Spring Boot&#xff08;若依框架&#xff09; 做前后端分离开发&#xff0c;以下是完整实现方案&#xff1a; 一、后端&#xff08;Spring Boot 若依&#xff09; 1. 数据库表设计 需…

作者头像 李华
网站建设 2026/7/26 2:56:19

通俗理解卷积核与特征图

引言 在当今的科技世界中&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的方方面面&#xff0c;尤其是图像识别、自动驾驶和医疗诊断等领域。其中&#xff0c;卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;简称CNN&#xff09;是深度…

作者头像 李华
网站建设 2026/7/26 20:20:59

中兴通讯基站维护培训:HeyGem生成工程师教学视频

中兴通讯基站维护培训&#xff1a;HeyGem生成工程师教学视频 在通信网络日益复杂的今天&#xff0c;5G基站的部署密度持续攀升&#xff0c;设备迭代周期不断缩短。一线维护人员面临一个现实难题&#xff1a;如何在最短时间内掌握最新的故障处理流程&#xff1f;传统的培训方式—…

作者头像 李华
网站建设 2026/7/26 13:38:37

社会实践报告还在“记流水账”?百考通AI平台3分钟生成有深度、有反思、有社会价值的高质量总结

暑期结束&#xff0c;面对学校要求的社会实践报告&#xff0c;你是否还在苦恼于内容空洞、结构松散、写来写去只有“第一天走访社区&#xff0c;第二天发放问卷&#xff0c;第三天整理材料”这类流水账式记录&#xff1f;看似条理清晰&#xff0c;实则缺乏问题意识、没有理论联…

作者头像 李华
网站建设 2026/7/26 21:36:43

课程设计报告写成“操作手册”?百考通AI平台3分钟生成有原理、有分析、有工程思维的高质量总结

课程设计是工科、经管、教育等专业教学中的关键实践环节&#xff0c;但很多同学交出的报告却像一份“操作步骤清单”——“第一步打开软件&#xff0c;第二步输入参数&#xff0c;第三步点击运行……”缺乏理论支撑、过程反思与结果分析&#xff0c;被导师批为“只有流程&#…

作者头像 李华