V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
shiyu6226
V2EX  ›  程序员

求助, Java 接口上传 2G 以上大文件 EOFException: null

  •  
  •   shiyu6226 · 2022-12-09 11:52:30 +08:00 · 4081 次点击
    这是一个创建于 722 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在写一个 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());
        }
    }
    
    30 条回复    2022-12-10 18:18:38 +08:00
    xiaohundun
        1
    xiaohundun  
       2022-12-09 13:24:57 +08:00
    应该不是文件大小配置的问题,因为不是这个异常,这个异常应该看看是不是网络的问题
    zsj1029
        2
    zsj1029  
       2022-12-09 13:41:02 +08:00   ❤️ 1
    大文件请用流式传输,普通文件操作 2g 会占用 2g 物理内存,大了会炸
    Stendan
        3
    Stendan  
       2022-12-09 13:55:22 +08:00
    koloonps
        4
    koloonps  
       2022-12-09 14:01:45 +08:00
    不用流式传输就需要在客户端把文件切好分开上传,不然分分钟 OOM
    shiyu6226
        5
    shiyu6226  
    OP
       2022-12-09 14:29:12 +08:00
    @xiaohundun 这个是用在内网环境下的,网络应该是不受影响
    shiyu6226
        6
    shiyu6226  
    OP
       2022-12-09 14:29:37 +08:00
    shiyu6226
        7
    shiyu6226  
    OP
       2022-12-09 14:33:08 +08:00
    @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();
    }
    shiyu6226
        8
    shiyu6226  
    OP
       2022-12-09 14:37:12 +08:00
    @Stendan
    感谢,目前看来好像只有分片上传可行了
    Xhack
        9
    Xhack  
       2022-12-09 15:52:18 +08:00
    try-with-resource
    zsj1029
        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.
    zsj1029
        11
    zsj1029  
       2022-12-09 16:55:37 +08:00   ❤️ 1
    上面的简易文件流处理
    另外
    EOFException 的问题: 从文件中读取对象的时候,如何判断是否读取完毕。jvm 会给抛出 EOFException ,表示的是,文件中对象读取完毕。所以呢,你在判断是否读取结束的时候,捕获掉这个异常就可以,是捕获不是抛出。

    重要的说三次,是捕获,捕获,捕获!
    DinnyXu
        12
    DinnyXu  
       2022-12-09 17:27:52 +08:00
    几年开发了?控制层的代码写成这样?哈哈
    V2Axiu
        13
    V2Axiu  
       2022-12-09 17:35:43 +08:00
    大文件还是分片吧。还能续传多好
    eatFruit
        14
    eatFruit  
       2022-12-09 17:40:30 +08:00
    @DinnyXu 那请问要怎么写才算得上“好”呢
    shiyu6226
        15
    shiyu6226  
    OP
       2022-12-09 19:09:58 +08:00 via iPhone
    @DinnyXu
    控制层不是越简单越好嘛…
    aguesuka
        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;
    aguesuka
        17
    aguesuka  
       2022-12-09 20:20:07 +08:00
    对了打印日志的正确姿势是 log.error(e.getMessage(), e);这样才会打印异常栈
    guyeu
        18
    guyeu  
       2022-12-09 20:38:00 +08:00
    @aguesuka 假装这是正确姿势。。。e.getMessage()是可以返回 null 的
    bertieranO0o
        19
    bertieranO0o  
       2022-12-09 20:56:15 +08:00
    @DinnyXu 老哥求指点这个哪里有问题😂
    DinnyXu
        20
    DinnyXu  
       2022-12-09 22:05:19 +08:00
    @eatFruit
    @shiyu6226
    @bertieranO0o
    不是代码的问题,是这种上传文件还是很老式的写法,现在很多上传文件或者图片都是使用 OSS 搭配使用,2G 的文件压缩一下上传到云端是很快的,而且还可以进行分片,将 2G 文件压缩后分成 N 个子压缩包进行断点续传,最终传到服务端的是一个或多个 URL ,后端再进行异步处理。
    DinnyXu
        21
    DinnyXu  
       2022-12-09 22:09:45 +08:00
    说一个大概的思路,前端检测上传的文件大小,根据文件大小进行压缩分片,比如 1G 的文件,分成 1024 个压缩包,每个压缩包 1M ,然后进行轮询请求上传到 OSS ,将 1024 个文件放到一个文件夹, 获取 OSS 的文件夹路径传给后端,由后端根据此路径读取 OSS 上传的所有分片文件,然后进行异步处理组装。
    shiyu6226
        22
    shiyu6226  
    OP
       2022-12-09 23:29:12 +08:00 via iPhone
    @aguesuka
    异常不在 service 也不在 controller ,我观察到的情况是 tomcat 缓存目录正在接收大文件时 到 2 个多 G 就中断 出异常了。
    堆栈日志其实是打印全的,但是有点多,我就只发了主要的
    aguesuka
        23
    aguesuka  
       2022-12-10 00:04:05 +08:00
    @guyeu null 就 null 呗, 异常栈都打出来了
    aguesuka
        24
    aguesuka  
       2022-12-10 00:14:28 +08:00
    @guyeu e.getMessage 改成啥都可以, 重点是要把 error 当第二个参数传进去
    aguesuka
        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
    aguesuka
        26
    aguesuka  
       2022-12-10 02:00:27 +08:00
    multipart 文件在转成 MultipartFile 的时候必须读完整个流, 所以会缓存到内存或硬盘里, 估计还有别的配置到上限了.
    bertieranO0o
        27
    bertieranO0o  
       2022-12-10 02:28:45 +08:00
    @aguesuka 一般不建议文件大小设置无限大,给个业务范围内的合理上限值即可
    bertieranO0o
        28
    bertieranO0o  
       2022-12-10 02:31:19 +08:00
    @DinnyXu 根本不需要这么复杂,就拿你举例的 OSS 来说,记得 19 年的时候 OSS 都已经有很成熟的支持分片上传的 API 了
    DinnyXu
        29
    DinnyXu  
       2022-12-10 15:28:29 +08:00
    @bertieranO0o 你也知道你调用的是 API 哈,我说的是逻辑思维,API 谁不会调
    bertieranO0o
        30
    bertieranO0o  
       2022-12-10 18:18:38 +08:00
    @DinnyXu 想听听你的“异步处理组装”的逻辑
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   992 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 20:00 · PVG 04:00 · LAX 12:00 · JFK 15:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.