Spring Boot内嵌Jetty大文件上传堆溢出问题及流式上传方案咨询
我之前刚好处理过类似的5GB大文件上传场景,你的问题核心很明确:OOM发生在Spring参数绑定阶段,也就是Jetty在解析Multipart请求时,把整个文件都加载到内存里了,根本没等到你的controller方法去处理流。下面分后端配置、前端调整和额外优化点来一步步解决:
一、后端核心配置:让Jetty用磁盘存储Multipart文件
Spring Boot默认用Jetty的Multipart解析器,但默认配置下,小文件会存在内存,大文件才会写到磁盘——但你的错误日志显示,Jetty还是在把文件往内存里塞(ByteArrayOutputStream在扩容),所以必须强制让Jetty把超过阈值的文件直接写到磁盘:
1. 基础配置(application.yml)
先在配置文件里设置Multipart的参数,覆盖默认值:
spring: servlet: multipart: enabled: true max-file-size: 5GB # 单个文件最大大小 max-request-size: 5GB # 整个请求最大大小 file-size-threshold: 1MB # 超过1MB就写到磁盘,而不是内存 location: ${java.io.tmpdir} # 临时文件存储目录,用系统临时目录就行
2. 自定义Jetty Multipart配置(可选但更稳妥)
如果上面的配置还没生效,可能是Jetty的默认MultiPartConfig没被覆盖,这时候需要自定义Jetty服务器配置:
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.MultiPartConfig; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.unit.DataSize; @Configuration public class JettyMultipartConfig { @Bean public JettyServerCustomizer multipartConfigCustomizer() { return server -> { // 获取Jetty的HTTP配置 ServerConnector connector = (ServerConnector) server.getConnectors()[0]; HttpConfiguration httpConfig = connector.getDefaultHttpConfiguration(); // 构建MultiPart配置 MultiPartConfig.Builder multipartBuilder = new MultiPartConfig.Builder() .setMaxFileSize(DataSize.ofGigabytes(5).toBytes()) .setMaxRequestSize(DataSize.ofGigabytes(5).toBytes()) .setFileSizeThreshold(DataSize.ofMegabytes(1).toBytes()) .setLocation(System.getProperty("java.io.tmpdir")); httpConfig.setMultiPartConfig(multipartBuilder.build()); }; } }
二、Controller代码优化:确保流式处理
你原来的controller代码其实已经是流式写文件了,但要确保MultipartFile的输入流是从磁盘读取,而不是内存。另外,可以进一步简化代码,用Spring的工具类或者Java NIO来减少手动buffer操作:
import java.nio.file.Files; import java.nio.file.StandardCopyOption; @PostMapping("import/upload") public void importUpload( @RequestParam("file") @ApiParam("Файл с данными для импорта") MultipartFile file) throws Exception { // 直接用Files.copy流式复制,避免手动写buffer逻辑 try (InputStream inputStream = file.getInputStream()) { Files.copy(inputStream, zipArchive.getFile().toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception ex) { throw new RuntimeException("Ошибка при загрузке файла: " + ex.getMessage(), ex); } }
这里的关键是:绝对不要调用MultipartFile.getBytes(),这个方法会把整个文件读到内存,一定要用getInputStream()来流式处理。
三、前端调整:确保浏览器流式上传
你的Ant Design Upload代码基本没问题,但可以加几个小优化,确保浏览器不会把整个文件加载到内存:
- 不要手动读取文件内容(比如用
FileReader),AntD的Upload默认会直接把File对象加到FormData里,浏览器会流式上传,不会占内存。 - 显式指定请求头,避免自动适配出问题:
<Upload.Dragger style={{paddingLeft: '10px', paddingRight: '10px'}} action="api/server/import/upload" name={name} onChange={this.changeFileState} showUploadList={false} accept=".zip" headers={{ 'Content-Type': 'multipart/form-data' }} > <p className="ant-upload-drag-icon"> <Icon type={icon}/> </p> <p className="ant-upload-text">{label}</p> {this.fileName ? ( <div> <p className="ant-upload-hint">{this.fileName}</p> <Progress size="small" status={this.status} percent={this.progress}/> </div> ) : ( <p className="ant-upload-hint">{hint}</p> )} </Upload.Dragger>
四、备选方案:分片上传(如果流式上传仍有问题)
如果遇到网络不稳定或者超大文件(比如10GB+),可以考虑分片上传:
- 前端把文件切成多个小分片(比如100MB/片),依次上传,后端接收后合并。
- AntD的Upload支持分片上传,只要配置
chunk和chunkSize参数:
<Upload.Dragger // 其他原有配置 chunk={true} chunkSize={1024 * 1024 * 100} // 100MB每片 > {/* 你的UI内容 */} </Upload.Dragger>
后端需要处理分片的接收、存储和合并逻辑,这会增加复杂度,但能更好地应对极端场景。
最后验证点
- 上传大文件时,观察系统临时目录(
java.io.tmpdir)是否有临时文件生成,且文件大小逐步增长,而不是直接跳到5GB。 - 监控JVM堆内存,确保上传过程中堆内存不会出现飙升。
内容的提问来源于stack exchange,提问作者Lukyanov Mikhail

