# 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` ```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` ```java @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:./upload/"); } } ``` ### 2.3 配置文件 — `application.yaml` ```yaml upload: dir: ./upload spring: mvc: static-path-pattern: /upload/** static-locations: file:./upload/ ``` 第一眼看过去,所有路径都统一写着 `./upload/`,似乎没有任何问题。 --- ## 三、排查过程 ### 第一步:确认文件是否真的存在 ```bash $ 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` ```yaml upload: dir: ${user.dir}/upload ``` 移除与 `WebMvcConfig` 重复的静态资源配置(二选一即可,重复注册可能导致优先级混乱): ```yaml spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB # 下面的配置已移除,由 WebMvcConfig 统一管理 # mvc: # static-path-pattern: /upload/** # static-locations: file:./upload/ ``` #### 2. 修改 `WebMvcConfig.java` 注入配置属性,拼接绝对路径: ```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 的工作目录是什么,文件写入和读取都指向同一个位置: ```bash # 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*