ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JSP大文件上传攻略:分块上传与多附件处理实战

JSP大文件上传攻略:分块上传与多附件处理实战 1. 从一次真实故障说起传统JSP上传大文件为什么必挂我是在一个维护了六七年的传统JSP项目里遇到这个问题的。系统本身不复杂JSP页面加Servlet加Tomcatwar包扔到webapps里就完事。之前上传的附件基本都在几十MB以内一直相安无事。直到有一天业务方要往系统里传一个多GB的地质数据包页面转圈了半个多小时最后浏览器直接报“无法访问此网站”后台日志只有一行连接被重置。排查了很久最终发现根本不是某一个环节坏了而是整条链路都为“小文件”设计碰到大文件时从浏览器到nginx再到Tomcat每一层都在卡脖子。当时网上搜出来的方案大多围绕“加大内存”“调大上传限制”试了之后能缓解但只要文件超过1GB依然会出问题。后来把上传方式改成“分块上传”这个问题才算真正解决。这篇文章适合还在维护传统JSPServletTomcat项目的人也适合用Idea新建JSP项目做毕设、实训的同学。我会把分块上传的完整思路、前端切片、后端接收合并、参数调优和踩坑经历都写清楚。核心关键词就三个JSP、大文件、多附件但看完你会发现真正解决问题的核心并不在JSP语法里而在请求模型的设计上。1.1 一次典型的“大文件上线事故”那次故障的触发条件很典型业务方凌晨上传一个2.3GB的数据包从凌晨0点传到快2点进度条一直在走最后却显示失败。用户心态崩了我们的电话也被打爆了。当时我们的技术栈是nginx做反向代理Tomcat 8.5跑应用上传用的是最传统的multipart/form-data方式。JSP页面上一个input typefile后端用request.getInputStream()读流再写到指定目录。这套组合对付一两百MB勉强能跑一旦上GB问题就开始连环出现浏览器端把整个文件组织成FormData一次发出内存和网络连接都要长时间占用nginx默认的client_max_body_size只有1MB虽然项目里改过但稍微没改到位就又拦一道Tomcat端要等完整请求体到达才能返回响应中间任何一个网络抖动之前传的数据全部作废更隐蔽的问题是Tomcat的maxSwallowSize和connectionTimeout也会在超大请求下触发异常表现却是五花八门的http500或者连接重置。这些坑单看都不难解决但它们叠加在一起时总觉得像拆东墙补西墙。真正让人下定决心改造的是那次上传失败后查看临时目录发现服务端已经收到了超过1GB的半截文件却没有合并逻辑可用只能人工清理。从那一刻我就意识到整包上传这条路在大文件场景下是不可持续的。1.2 浏览器、nginx、Tomcat三层各自卡在哪与其一个一个查文档不如把一次上传拆成三层来看客户端、反向代理层、应用服务器层。每一层都有自己的“默认限制”也要分别处理。层级关键参数典型默认值大文件时的表现浏览器请求体一次性构造无明示限制JS内存上涨、弱网易中断、失败后整包重来nginxclient_max_body_size1m大文件直接413 Request Entity Too Largenginxproxy_request_bufferingon会先把请求体缓冲到临时文件增加磁盘开销TomcatmaxPostSize2MB左右超过后Tomcat可能直接拒绝解析表单参数TomcatmaxSwallowSize2MB左右上传中断时吞不完剩余数据连接处理异常应用代码InputStream一顿读无一次性读入内存大文件撑爆堆内存很多人搜“nginx支持jsp吗”其实nginx本身不执行JSP它只是反向代理到Tomcat但代理层对请求体的大小和处理方式有独立的限制。如果只改Tomcat不改nginx大文件会在代理层被拦掉只改nginx不改Tomcat请求虽然进了Tomcat也可能因为表单解析限制而失败。这两层必须一起调。应用代码就更直接了。很多老项目在Servlet里写的是byte[] bytes IOUtils.toByteArray(request.getInputStream());这种写法等于是把整个文件装进内存文件一大了基本等于自杀。还有用commons-fileupload的虽然它会把文件先落盘但如果分块逻辑没做依然解决不了网络中断导致的全量重传问题。1.3 分块上传到底解决什么、不解决什么分块上传的核心思路很简单把一个大文件切成若干个小块分别上传全部上传完后再合并。每块可以独立失败、独立重试服务端提前记录已收到的块客户端对“已经传过的块”可以跳过这就是断点续传的基础。但它不是银弹。分块上传解决的是“一次请求体过于庞大导致的不稳定”不解决带宽不够、服务端磁盘不够、文件重复存储、权限校验不完善这些工程问题。如果一个文件动不动就是100GB我的建议是直接走对象存储加分块下载方案不要再让流量经过业务Tomcat中转。那已经是另一个话题了要在“大文件下载测试”里单独验证。而且分块上传是有成本的。每切一块就是一次额外请求前端要管理队列后端要管理工作目录和合并逻辑复杂度一定会上升。如果业务里绝大多数文件都在几十MB以内老老实实调大nginx和Tomcat限制就够了没必要为了技术上的“政治正确”强行分块。2. 分块上传的整体设计协商、分块提交、最终合并改造之前我先把上传流程画了一遍。我不是画图只是列步骤。分块上传的骨架其实就三个环节前端选择文件后先确定文件标识fileId、总块数totalChunks、每块大小把这些元数据告诉后端前端把文件按固定大小切成N块每一块用独立的HTTP请求提交后端把每一块写入临时目录所有分块提交完成后后端按顺序把分块合并成完整文件做大小和校验和验证最后返回可访问的文件地址。这套设计里最容易被忽略的是“协商”这一步。很多初学者上来就切分块但没有文件标识、没有总块数后端接收之后根本不知道这个文件一共有几块、是否齐了。所以我强烈建议先把接口协议定义清楚。2.1 接口与参数约定我用的是一套很朴素的接口不需要额外引入框架支持接口入参说明POST /upload/chunkfileId、fileName、fileSize、totalChunks、chunkIndex、file上传单个分块POST /upload/mergefileId、fileName、fileSize、totalChunks、fileMd5所有分块传完后触发合并GET /upload/progressfileId查询已上传分块索引用于断点续传参数里最关键的是fileId。同一文件的所有分块必须共用一个fileId后端靠它创建独立目录避免不同用户上传同名文件时出现分块互相覆盖。fileId一般用UUID在前端生成也可以由后端在“创建上传任务”时下发。chunkIndex建议从0开始和后端循环顺序一致避免出现“我以为从1开始后端从0开始”的错位问题。fileSize必须传合并后拿实际大小跟它比对是判断分块是否丢失最简单的手段。fileMd5可以后补因为大文件在前端计算MD5非常耗时如果网络好、时间紧可以只做大小校验。2.2 临时目录与文件命名规范后端接收分块后第一件事是为每个fileId建立独立目录目录结构长这样/data/upload_tmp/ 20250612/ {fileId}/ 0.part 1.part 2.part metadata.json为什么不用客户端的原始文件名直接拼接路径因为文件名会带来两类问题一是不同目录下的同名文件互相覆盖二是中文文件名、特殊字符可能引发编码问题甚至被构造出路径穿越。所以我在存储层面全程只用fileId和chunkIndex原始文件名只保存在元数据里最后合并时重新命名。metadata.json里记录了fileName、fileSize、totalChunks、createTime这些字段。用文件做元数据简单直接但如果系统已经接入了MySQL也可以建一张upload_task表字段对应上面这些信息再加一个status字段标记“上传中”“合并中”“已完成”“已失败”。有数据库的好处是查询进度、清理临时文件都方便坏处是写代码多一点。我两种都试过如果只是应急改造用metadata.json最快。还有一个容易踩的坑临时目录不要放在webapps下面尤其是传统JSP项目打包war部署的场景重新部署war包时Tomcat有可能会把整个应用目录清掉你的半成品分块就没了。更合理的路径是独立配置比如/data/upload_tmp和应用目录彻底隔离。2.3 同步合并还是异步合并最后一小块普通请求把其余分块合并掉看起来最省事。但问题在于合并大文件会占用请求线程Tomcat线程一直占着不放前端那个请求就一直转圈。如果文件正好几千兆合并可能耗时几十秒期间HTTP连接超时、用户刷新页面线程才会释放。所以设计合并环节时要先想清楚“最后一小块到达后是同步返回结果还是立刻返回‘合并中’由前端轮询/等待通知”。我的经验是文件总大小低于500MB时同步合并够用超过这个量建议异步合并。异步也很简单服务端放一个线程池ExecutorService mergeExecutor Executors.newFixedThreadPool(4); // 在最后一个分块落地后提交合并任务 mergeExecutor.submit(() - mergeFile(fileId, totalChunks));合并任务执行期间前端定时调用GET /upload/progress或者用Servlet 3.1的AsyncContext等合并成功后再把响应写回去。传统JSP项目可能没引入WebSocket轮询是成本最低的方案合并一个几个GB的文件轮询间隔设1到2秒完全够用。3. JSP页面端改造纯JS切片与多附件管理JSP页面在这个方案里的任务很纯粹渲染页面、引入前端脚本、展示上传状态。不要在JSP脚本片段里写上传逻辑更不要用% %去拼一堆Java状态否则页面刷新一次整个上传队列就没了。上传队列、分块索引、重试状态这些应该放在浏览器端JS对象里。3.1 JSP页面编码与资源引入页面头部的编码设置必须一致否则中文文件名非常容易乱码。JSP页面我通常这样写% page pageEncodingUTF-8 contentTypetext/html;charsetUTF-8 %对外的URL上传参数里中文文件名必须做一次encodeURIComponent后端拿到后再用URLDecoder.decode还原。如果你发现下载下来的文件名全是问号多半是中间某一层没有统一UTF-8或者漏了URL编解码。为了保险我在controller里一律这么处理String fileName URLDecoder.decode(req.getParameter(fileName), UTF-8);前端用原生XHR或axios都可以。如果项目里已经引了jQuery用$.ajax也问题不大但要注意processData和contentType必须设成false否则FormData会被jQuery转换掉后端getPart(file)会取不到文件。3.2 切片与并发控制代码这是前端最核心的部分。我直接用原生JS写了个精简版逻辑是选择文件后按固定分块大小切片然后维护一个任务队列限制并发数为3每传完一块就补一个新任务。这样既能提高速度又不会一次性把几十个请求全打出去把Tomcat压垮。input typefile idfileInput multiplemultiple / button onclickstartUpload()开始上传/button progress idtotalProgress value0 max100/progressconst MAX_CONCURRENT 3; const CHUNK_SIZE 10 * 1024 * 1024; // 10MB let taskQueue []; let activeCount 0; let totalUploaded 0; let totalSize 0; function startUpload() { const files document.getElementById(fileInput).files; totalSize Array.from(files).reduce((sum, f) sum f.size, 0); Array.from(files).forEach(file { const fileId generateUUID(); const totalChunks Math.ceil(file.size / CHUNK_SIZE); for (let i 0; i totalChunks; i) { const start i * CHUNK_SIZE; const end Math.min(start CHUNK_SIZE, file.size); const blob file.slice(start, end); taskQueue.push({ fileId, fileName: file.name, fileSize: file.size, totalChunks, chunkIndex: i, blob }); } }); pumpTasks(); } function pumpTasks() { while (activeCount MAX_CONCURRENT taskQueue.length 0) { const task taskQueue.shift(); activeCount; uploadChunk(task).finally(() { activeCount--; pumpTasks(); }); } } function uploadChunk(task) { const formData new FormData(); formData.append(fileId, task.fileId); formData.append(fileName, encodeURIComponent(task.fileName)); formData.append(fileSize, task.fileSize); formData.append(totalChunks, task.totalChunks); formData.append(chunkIndex, task.chunkIndex); formData.append(file, task.blob, task.fileName); return new Promise((resolve, reject) { const xhr new XMLHttpRequest(); xhr.open(POST, /upload/chunk); xhr.upload.onprogress evt { if (evt.lengthComputable) { // 这里只代表当前分块的进度整体进度要在外部累加 } }; xhr.onload () { totalUploaded task.blob.size; document.getElementById(totalProgress).value Math.floor(totalUploaded / totalSize * 100); resolve(); }; xhr.onerror () reject(xhr.statusText); xhr.send(formData); }); } function generateUUID() { return xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx.replace(/[xy]/g, c { const r Math.random() * 16 | 0; const v c x ? r : (r 0x3 | 0x8); return v.toString(16); }); }我特别想说一下并发数。并发不是越大越好。每开一个上传请求Tomcat就要占用一个线程nginx也要维持一条连接。并发从3调到5上传速度可能没有明显提升但线程占用和失败重试的次数会明显增加。尤其在内网带宽不高的情况下并发5时常出现某些分块超时反而需要重传。我们压测下来普通办公网络下并发3是个性价比很高的值。3.3 多附件选择、进度展示与断点续传多附件和单文件最大的区别在于每个文件是独立的上传任务失败、重试、取消都不能互相拖累。所以我给每个文件分配独立fileId进度条也分两级总进度条显示所有文件的整体进度文件列表每一项有独立的分块进度。用户能直观看到“a.zip传了3/10”“b.tar.gz传了7/20”。断点续传的实现思路其实不复杂。上传分块时后端把已落地的分块索引记录下来。前端在上传前先调一次GET /upload/progress?fileIdxxx拿到后端返回的SetInteger生成任务队列时跳过这些索引。但有一点必须说清楚页面刷新后File对象就没了必须让用户重新选择文件。此时fileId能不能对得上取决于你愿不愿意把fileId存在localStorage里。我踩过这个坑之后的做法是选择文件时生成fileId同时把fileId和文件基本信息写入localStorage页面刷新后如果发现localStorage里有这个文件且文件大小相同就继续沿用原fileId否则重新生成。这属于锦上添花但体验差异非常大特别是那些传一个多GB文件传到80%突然手滑刷新页面的人。如果担心JS在主线程切片、读文件导致UI卡顿可以考虑把切片和文件读取逻辑放进Web Worker。Worker处理的是二进制数据不影响页面渲染。老项目引入Worker时最需要注意的是URL同源问题部署成war包后把Worker脚本放在js目录下就行。这个改造不是必须的文件在2GB以下时主线程直接slice()的感受并不明显但如果你追求极致这是一个可选项。4. 后端Servlet实现分块接收、完整性校验与异步合并后端是整个分块上传真正受力最多的地方。前端切块再溜后端如果不能正确接收、落盘、合并、校验一切都是白搭。传统JSP项目用的最多的就是Servlet所以这部分我直接按Servlet写。4.1 Servlet 3.0 multipart配置Servlet 3.0开始支持request.getPart(file)不用再手动解析multipart流。给上传分块的Servlet加一个注解即可WebServlet(/upload/chunk) MultipartConfig( fileSizeThreshold 1024 * 1024 * 2, maxFileSize 1024 * 1024 * 20, maxRequestSize 1024 * 1024 * 20, location /data/upload_tmp ) public class ChunkUploadServlet extends HttpServlet { // ... }这里每个参数都有讲究fileSizeThreshold设为2MB表示超过2MB的part直接写入磁盘不再放内存防止多个并发分块同时堆在堆内存里。maxFileSize设为20MB是为了配合前端10MB分块挡住有人“绕过前端直接整包POST”的行为。maxRequestSize同样设为20MB让每个请求体的上限可控。如果你不想限制可以写成-1但生产环境我不建议这样做等于给DoS攻击留了个大洞。location指向临时根目录必须确保目录存在并有写权限。如果项目还在用web.xml部署方式就用multipart-config标签配置效果一样。传统JSP项目打包war时这个配置会打包进war里在Tomcat里正常生效。4.2 分块落盘与元数据记录分块落地我不用part.write()因为不同容器对Part.write(String fileName)的路径解析方式不完全一样遇到路径分隔符和相对路径很容易踩坑。稳妥做法是读取输入流自己写入目标文件protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); String fileId req.getParameter(fileId); String fileName URLDecoder.decode(req.getParameter(fileName), UTF-8); long fileSize Long.parseLong(req.getParameter(fileSize)); int totalChunks Integer.parseInt(req.getParameter(totalChunks)); int chunkIndex Integer.parseInt(req.getParameter(chunkIndex)); File chunkDir new File(/data/upload_tmp, fileId); if (!chunkDir.exists() !chunkDir.mkdirs()) { throw new IOException(create chunk dir failed); } Part part req.getPart(file); File chunkFile new File(chunkDir, chunkIndex .part); try (InputStream in part.getInputStream(); FileOutputStream out new FileOutputStream(chunkFile)) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } } // 写入元数据 if (!new File(chunkDir, metadata.json).exists()) { // 用Jackson/Gson或手拼JSON都可以这里不展开 } // 查询已落地的分块数量 long completed Arrays.stream(chunkDir.listFiles((dir, name) - name.endsWith(.part))) .count(); resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\chunkIndex\: chunkIndex ,\completed\: completed }); }写完每个分块后检查已落地分块数量是否等于totalChunks。如果相等说明最后一个分块到了可以触发合并。这里有一个并发窗口问题某些分块还在上传时另一个请求也可能同时检查到数量相等导致同一个fileId被合并两次。我的处理方式是在合并前加一个状态标记File lockFile new File(chunkDir, merging.lock); if (completed totalChunks lockFile.createNewFile()) { mergeExecutor.submit(() - mergeFile(fileId, fileName, fileSize, totalChunks, chunkDir)); }createNewFile()能保证同一时刻只有一个线程成功创建锁文件其他线程看到文件已存在就不触发合并了。这种文件锁方式适合单机部署比synchronized(fileId.intern())更安全而且重启后锁文件还在不会重复合并。4.3 合并与完整性校验合并逻辑并不复杂按顺序把所有.part文件内容写入最终文件private void mergeFile(String fileId, String fileName, long fileSize, int totalChunks, File chunkDir) { File target new File(/data/upload/ System.currentTimeMillis() _ fileName); try (FileOutputStream fos new FileOutputStream(target); FileChannel out fos.getChannel()) { for (int i 0; i totalChunks; i) { File partFile new File(chunkDir, i .part); if (!partFile.exists()) { // 分块缺失可以记录失败状态后中断 return; } try (FileInputStream fis new FileInputStream(partFile); FileChannel in fis.getChannel()) { in.transferTo(0, in.size(), out); } } } catch (IOException e) { // 合并失败删除半成品文件 target.delete(); return; } // 大小校验 if (target.length() ! fileSize) { target.delete(); return; } // 可选MD5校验 // if (!md5(target).equalsIgnoreCase(fileMd5)) { target.delete(); return; } // 合并成功删除临时目录 deleteQuietly(chunkDir); }这里有两个细节很值得说。第一个是transferTo的效率比边读边写的通用IO好因为它在底层可能走sendfile或零拷贝优化合并大文件时差距明显。第二个是合并期间如果某一块缺失不要继续合并直接返回。我刚开始做的时候觉得“先把有的合了等缺的块补上来再补写”也行后来发现这样会引入大量“已合并部分字节数”的复杂度最后文件错乱。分块上传就应该严格遵循“全部就位才合并”。MD5校验要谨慎使用。一个大文件算一次MD5非常慢在传统JSP项目的同步请求里做很可能拖到前端超时。所以我建议把MD5校验也放到异步合并线程中而且即使要校验也得在大小校验通过之后再算避免无谓开销。另一个思路是每传一个分块时前端同时传一个chunkMd5后端逐块校验。开销更小定位问题也更精确。秒传的判断可以放在合并之前如果数据库里已经存在相同fileId、相同fileSize、相同fileMd5的记录那就直接把已存储文件的访问地址返回给前端不需要再合并一次。这个功能对重复上传场景收益很高尤其是工程行业那种“不同人反复上传同一个数据包”的情况。4.4 清理策略与集群部署的坑临时目录一定会残留。用户传到一半关闭浏览器、弱网断开、断电都会留下半成品分块。不清理的话/data/upload_tmp很快会被填满。我写过最简单的定时任务每天凌晨扫描临时根目录删除所有“最后修改时间超过24小时且没有合并锁文件”的任务目录。如果任务目录里有merging.lock说明合并中或者合并异常还需要人工介入但大多数残留都能自动清掉。集群部署是另一个大坑。传统JSP项目打包war后如果真的部署了多个Tomcat实例你就得想清楚一个问题同一个fileId的不同分块可能被负载均衡分到不同的机器上。如果每台机器都写本地磁盘分块散落在各自节点合并时谁也找不到谁。解决方向有三个把临时目录挂到共享存储上比如NFS让所有节点读同一个目录按hash路由把相同fileId的请求都发到同一台机器但要保证机器不宕机不依赖本地文件系统直接把分块上传到对象存储合并也在对象存储侧完成。第一种最符合老项目习惯但NFS的锁和IO性能要压测。第三种是长期最优解后面我会单独说。5. 压测结果与调优参数改造完成后我在测试环境做了几轮压测。压测不是为了看一个“上传速度”的数字而是为了回答三个问题分块大小选多少并发数设多少nginx和Tomcat参数怎么配才不会被“分块”这种多请求模型拖垮。5.1 测试环境与文件准备测试环境不复杂一台2核4G的虚拟机跑Tomcat 8.5前面挂nginx 1.18客户端是Chrome。测试文件我用命令生成Linux用ddWindows可以用fsutil file createnew都很快dd if/dev/zero oftest_1g.bin bs1M count1024注意不要用/dev/urandom生成大文件随机数的生成速度远低于零字节测试文件还没生成完时间都浪费了。5.2 不同分块大小和并发数的对比我分别测了2MB、5MB、10MB、20MB四种分块大小以及并发2、3、5三种情况。数据整理出来比较能说明问题但也要说明不同网络环境下的绝对值差异会很大重点是趋势。分块大小并发数1GB文件耗时参考请求数量观察结果2MB2约5分30秒512个稳定但请求太多Tomcat解析开销高5MB3约4分20秒205个稳定适合弱网环境10MB3约3分50秒103个推荐组合网络与请求数平衡10MB5约4分10秒103个偶发超时需要重试线程占用高20MB3约3分40秒52个速度最快但弱网下失败后重试成本高分块太小比如1MB或2MB单个请求本身很快但总请求数量暴涨Tomcat对每个multipart请求都要做解析和临时文件写入CPU和磁盘小文件操作成本反而拖慢整体速度。分块太大比如50MB几乎退化成整包上传网络一旦抖动重试一个50MB分块很痛苦。综合下来普通办公网络选10MB、并发3最稳。5.3 nginx与Tomcat参数调整分块上传也并不是“只要分块了其他参数就不用调了”。nginx和Tomcat层面的临时目录、缓冲区大小仍然会影响单块的传输速度。nginx里我给的配置是这样client_max_body_size 20m; proxy_request_buffering off; proxy_read_timeout 300s; proxy_send_timeout 300s;client_max_body_size只要比前端分块大小大理论上20MB就够。但有些用户可能在浏览器里直接拖了一个超过限制的文件进来或者外部脚本绕过前端整包POST所以如果你想留一点兜底空间可以调到200m甚至0表示不限制但后端Servlet的maxRequestSize要配合控制避免有人发超大请求攻击。重点说下proxy_request_buffering off。很多教程不会提这个但nginx默认会先把请求体缓冲到临时文件等完整收到后才转发给后端。对单个10MB分块影响不大但如果分块并发是55个10MB的请求同时被缓冲磁盘IO会多出一层负担。关掉缓冲区后nginx把请求体流式转发给Tomcat上传体验明显更顺滑。Tomcat的Connector配置这样改Connector port8080 protocolHTTP/1.1 maxPostSize20971520 maxSwallowSize-1 connectionTimeout60000 maxThreads200 /maxPostSize设为20MB匹配单块大小maxSwallowSize-1是允许异常中断时吞掉任意大小的残余数据。如果你不设maxSwallowSize默认只有2MB左右用户传一半断了Tomcat为了复用连接会尝试把剩余数据读完超过限制就抛异常前端看到的表现就是连接被重置。这个参数太隐蔽了我一开始根本没注意到是看Tomcat localhost日志里疯狂刷SEVERE才定位到的。6. 从分块上传到更高阶方案分块上传解决了我项目里“JSP页面能传大文件和多附件”的问题但它并不是终点。随着附件越来越大、上传量越来越多你会发现业务Tomcat既要跑页面又要中转文件始终是个瓶颈。6.1 断点续传的工程化经验断点续传最能提升用户信心。我在前端实现时不仅传分块还在每个分块请求成功时把chunkIndex存到localStorage的一份Map里{ fileId: xxx, uploaded: [0,1,2,3] }重新选择同一个文件后前端先请求GET /upload/progress?fileIdxxx拿后端返回的已传分块索引两个集合求差集只传缺的部分。这个功能上线后用户再也不用因为一次网络闪断从头传起。不过要留意这是前端层面的语义后端仍然要保证分块落盘和索引记录准确两边对不齐的话续传就是续了个寂寞。6.2 对象存储把分块逻辑交给MinIO这类系统如果项目有条件引入对象存储我会非常建议把分块上传的“分块管理”责任从业务代码里挪出去。以MinIO为例它提供S3标准的Multipart Upload接口前端或后端调用CreateMultipartUpload、UploadPart、CompleteMultipartUpload分块的合并由MinIO自己完成应用层不需要维护临时目录也不需要写合并线程。JSP后端在其中的角色就变成了“签发行为人”校验用户身份后生成上传凭证或者预签名URL前端拿这个URL直传MinIO/OSS。业务Tomcat不经过大文件流量只处理元数据和回调。这正是很多人搜“minio上传很多大文件方案”时想要的答案。迁移的工作量其实不大JSP页面里的切片逻辑可以复用只是把请求地址从/upload/chunk换成对象存储的UploadPart地址后端从“读流写文件”变成“管理分块状态”。6.3 前端Worker与大文件下载的延伸思考前端如果要用Web Worker做切片和MD5计算我建议规划好Worker脚本的目录。在传统JSP项目打包war后Worker脚本放在webapp/static/js/worker.js页面里new Worker(static/js/worker.js)即可。但要注意有些容器对静态资源路径的处理和开发环境不一致上线前一定要用部署后的war包路径再验证一次。和大文件上传对应的还有大文件下载。很多人搜“大文件下载链接”“大文件导出”本质是把服务端文件以流式响应写给浏览器或者让浏览器走断点续传/Range请求。分块上传的思路反过来用就成了分块下载。这套方法在传和导两个方向都适用所以把分块概念吃透之后后续做报表大文件导出也能少走弯路。最后说一个我个人的习惯凡是给传统项目做上传改造我都会在测试环境里故意用弱网工具模拟丢包然后试一次中途断电、一次磁盘写满、一次nginx重启。分块上传逻辑如果能在这些极端情况下不留下脏数据和错乱文件才算真正合格。很多代码看起来没问题一遇到“最后一个分块还没到但合并线程已经跑了”这种边界情况就翻车这些坑都是要在上线前用真实故障去逼出来的。
返回列表