Spring MultipartFile.transferTo 相对路径的坑

1 阅读2分钟

背景

做博客的图片上传功能时,遇到一个 FileNotFoundException:

java.io.FileNotFoundException:
/private/var/folders/2s/jm22wykx6mg6lnbbvhnt_78m0000gn/T/tomcat.8080.7036948421786898818/
work/Tomcat/localhost/ROOT/./uploads/2026/10/03/xxx.jpg
(No such file or directory)

奇怪的是:我的代码里写的路径是 ./uploads,为什么报错路径跑到了 Tomcat 的临时目录里去了?

现象

上传接口代码:

@Value("${app.upload.dir:./uploads}")
private String uploadDir;

@PostMapping("/image")
public Result<String> uploadImage(@RequestParam("file") MultipartFile file) {
    File dir = new File(uploadDir, datePath);
    if (!dir.exists() && !dir.mkdirs()) {
        throw new BizException(500, "无法创建上传目录");
    }
    File dest = new File(dir, filename);
    file.transferTo(dest);   // ← 这行报错
    ...
}

配置:

app:
  upload:
    dir: ./uploads

按理说文件应该存到项目根目录下的 uploads/,但报错路径却是:

/private/var/folders/.../Tomcat/localhost/ROOT/./uploads/...

跑到了 Tomcat 的临时目录里去了。

原因

关键在 MultipartFile.transferTo(File dest) 这个方法的行为。

看 Spring 官方文档:

If the destination file is a relative path, it is resolved relative to the temporary directory of the Servlet container.

翻译一下:如果 dest 是相对路径,它会被解析到 Servlet 容器的临时目录里。

而我传的是 new File("./uploads", datePath),是相对路径。

  • dir.mkdirs() 会成功(因为相对路径在本进程的 user.dir 下能建成功);
  • 但 file.transferTo(dest) 里的 dest 会被 Tomcat 解析到临时目录;
  • 结果:目录建在了 A 处,文件写到了 B 处——B 处目录不存在,报 FileNotFoundException。

解决

把相对路径转成绝对路径再传给 transferTo:

import java.nio.file.Path;
import java.nio.file.Paths;

// 转成绝对路径
Path rootDir = Paths.get(uploadDir).toAbsolutePath().normalize();
Path targetDir = rootDir.resolve(datePath);
Path targetFile = targetDir.resolve(filename);

Files.createDirectories(targetDir);
file.transferTo(targetFile.toFile());   // 现在 targetFile 是绝对路径,安全

同理,静态资源映射也要用绝对路径:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    String absolute = Paths.get(uploadDir).toAbsolutePath().normalize().toString();
    registry.addResourceHandler("/uploads/**")
            .addResourceLocations("file:" + absolute + "/");
}

两处都改成绝对路径后,上传成功、静态资源也能访问。

举一反三

所有"往磁盘写文件"的地方,都要用绝对路径:

场景建议
文件上传Paths.get(dir).toAbsolutePath()
日志文件配置里写绝对路径
导出 Excel临时文件用 Files.createTempFile()
生成图片目标路径必须是绝对路径

一个最简单的判断标准:

  • 代码里如果出现 new File("./xxx")、new File("../xxx") 这种相对路径,一律改成绝对路径;
  • 绝对路径 = 项目根目录 + 相对路径,或配置里直接写绝对路径。

总结

  1. MultipartFile.transferTo 对相对路径的处理是"相对 Servlet 容器临时目录"——和大多数人的直觉相反;
  2. 所有往磁盘写文件的代码,一律用绝对路径;
  3. 文件上传、静态资源映射、导出——三处都容易踩这个坑;
  4. 排查方法:看报错路径,如果路径里出现了 /tmp/、/private/var/folders/、Tomcat/localhost——基本就是相对路径被解析到临时目录了。

参考

本文是"踩坑系列"第 3 篇: 第 1 篇:Element Plus el-dialog 在 sticky header 里弹不出的坑 第 2 篇:MySQL TINYINT(1) 被 MyBatis-Plus 映射成 Boolean 的坑