首发于 Java
MultipartFile的transferTo()方法详解

MultipartFile的transferTo()方法详解

背景

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

问题

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

2022-12-16 10:59:41.971 ERROR 23000 --- [nio-8080-exec-1] 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.3170522581386951881\work\Tomcat\localhost\ROOT\upload_657023da_e3c3_461e_ae98_639e45ed79cd_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]

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 recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
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);