记一次诡异的 Bug:为什么 file.transferTo() 把文件存到了 /tmp 目录?

default

问题现象:文件神秘失踪

事情是这样的,我在写一个文件上传功能时,遇到了一个让人摸不着头脑的问题:

1
2
3
4
5
6
7
@PostMapping("/upload")
public String upload(MultipartFile file) {
    // 明明指定了路径,为什么文件不在这里?
    File floorPlatFile = new File("uploads/" + file.getOriginalFilename());
    file.transferTo(floorPlatFile);
    return "success";
}

代码看起来人畜无害,我满怀信心地启动项目,上传文件,然后去 uploads/ 目录下寻找… 结果空空如也!

更诡异的是,我翻遍了项目目录,甚至用 find 命令全局搜索,都找不到这个文件。它就像凭空消失了一样。

直到我无意间打开了 /tmp 目录,才发现文件居然安安静静地躺在这里!

破案过程:源码之下无秘密

我决定从源码入手,看看 transferTo() 到底做了什么。

找到罪魁祸首

MultipartFile 的默认实现是 StandardMultipartFile,它的 transferTo() 方法源码如下:

1
2
3
4
5
6
7
8
9
// org.springframework.web.multipart.support.StandardMultipartFile
public void transferTo(File dest) throws IOException, IllegalStateException {
    // 关键判断:如果目标文件不是绝对路径
    if (!dest.isAbsolute()) {
        // 这里就是问题的根源!
        dest = new File(this.location, dest.getPath());
    }
    this.part.write(dest.getPath());
}

看到这里,真相大白了!

原来 transferTo() 内部有一个“自作主张”的逻辑:当你传入相对路径时,它会自动拼接一个基础路径(this.location),而这个基础路径正是容器的临时目录!

location 从何而来?

继续追踪 this.location 的赋值过程:

1
2
3
4
5
// StandardMultipartFile 构造器
public StandardMultipartFile(Part part, String location) {
    this.part = part;
    this.location = location;  // 这个 location 是哪里来的?
}

最终发现,这个 location 是在 StandardServletMultipartResolver 中设置的,它取自 MultipartConfigElement 配置的临时目录。如果没有特殊配置,默认就是 Servlet 容器的临时目录:

Tomcat:/tmp/tomcat-xxx/work/Tomcat/localhost/ROOT

Jetty:/tmp/jetty-xxx/work

Undertow:/tmp/undertow-xxx

所以,你传入的相对路径 uploads/file.txt,最终被拼接成了:

1
/tmp/tomcat-xxx/work/Tomcat/localhost/ROOT/uploads/file.txt

为什么这样设计?

这个设计看起来有点“反直觉”,但实际上是有充分考虑的:

安全性:Web 应用通常部署在只读的目录下(如 /usr/local/tomcat/webapps),不允许直接写入

隔离性:多个应用实例不会互相干扰

可靠性:临时目录是可写的,避免权限问题导致异常

解决方案:让文件去它该去的地方

最简单直接的方案,让 File 对象持有绝对路径:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Value("${file.upload.dir:/app/uploads}")
private String uploadDir;

public void uploadFile(MultipartFile file) throws IOException {
    // 确保目录存在
    File dir = new File(uploadDir);
    if (!dir.exists()) {
        dir.mkdirs();
    }
    // 使用绝对路径
    File targetFile = new File(dir, file.getOriginalFilename()).getCanonicalFile();
    file.transferTo(targetFile);
}
Gear(夕照)的博客。记录开发、生活,以及一些不足为道的思考……