最近在写一个 web 端的私人网盘服务,测试发现上传 2G 以上大文件时 后台会出现异常,请问有大佬做过相关的需求吗?怎么解决这类问题?
异常日志如下
ERROR 19593 --- [io-18073-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException: Processing of multipart/form-data request failed. java.io.EOFException] with root cause
java.io.EOFException: null ...
我修改了好多参数也不好使
@Configuration @Slf4j public class EmbeddedTomcatConfig implements WebServerFactoryCustomizer
@Override public void customize(ConfigurableServletWebServerFactory factory) { log.info("Init EmbeddedTomcatConfig..."); ((TomcatServletWebServerFactory)factory).addConnectorCustomizers(new TomcatConnectorCustomizer() { @Override public void customize(Connector connector) { Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); protocol.setMaxConnections(3000); protocol.setMaxThreads(800); protocol.setAcceptCount(200); protocol.setSelectorTimeout(30000); protocol.setSessionTimeout(60000 * 2); protocol.setConnectionTimeout(60000 * 5); protocol.setDisableUploadTimeout(false); protocol.setConnectionUploadTimeout(60000 * 10); } }); }
}
application 参数
spring.servlet.multipart.max-request-size=-1 spring.servlet.multipart.max-file-size=-1 server.tomcat.max-swallow-size=-1 server.tomcat.max-http-form-post-size=-1
控制层
@ResponseBody @ApiOperation(value = "上传文件",notes = "上传文件") @RequestMapping(value = "/FilesUpload",method = RequestMethod.POST) public BaseResponse uploadFiles( @RequestParam(required = true) MultipartFile files, HttpServletRequest request, HttpServletResponse response ) { if (files.isEmpty() || files.getSize() == 0) { response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); return BaseResponse.initErrorBaseResponse("不能上传空文件!"); } try { return BaseResponse.initSuccessBaseResponse(fileExecuteService.uploadFiles(files,request), "操作成功"); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); return BaseResponse.initErrorBaseResponse(e.getMessage()); } }
![]() | 1 xiaohundun 2022-12-09 13:24:57 +08:00 应该不是文件大小配置的问题,因为不是这个异常,这个异常应该看看是不是网络的问题 |
![]() | 2 zsj1029 2022-12-09 13:41:02 +08:00 ![]() 大文件请用流式传输,普通文件操作 2g 会占用 2g 物理内存,大了会炸 |
3 Stendan 2022-12-09 13:55:22 +08:00 |
4 koloonps 2022-12-09 14:01:45 +08:00 不用流式传输就需要在客户端把文件切好分开上传,不然分分钟 OOM |
5 shiyu6226 OP @xiaohundun 这个是用在内网环境下的,网络应该是不受影响 |
7 shiyu6226 OP @zsj1029 @koloonps 实现方式是用的缓冲流写入的,实际运行过程中,内存使用一直保持在 500MB 上下,代码如下 private static boolean writeFileToLocal(String toLocalFilePath, MultipartFile file) throws Exception { boolean flag = false; BufferedOutputStream bufferedOutputStream = null; BufferedInputStream bufferedInputStream = null; try { bufferedInputStream = new BufferedInputStream(file.getInputStream()); bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(toLocalFilePath)); int index; byte[] bytes = new byte[4096]; while ((index = bufferedInputStream.read(bytes)) != -1) { bufferedOutputStream.write(bytes, 0, index); bufferedOutputStream.flush(); } flag = true; } catch (IOException e) { log.error("文件写入失败," + e.getMessage()); if (new File(toLocalFilePath).exists()) { new File(toLocalFilePath).delete(); } throw new Exception(e.getMessage()); } finally { if (bufferedOutputStream != null) { try { bufferedOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (bufferedInputStream != null) { try { bufferedInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } System.gc(); } |
![]() | 9 Xhack 2022-12-09 15:52:18 +08:00 try-with-resource |
![]() | 10 zsj1029 2022-12-09 16:44:59 +08:00 @shiyu6226 ``` @RequestMapping( value = "url", method = RequestMethod.POST ) public void uploadFile( @RequestParam("file") MultipartFile file ) throws IOException { InputStream input = upfile.getInputStream(); Path path = Paths.get(path);//check path OutputStream output = Files.newOutputStream(path); IOUtils.copy(in, out); //org.apache.commons.io.IOUtils or you can create IOUtils.copy } ``` You should close your stream after whole data is written. |
![]() | 11 zsj1029 2022-12-09 16:55:37 +08:00 ![]() 上面的简易文件流处理 另外 EOFException 的问题: 从文件中读取对象的时候,如何判断是否读取完毕。jvm 会给抛出 EOFException ,表示的是,文件中对象读取完毕。所以呢,你在判断是否读取结束的时候,捕获掉这个异常就可以,是捕获不是抛出。 重要的说三次,是捕获,捕获,捕获! |
12 DinnyXu 2022-12-09 17:27:52 +08:00 几年开发了?控制层的代码写成这样?哈哈 |
13 V2Axiu 2022-12-09 17:35:43 +08:00 大文件还是分片吧。还能续传多好 |
16 aguesuka 2022-12-09 20:09:42 +08:00 从你发的代码看不出啥问题. 首先得把问题定位到行, 你发的错误中只有信息而没有异常栈, 因为你的 controller catch 到异常没有打印日志(注意要打印异常栈). 打印异常栈后, 再判断是不是在你的 service 中报的错. 如果是在 service 中, 那直接把 service 改成 ``` private static void writeFileToLocal(String toLocalFilePath, MultipartFile file) throws IOException { try(InputStream inputStream = file.getInputStream()){ Files.copy(inputStream, Path.of(toLocalFilePath), StandardCopyOption.REPLACE_EXISTING); } } ``` 不要直接使用 inputstream, 不要吞异常, 不要 gc; |
17 aguesuka 2022-12-09 20:20:07 +08:00 对了打印日志的正确姿势是 log.error(e.getMessage(), e);这样才会打印异常栈 |
![]() | 19 bertieranO0o 2022-12-09 20:56:15 +08:00 @DinnyXu 老哥求指点这个哪里有问题 |
20 DinnyXu 2022-12-09 22:05:19 +08:00 @eatFruit @shiyu6226 @bertieranO0o 不是代码的问题,是这种上传文件还是很老式的写法,现在很多上传文件或者图片都是使用 OSS 搭配使用,2G 的文件压缩一下上传到云端是很快的,而且还可以进行分片,将 2G 文件压缩后分成 N 个子压缩包进行断点续传,最终传到服务端的是一个或多个 URL ,后端再进行异步处理。 |
21 DinnyXu 2022-12-09 22:09:45 +08:00 说一个大概的思路,前端检测上传的文件大小,根据文件大小进行压缩分片,比如 1G 的文件,分成 1024 个压缩包,每个压缩包 1M ,然后进行轮询请求上传到 OSS ,将 1024 个文件放到一个文件夹, 获取 OSS 的文件夹路径传给后端,由后端根据此路径读取 OSS 上传的所有分片文件,然后进行异步处理组装。 |
22 shiyu6226 OP @aguesuka 异常不在 service 也不在 controller ,我观察到的情况是 tomcat 缓存目录正在接收大文件时 到 2 个多 G 就中断 出异常了。 堆栈日志其实是打印全的,但是有点多,我就只发了主要的 |
25 aguesuka 2022-12-10 00:55:01 +08:00 spring.servlet.multipart.max-file-size: -1 spring.servlet.multipart.max-request-size: -1 亲测 ok, 依赖只有 spring-boot-starter-web:3.0.0 |
26 aguesuka 2022-12-10 02:00:27 +08:00 multipart 文件在转成 MultipartFile 的时候必须读完整个流, 所以会缓存到内存或硬盘里, 估计还有别的配置到上限了. |
![]() | 27 bertieranO0o 2022-12-10 02:28:45 +08:00 @aguesuka 一般不建议文件大小设置无限大,给个业务范围内的合理上限值即可 |
![]() | 28 bertieranO0o 2022-12-10 02:31:19 +08:00 @DinnyXu 根本不需要这么复杂,就拿你举例的 OSS 来说,记得 19 年的时候 OSS 都已经有很成熟的支持分片上传的 API 了 |
29 DinnyXu 2022-12-10 15:28:29 +08:00 @bertieranO0o 你也知道你调用的是 API 哈,我说的是逻辑思维,API 谁不会调 |
![]() | 30 bertieranO0o 2022-12-10 18:18:38 +08:00 @DinnyXu 想听听你的“异步处理组装”的逻辑 |