几个月前在一家做央企信息化的公司做技术支持,碰到个特别典型的问题:用户往系统里传一个 800M 的勘察压缩包,传了快半小时,界面卡死,最后报“网络连接被重置”。后端日志里躺着 java.lang.OutOfMemoryError: insufficient memory,Tomcat 线程池被打满,前端页面上传控件提示“不能装载”之类的一堆废话。折腾了几天后我把这套上传逻辑整体推倒重做,才意识到问题根本不在某一行代码,而是整条链路的方案选错了。
这篇文章就是想把那段复盘沉淀下来。如果你也负责 Java Web 系统,且经常要接收几百 M 甚至几个 G 的文件,比如工程图纸、审计底稿、影像资料、ERP 批量导入数据,那这套思路基本可以直接抄。重点会放在:为什么普通一次性上传在大型组织内网里走不通,分片上传该怎么拆,Java 后端怎么写才不会 OutOfMemoryError,断点续传、秒传、临时文件回收这些细节怎么落地。
先把这个项目的环境交代清楚。单位内部有一套老的 Java 管理系统,部署在 Tomcat 上,用户主要用 Windows 办公电脑,浏览器有些是 Chrome 内核的国产浏览器,但打开系统默认进入 IE 兼容模式。部门之间通过办公网互访,出口有统一的接入网关,Web 应用前面还有一层 Nginx 做转发。这套组合是很多央企系统里很常见的拓扑,也是大文件上传最容易出问题的拓扑。
1. 从一次真实的崩溃开始:一份800M的压缩包把老系统弄瘫了
1.1 崩溃现场发生了什么
最初这个系统的上传功能是一个很普通的 HTML <input type="file"> 加一个 Servlet 接口。前端把整个文件放到 FormData 里一次性提交,后端拿到的直接是 MultipartFile,然后调用 getBytes() 把整个文件读成 byte[],再往服务器磁盘写。处理小文件没问题,但一旦出现几百兆的大文件,很快就出事了。
页面表现是:点击上传后浏览器转圈,进度条不走,等十几分钟后弹出一个错误。服务端表现是:JVM 老年代内存持续上涨,GC 后内存回收不动,紧接着出现 insufficient memory,Tomcat 的线程池里好多个线程都卡在文件上传的读取操作上。用户重试两次后,整个系统响应都变慢了。
当时我第一反应是“内存不够就加内存”。把应用的 JVM 参数从 -Xmx1g 调到 -Xmx4g,重启后确实好了一阵,但再传大文件时问题依旧。后来才意识到,服务器的物理内存毕竟是有限的,不可能靠堆大小去硬扛“把整个文件加载进内存”这种写法。真正该做的是:让文件内容以流的形式经过 JVM,而不是在内存里留下一整份拷贝。
1.2 问题延伸:前端控件依赖后患无穷
排查过程中还翻出一个历史遗留因素。该系统之前用过一种基于浏览器插件的大文件上传控件,控件通过 ActiveX 或者客户端的某些组件机制去读写本地文件。这类控件的典型问题是跟浏览器绑定得很死,要求老版本内核,而且换浏览器或调整安全策略后经常出现“不能装载”“请设置浏览器”之类让人摸不着头脑的提示。
既然新方案要彻底替换掉对控件的依赖,我最初的思路就比较明确了:不能再用插件,不能依赖浏览器特有接口,全部用标准 HTML5 的文件 API 配合 HTTP 请求来做。这种方式天然兼容现代浏览器,也不受控件安装环境影响。只要浏览器支持 File.slice(),我们就能在 JS 里自己把大文件切成小片段,逐个上传。这样即使用户的浏览器是老版本内核,只要支持最基本的 ES5 语法和 XHR 对象,依然能用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 被忽略的环境约束:央企网络和浏览器其实决定了方案选型
2.1 网络不是“带宽不够”,而是“连接不可靠”
很多人以为内网带宽够大,大文件传输不应该有问题。实际上内网环境有很多隐性限制。接入层网关往往对单个 HTTP 连接的空闲时间有限制,一旦超过一定时间没有数据流动,连接就会被掐断;Nginx 转发层默认也可能设置了读取超时时间;部分中间件还会在请求体过大时直接返回 413 或干脆断开。这就意味着一个 800M 文件如果按一整 Request 传,只要中间有几十秒的抖动、网络闪断,整个传输就失败了,用户必须从头再来。
分片上传的核心价值就在这里:把一个大任务拆成很多个可以独立成功的小任务。每一片都是几十秒内能传完的小请求,即使某一片失败,只需要重传那一片,而不是整个文件。这就像搬家时不会把一卡车书一次性扛上楼,而是分成一箱一箱地搬,哪箱碎了只需补哪箱。
2.2 浏览器环境决定了你不能随便用新特性
央企总部和下属单位用的浏览器并不统一。有人用很老的 IE,有人用国产浏览器兼容模式,有人用 Chrome。但有个共同点:都不会因为你做一个页面就同意给所有终端统一升级。
所以前端的实现要尽量稳,用到的最核心能力是 File 对象的 slice() 方法。这个 API 在 IE10 之后就有支持,国产浏览器的兼容模式一般也能处理。数据传输不走 WebSocket,只用普通的 XHR 或者 fetch()。后端甚至不需要关心前端用的什么框架,只要把 HTTP 接口约定清楚就行。
另外一个额外好处是,用 HTTP 分片传输可以绕过很多老控件对本地文件的独占式读取。用户不再需要安装任何客户端,只要浏览器允许选择文件,后续的上传过程对就是一个普通的 HTTP PUT 或 POST 请求。用户层面的操作难度显著降低了。
