blog-upload-path-issue.md 7.1 KB

Spring Boot 文件上传路径血泪史:一次因相对路径引发的"灵异"Bug

引言

在开发校园闲置物品交易平台时,遇到了一个令人抓狂的 Bug:上传的图片明明保存在了指定目录,但前端访问时却总是 404 或 FileNotFoundException。错误日志指向了一个匪夷所思的路径:

java.io.FileNotFoundException: /tmp/tomcat.8090.17561196457261115842/
  work/Tomcat/localhost/ROOT/./upload/dcb42730-0298-43f8-982f-3f37792ddbff.png
  (No such file or directory)

Tomcat 为什么会跑到 /tmp/ 目录下去找上传文件?本文详细记录了问题的排查过程、根本原因和最终解决方案。


一、现象复现

应用基于 Spring Boot 2.x + MyBatis,通过内嵌 Tomcat 运行在 localhost:8090。功能很简单:

  • 用户上传图片,后端保存到 ./upload/ 目录
  • 前端通过 /upload/{文件名} 访问图片

上传接口正常返回了图片 URL(如 /upload/xxx.jpg),但浏览器访问该 URL 时报错,后端日志输出上述 FileNotFoundException。


二、代码现状

项目中与文件上传相关的代码有两处:

2.1 文件保存 — FileUploadUtil.java

@Component
public class FileUploadUtil {

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

    public String upload(MultipartFile file) {
        // ... 校验逻辑 ...
        String filename = UUID.randomUUID() + ext;
        File dest = new File(uploadDir, filename);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        file.transferTo(dest);
        return "/upload/" + filename;
    }
}

2.2 静态资源映射 — WebMvcConfig.java

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/upload/**")
                .addResourceLocations("file:./upload/");
    }
}

2.3 配置文件 — application.yaml

upload:
  dir: ./upload

spring:
  mvc:
    static-path-pattern: /upload/**
    static-locations: file:./upload/

第一眼看过去,所有路径都统一写着 ./upload/,似乎没有任何问题。


三、排查过程

第一步:确认文件是否真的存在

$ ls -la ./upload/
total 24
drwxr-xr-x  2 root root  4096 ...
-rw-r--r--  1 root root 12345 ... dcb42730-0298-43f8-982f-3f37792ddbff.png

文件确实在项目根目录的 ./upload/ 下,没有丢失。

第二步:分析错误路径

错误信息中,Tomcat 找的是:

/tmp/tomcat.8090.17561196457261115842/work/Tomcat/localhost/ROOT/./upload/xxx.png

而我们的文件实际在:

/home/user/project/upload/xxx.png

路径完全不一致。

第三步:定位问题来源

/tmp/tomcat.8090.xxxxx/ 是 Spring Boot 内嵌 Tomcat 运行时创建的临时工作目录。这意味着 file:./upload/ 这个相对路径,被 Tomcat 以自己的临时工作目录为基准进行了解析,而不是以项目根目录为基准。


四、根因分析

这是典型的 相对路径解析上下文不一致 问题。

组件 路径写法 解析基准 结果
FileUploadUtil new File("./upload", name) JVM user.dir(项目根目录) ✅ 文件保存到项目根目录下的 upload/
WebMvcConfig file:./upload/ Tomcat 临时工作目录 ❌ 解析到 /tmp/tomcat.xxx/./upload/
application.yaml file:./upload/ Tomcat 临时工作目录 ❌ 同上

Spring Boot 应用启动时,./ 这个相对路径在不同阶段指向不同的"当前目录":

  • 应用代码层(new File()):以 JVM 启动时的 user.dir 为基准
  • 内嵌 Tomcat 静态资源处理层(ResourceHandler / static-locations):以 Tomcat 自己的工作目录为基准

两者不一致,导致文件写到了 A 处,读取时却从 B 处找,自然找不到。

注意:并非所有环境都会复现此问题。在部分 IDE 或直接 java -jar 运行时,两者可能恰好一致,让人误以为配置没问题。一旦部署到特定环境(如通过 spring-boot:run 插件启动、或部署到外部 Tomcat),问题就会暴露。


五、解决方案

核心思路:使用绝对路径统一两边的解析基准

利用 ${user.dir} 这个 JVM 系统属性,固定指向项目根目录,让所有组件都使用同一个绝对路径。

1. 修改 application.yaml

upload:
  dir: ${user.dir}/upload

移除与 WebMvcConfig 重复的静态资源配置(二选一即可,重复注册可能导致优先级混乱):

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 50MB
  # 下面的配置已移除,由 WebMvcConfig 统一管理
  # mvc:
  #   static-path-pattern: /upload/**
  #   static-locations: file:./upload/

2. 修改 WebMvcConfig.java

注入配置属性,拼接绝对路径:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Value("${upload.dir}")
    private String uploadDir;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/upload/**")
                .addResourceLocations("file:" + uploadDir + "/");
    }
}

3. FileUploadUtil.java 无需修改

它已经在读取 @Value("${upload.dir}"),application.yaml 改为绝对路径后自动生效。


六、修复后的效果

修复后,三个组件都使用同一个绝对路径 ${user.dir}/upload,无论 Tomcat 的工作目录是什么,文件写入和读取都指向同一个位置:

# user.dir = /home/user/project
# upload.dir = /home/user/project/upload
  • FileUploadUtil 保存 → /home/user/project/upload/xxx.png ✅
  • WebMvcConfig 读取 → file:/home/user/project/upload/ ✅
  • 前端访问 → http://localhost:8090/upload/xxx.png ✅

七、经验教训

  1. 永远不要在 Spring Boot 中使用 ./ 相对路径 — 不同组件的解析基准可能不同,应使用 ${user.dir} 或硬编码绝对路径
  2. 避免重复配置 — spring.mvc.static-locations 和自定义 WebMvcConfigurer.addResourceHandlers 功能重叠,选其一即可,否则容易互相干扰
  3. Spring Boot 配置属性可以引用 JVM 系统属性 — ${user.dir}、${java.home} 等系统属性可以在 application.yaml 中直接使用,非常方便
  4. Bug 复现的环境依赖性 — 有时候不是因为代码"对了",而是因为环境"恰好对了"。跨环境运行时,这类路径问题就会暴露出来

结语

一个 ./ 引发的 Bug,排查起来说难不难,说简单也花了大半天。归根结底是对 Spring Boot 内部路径解析机制的理解不够深入。如果你也遇到了类似的"文件保存了但访问不到"的问题,不妨检查一下项目中是否存在四处散落的相对路径 — 把它们统一改为绝对路径,问题很可能就迎刃而解了。


本文发布于 2026-06-27