在JAVA web应用里折腾超大附件上传,几乎是每个后端工程师都绕不过去的坎。视频文件、日志包、设计稿压缩包、数据库备份文件,动不动就是几个G起步。你要是直接用传统的multipart/form-data方式一次性提交,轻则后端内存飙到Red线,重则请求超时直接断连,最后前端报个“网络异常”,用户一脸懵,你还得背锅。解决这个问题的主流方案就是分块上传,也叫分片上传、chunked upload,核心思路很简单:把大文件切成若干小块,逐块上传,最后在后端合并。这篇文章我会从设计思路讲到Java后端的具体实现,再把断点续传、并发控制、资源清理这些坑一个个掰开揉碎,希望能给正在做类似需求的你一些直接能落地的参考。
1. 为什么超大附件必须分块上传
1.1 传统单文件上传的两个硬伤
先聊聊为什么不能直接传大文件。常规的web上传流程里,浏览器把整个文件放进HTTP请求体,后端通过Servlet或Spring MVC的MultipartFile接收。问题在于,这个过程中文件内容通常是先被读到服务端内存或临时目录里的。JVM的堆内存默认一般来说也就几个G,你给Tomcat、Spring Boot容器一分配,实际能留给上传缓冲的更少。如果文件是2个G,一次上传很可能直接触发java.lang.OutOfMemoryError: insufficient memory,这也就是开发时经常看到的那类内存溢出异常。
还有一个硬伤是网络稳定性。大文件传输耗时久,期间只要Wi-Fi闪断、手机切网、服务器重启,整个上传就前功尽弃。用户得从头再来,体验极其糟糕。分块上传恰好能同时解决这两个问题:每块数据量小,内存压力低;上传过程被切分成多个小请求,失败后只需要重传失败的块,不需要重新传整个文件。
1.2 分块上传能解决什么问题
分块上传的核心价值,是让“上传”这件事从一次性大事务变成可重试、可恢复的小批次操作。从用户视角看,上传大文件时能看到进度条稳步前进,断网了重新连上还能接着传,不会因为等待太久而焦虑。从服务端视角看,每一块的数据量可控,请求占用时间短,Tomcat的工作线程不会被长时间占用,整体并发能力也更好。
而且在做文件秒传和断点续传的时候,分块上传几乎是最省事的实现载体。比如我们会在文件上传前先算一个全局MD5,服务端如果发现这个MD5对应的文件已经存在,直接返回“秒传”成功,根本不需要真的传内容。分块机制也会顺带支持块级别的MD5校验,块损坏了就重传那一块,不需要整体重传。
1.3 适用场景与阈值建议
也不是所有场景都非要分块。如果一个文件只有几十兆,老老实实用一次性普通上传就够了,引入分块反而增加复杂度。我一般建议是:单个文件超过200MB,或者你对断点续传有明确要求时,才考虑走分块上传。具体阈值要根据业务来定,比如某些网盘产品把阈值定在128MB,有些定在512MB。我们团队在做一个内部素材平台时,定的规则是小于100MB走普通上传,大于等于100MB自动切换分块上传,实测下来效果比较稳。
除了文件大小,还要考虑用户的使用场景。如果用户大概率是在办公室固定网络下上传,网络很稳定,那么分块上传的必要性没那么迫切;如果用户分布在移动网络、跨地域、弱网环境,那分块加断点续传就是刚需。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分块上传的整体设计思路
2.1 一个可靠分块方案的五个关键环节
一个完整的可落地分块上传方案,绝不是单纯把文件切几份就算完事。我之前见过不少实践,前端切块后一个劲往上发,后端收到就写磁盘,到最后合并时才发现块丢了或者顺序乱了。一个可靠方案至少包含五个环节:创建上传任务、逐块上传、校验块完整性、合并文件、清理临时数据。
创建上传任务时,前端先把文件名、文件大小、分块大小、总块数以及文件唯一标识(一般用MD5或SHA256)发给后端,后端生成一个uploadId(或者叫taskId),把元信息保存下来。逐块上传时,前端带上uploadId、块索引和块数据,后端收到块之后先落盘,并记录当前块的状态。全部块传完后,前端调用一个“完成上传”接口,后端校验所有块是否齐全、大小是否符合预期,然后按顺序合并,合并后再做一次整体校验。校验通过后,把临时块文件清理掉,返回最终文件访问地址。
2.2 前端分块策略:固定分块大小还是动态分块
这件事很多文章会忽略,但恰恰很影响体验。固定分块大小最简单,比如每块5MB或10MB,代码好写,逻辑也直观。但固定分块在弱网环境下有个问题:某个块传输速度很慢,或者某个块反复失败,你就得整块重传。动态分块可以根据网络情况动态调整块大小,比如网速好时用大块,网速差时自动降为小块,但它需要前端有网络检测逻辑,复杂度高不少。
我的建议是,第一版就老老实实用固定分块。对绝大多数业务来说,10MB一个块是比较均衡的选择。分块太小,比如1MB,会导致请求数量过多,网络握手和HTTP头开销占比变大;分块太大,比如50MB,又失去了分块的意义,弱网下某个块失败的成本太高。另外要留意浏览器和服务器对单个请求体大小的限制,通常10MB不会触发Nginx的client_max_body_size默认限制,但如果你改了Nginx配置,也要同步确认。
2.3 后端如何设计上传接口
后端接口设计,我推荐按“三个接口+一个辅助接口”拆分。
POST /upload/init:用于初始化上传任务,前端把文件摘要信息传过来,后端返回uploadId。POST /upload/chunk:用于上传单个分块。请求体里包含uploadId、chunkIndex、chunkData(二进制内容)以及其他校验参数。POST /upload/complete:用于合并文件。前端在全部块上传完毕后调用,后端执行合并和校验。GET /upload/status:可选,用于前端获取已上传的分块列表,支持断点续传。
有些设计方案会把“合并”放在一个异步任务里,complete接口只负责通知后端“块都传完了”,然后立刻返回“合并中”,真正的合并操作丢到消息队列或线程池里做。对大文件来说,合并是一个消耗CPU和磁盘IO的过程,同步执行会让前端等待太久,用户体验不好。不过实现异步合并且要处理好状态轮询,我建议第一版做同步合并,文件大到GB级别时再改成异步。下面具体实现部分我按同步合并讲,更直观,也更容易调试。
3. 核心实现:Java后端处理分块上传
3.1 环境与依赖准备
这里我以Spring Boot 2.x作为基础,JDK 1.8以上就够了。项目里除了spring-boot-starter-web,建议加一个hutool工具包或者commons-io,方便做文件操作。Hutool
