【SpringBoot】MultipartFile的transferTo()方法详解

背景

我们在编写Spring Boot应用中,常会遇到文件上传问题,Spring Boot Web提供了MutipartFile的文件支持,具体和File的区别可自行上网搜索查阅。

问题

使用过程中,大部分同学可能会遇到当调用的tansferTo()方法后,再次获取file.getInputStream()方法时,就会报临时文件异常,如:


2022-12-16 11:42:21.994 ERROR 14780 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.io.FileNotFoundException: C:\Users\lenovo\AppData\Local\Temp\tomcat.8080.4014709126083758502\work\Tomcat\localhost\ROOT\upload_ac92d6c4_3e50_41b2_85a7_f1a3bd33ddd8_00000000.tmp (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method) ~[na:1.8.0_322]
	at java.io.FileInputStream.open(FileInputStream.java:195) ~[na:1.8.0_322]
	at java.io.FileInputStream.<init>(FileInputStream.java:138) ~[na:1.8.0_322]
	at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.getInputStream(DiskFileItem.java:198) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
	at org.apache.catalina.core.ApplicationPart.getInputStream(ApplicationPart.java:100) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
	at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getInputStream(StandardMultipartHttpServletRequest.java:254) ~[spring-web-5.3.22.jar:5.3.22]
	at com.tanwei.spring.app.controllers.FileController.file(FileController.java:29) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_322]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_322]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_322]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_322]

FileNotFoundException异常不难理解,就是文件找不到了?也就是说tansferTo()可能在传输完成后把临时文件删除了,这是肯定的,但是答案只能说是对一半,我们将一步一步的进行源码分析

源码分析

调用tansferTo()方法,Spring Boot Web默认是调用StandardMultipartHttpServletRequest.StandardMultipartFile.tansferTo()方法,如下所示:

public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
    
    // more code..
    
    private static class StandardMultipartFile implements MultipartFile, Serializable {
        // more code..
        
        public void transferTo(File dest) throws IOException, IllegalStateException {
            this.part.write(dest.getPath());
            if (dest.isAbsolute() && !dest.exists()) {
                FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath()));
            }
        }
    
        public void transferTo(Path dest) throws IOException, IllegalStateException {
            FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));
        }
    }
}

我们主要看一下.tansferTo(File dest)这个方法里的this.part.write(dest.getPath());代码,这里的part实现是ApplicationPart,如下所示:

//
// Source code 
//

public class ApplicationPart implements Part {
    
    // more code ...
    
    public void write(String fileName) throws IOException {
        // 构建一个需要存储的文件
        File file = new File(fileName); 
        // 判断文件的地址是否是一个绝对路径地址,形如D://x.txt,返回true
        if (!file.isAbsolute()) {
            // 如果不是一个绝对路径地址,则在this.location下创建
            // this.location是一个临时文件对象,地址(C:\xx\Temp\tomcat.8080.3170522581386951881\work\Tomcat\localhost\ROOT)
            file = new File(this.location, fileName); 
        }

        try {
            this.fileItem.write(file);
        } catch (Exception var4) {
            throw new IOException(var4);
        }
    }

     // more code ...
}

this.fileItem.write(file);这行代码是主要的核心代码,我们继续跟进去查看一下具体做了什么,如下所示:

//
// Source code ...
//

package org.apache.tomcat.util.http.fileupload.disk;

// imports ...

public class DiskFileItem implements FileItem {
    
    
    // more code ...

    public void write(File file) throws Exception {
        // 判断文件项是否缓存在内存中的,这里我们没设置,一般都是存在上面的临时磁盘中
        if (this.isInMemory()) {
            // more code..
            
        } else {
            // 主要看一下这个代码块
            // 获取文件项的存储位置,即你上传的文件在磁盘上的临时文件
            File outputFile = this.getStoreLocation();
            if (outputFile == null) {
                throw new FileUploadException("Cannot write uploaded file to disk!");
            }

            // 获取文件长度
            this.size = outputFile.length();
            if (file.exists() && !file.delete()) {
                throw new FileUploadException("Cannot write uploaded file to disk!");
            }
            
            // 这里至关重要
            // 之所以不能再调用file.getInputStream()方法,就是在这
            // fileA.renameTo(fileB)方法:
            //    1) 当fileA文件信息(包含文件名、文件路径)与fileB全部相同时,只是单纯的重命名
            //    2) 当fileA文件信息(特别是文件路径)与fileB不一致时,则存在重命名和剪切,这里的剪切就会把临时文件删除,并将文件复制到fileB位置
            // 所以,在调用file.getInputStream()时,file获取的还是原始的文件位置,调用transerTo()方法后(其实调用了renameTo()),原始文件已经不存在了
            // 故而抛出FileNotFoundException异常
            if (!outputFile.renameTo(file)) {
                BufferedInputStream in = null;
                BufferedOutputStream out = null;

                try {
                    in = new BufferedInputStream(new FileInputStream(outputFile));
                    out = new BufferedOutputStream(new FileOutputStream(file));
                    IOUtils.copy(in, out);
                    out.close();
                } finally {
                    IOUtils.closeQuietly(in);
                    IOUtils.closeQuietly(out);
                }
            }
        }
    }
}

后记

以上跟踪源码信息,便可以一目了然的去理解为什么会出现上面的问题描述了。在开发过程中,一个不经意的小错误,便有可能导致全局崩盘,写代码或者说在用代码的时候,我们还是要多去查看源码实现,这样才会少走弯路,完美闭坑。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值