SpringBoot上传报错temp目录失效?一文教你彻底解决
1. 故障现场一个长假过后文件上传突然“不可用”先还原一下典型的故障场景。部门内部跑着一个基于 SpringBoot 的运营后台平时流量不大尤其是周末和节假日几乎没人访问。假期结束后同事回来上班登录页面正常打开列表查询也正常但一上传附件就抛异常日志里出现这么一段java.io.IOException: The temporary upload location [/tmp/tomcat.8080.123456789/work/Tomcat/localhost/ROOT] is not valid或者 Windows 环境下常见的报错java.io.IOException: The temporary upload location [C:\Users\apps\AppData\Local\Temp\tomcat.8080.123456789\work\Tomcat\localhost\ROOT] is not valid如果打开页面版错误通常是这样Could not parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [...] is not valid很多人第一反应是“重启一下就好了”。重启之后确实大概率好因为 SpringBoot 内嵌 Tomcat 启动时会把临时目录重新创建出来。但这只是把问题延后不是解决。用户没访问的时间一长同样的故障会再来一遍生产环境每出现一次就是一次事故。为什么“长时间未访问”会触发这个问题关键在于 SpringBoot 内嵌 Tomcat 的临时目录默认放在系统临时目录下面。Linux 大多把java.io.tmpdir指向/tmp而/tmp会被操作系统定期清理清理原则是“超过指定天数未访问的文件和目录全部删掉”。业务冷启动后长时间没有写入临时目录里的文件 mtime 一直没更新触发清理条件后整个目录被连根拔起。应用本身还活着内存里的配置也还在但磁盘上的临时目录已经不存在了下次上传文件时自然就报“location is not valid”。这类问题的麻烦点在于它不像内存溢出那样频繁崩溃也不像端口冲突那样启动时报错而是藏在“应用运行了好多天之后才偶现”的场景里属于典型的间歇性故障。排查思路对了十分钟能定位思路不对可能在数据库、连接池、网关那边绕一大圈。2. 根因拆解SpringBoot、Tomcat、系统清理到底各扮演什么角色2.1 三个容易混淆的“临时目录”为了不定位错方向先要把概念理清楚。SpringBoot 内嵌 Tomcat 运行时实际涉及三个级别的临时目录概念默认来源作用JVM 系统临时目录System.getProperty(java.io.tmpdir)JVM 写临时文件的基础目录Linux 一般是/tmpWindows 是%TEMP%Tomcat baseDirSpringBoot 的server.tomcat.basedir默认取java.io.tmpdir内嵌 Tomcat 存放运行时文件的根目录它会在 baseDir 下生成tomcat.端口.随机数目录ServletContext 临时目录Tomcat 启动后在 baseDir 下创建work/Tomcat/localhost/ROOT上传文件的临时落盘位置JSP 编译也用它报错里给出的路径一般是tomcat.端口.随机数/work/Tomcat/localhost/ROOT这一类这正好是 servlet 上下文临时目录。SpringBoot 的spring.servlet.multipart.location如果没有显式配置默认是空字符串最终 Multipart 解析时用的就是 servlet 上下文临时目录。也就是说操作系统的清理程序把整个/tmp/tomcat.8080.xxx目录删掉应用再想往work/Tomcat/localhost/ROOT里写临时文件文件系统上根本不存在这条路径于是直接抛 IOException。这里有个很容易误导人的点报错里写着Tomcat很多人就会跑去查外部 Tomcat 的temp目录和work目录。但你用的如果是 SpringBoot 的java -jar方式启动根本没有独立的 Tomcat 安装目录所有东西都在 JVM 进程内部。不要被路径里的大写Tomcat字样带偏。2.2 真正的“凶手”Linux 的定时清理任务Linux 服务器上清理/tmp的机制常见有两种systemd-tmpfiles以 Ubuntu 18.04、CentOS 7/8、Debian 10 为代表清理服务由定时器触发systemctl list-timers | grep tmp你会看到systemd-tmpfiles-clean.timer。它到点后执行systemd-tmpfiles --clean读取/usr/lib/tmpfiles.d/tmp.conf等配置文件。默认规则一般是q /tmp 1777 root root 10d q /var/tmp 1777 root root 30d意思是/tmp下超过 10 天未访问的文件和目录会被清理/var/tmp是 30 天。注意这里统计的是文件的atime、mtime、ctime中的某一个时间戳具体看实现。应用只要超过 10 天没有上传操作临时目录里的文件就成了“老不死”的清理对象。tmpwatch/cron.dailyCentOS 6、老版 RHEL 上常用/etc/cron.daily/tmpwatch来清理/tmp。它同样默认按天数通常是 240 小时或 10 天清理未访问文件。两种机制删除的都是/tmp下较长时间没动静的内容。内嵌 Tomcat 的临时目录恰好完全满足“未访问时间长”这个条件于是被清掉再正常不过。2.3 Windows 上为什么也会发生Windows 服务器上也有“临时文件清理”问题。Windows 的磁盘清理、安全软件、公司统一分发的主机管理工具都可能清理%TEMP%下超过一定时间的文件。虽然策略和 Linux 不完全一样但现象一致Tomcat 临时目录被外部程序删了应用自身感知不到下次上传时才发现路径失效。所以在写方案时不要只盯着 Linux 服务器。即使你把系统迁到了 Windows只要临时目录还存放在系统的temp目录下风险就依然存在。核心思路应该是把临时目录挪出“系统会定期清理”的默认区域。3. 快速止血不重启也能让业务先跑起来生产环境遇到大白天出故障第一目标不是“根治”而是让业务尽快恢复。重启应用当然最干净但重启意味着连接池重建、缓存预热、在线用户掉线某些场景下代价不小。在不重启的前提下有两个应急动作可以做。3.1 先确认真实临时路径不要凭猜直接看异常里打印的完整路径或者调用接口动态获取RestController public class TempDirController { GetMapping(/tmp-info) public MapString, String tmpInfo(ServletContext context) { return Map.of( java.io.tmpdir, System.getProperty(java.io.tmpdir), servletTempDir, String.valueOf(context.getAttribute(ServletContext.TEMPDIR)) ); } }ServletContext.TEMPDIR就是javax.servlet.context.tempdir对应的是上传插件实际使用的目录。在浏览器里访问/tmp-info得到类似{ java.io.tmpdir: /tmp, servletTempDir: /tmp/tomcat.8080.123456789/work/Tomcat/localhost/ROOT }然后到服务器上确认目录是否存在ls -ld /tmp/tomcat.8080.123456789/work/Tomcat/localhost/ROOT如果返回No such file or directory问题基本坐实。接下来要么重启要么手工补目录。3.2 手工重建目录做应急恢复既然报错是因为目录不存在那理论上把目录重新建出来进程内已持有的临时目录引用就能重新生效。这个方法在多数场景下有效但注意要按异常里给出的完整路径逐级创建mkdir -p /tmp/tomcat.8080.123456789/work/Tomcat/localhost/ROOT chmod 755 /tmp/tomcat.8080.123456789 chmod 775 /tmp/tomcat.8080.123456789/work/Tomcat/localhost/ROOT权限很重要。如果应用不是以 root 运行目录没有写权限接下来依然会失败。不确定用户时先看进程ps -o user,pid,cmd -p $(pgrep -f springboot)创建完目录后立刻测试上传。如果恢复正常说明应急成功如果还挂着同样的错则说明版本或框架内部缓存了旧的 File 引用稳妥起见还是重启。3.3 什么时候直接重启更划算手工重建适合“业务不能中断、运维正在写方案”的窗口期。但如果这个临时目录不止一处用到或者你担心还有其他隐藏路径也被清掉直接重启更稳妥。SpringBoot 内嵌 Tomcat 启动时会把临时目录完整重建一遍比手工 mkdir 可靠。不过要强调重启只是让问题重置不是让问题消失。假如不改变默认临时目录位置下个长假、下个低峰期还会重演。别把“重启后好了”当成“修好了”。4. 根治方案把应用临时目录从系统 /tmp 挪到持久化目录既然系统要清理“长期未访问”的文件应用就别把临时目录放在它的监管范围里。最干净的做法是指定一个业务自己的持久化目录比如/data/app下。修改application.ymlserver: tomcat: basedir: /data/app/tomcat servlet: multipart: enabled: true max-file-size: 200MB max-request-size: 200MB location: /data/app/tmp服务启动前先确保目录存在mkdir -p /data/app/tomcat mkdir -p /data/app/tmp chown -R deploy:deploy /data/app4.1 server.tomcat.basedir 到底改了什么这个配置把内嵌 Tomcat 的 base 目录从java.io.tmpdir指向/data/app/tomcat。之后 Tomcat 生成的运行时目录就变成/data/app/tomcat/tomcat.8080.xxxservlet 上下文临时目录变成/data/app/tomcat/tomcat.8080.xxx/work/Tomcat/localhost/ROOT一类路径。这些路径都不在/tmp下systemd-tmpfiles 和 tmpwatch 的默认规则根本不会去动它。这里有个细节不要只设basedir就以为万事大吉。如果同时设置了spring.servlet.multipart.location那上传落盘位置会明确指向你指定的路径不再依赖 servlet 上下文临时目录。两个配置一起设相当于给上传文件单独画了一个专属区域路径直观排查方便。4.2 spring.servlet.multipart.location 的作用spring.servlet.multipart.location对应 Spring Boot 的MultipartConfigElement.location。显式设置后Multipart 解析器创建临时文件时会直接在这个路径下写。它比 Tomcat baseDir 更精准只影响上传文件不影响 JSP 编译或 servlet 上下文其他临时内容。有一点需要注意如果指定了location但目录不存在上传时才可能报错。因此这个目录必须在应用启动前创建好或者由应用启动逻辑自动创建。目录权限也要确保运行用户可写否则会出现java.io.IOException: Permission denied4.3 三种配置路径怎么选实际部署中有三种方式可以把临时目录挪走方式配置影响范围适用场景修改 JVM 系统属性-Djava.io.tmpdir/data/app/tmp整个 JVM 的临时文件都受影响包括 JIT、RMI、JSP 编译等不推荐副作用太大修改 Tomcat baseDirserver.tomcat.basedir/data/app/tomcat内嵌 Tomcat 的运行时目录、servlet 上下文临时目录最推荐覆盖面合适单独指定 Multipart locationspring.servlet.multipart.location/data/app/tmp只影响文件上传临时文件推荐与 basedir 同时使用-Djava.io.tmpdir看起来很彻底但副作用很隐蔽。比如某些 Java 库会把临时文件写到这里某些加密工具、报表工具也会借助系统临时目录整个 JVM 全部改过去之后如果/data/app/tmp出了问题波及面更广。所以我的建议是不要动 JVM 级属性用 Tomcat 这层来控制。4.4 配置后的验证方法改完配置重启后先走一遍上面的/tmp-info接口看到返回的servletTempDir已经变成/data/app/tomcat/tomcat.8080.xxx/work/Tomcat/localhost/ROOT再实际传一个文件验证。然后观察两天日志确认不再出现临时目录报错。如果业务量很小、很少访问你甚至可以主动验证一下“长时间没人访问”的场景停掉外部流量用touch -d 30 days ago修改临时目录的时间戳再跑一次清理任务确认系统不会碰/data/app下的文件。注意fallback到外部 Tomcat 部署 war 包时server.tomcat.basedir依然有效。但如果你用的是独立安装的 Tomcat那要关注的是CATALINA_BASE/temp和work目录不是 SpringBoot 配置二者别混。5. Linux 系统清理规则隔离让清理任务主动忽略应用目录如果你的应用因为历史原因必须把临时目录留在/tmp下那就还有一条路修改系统清理规则让清理任务显式跳过应用目录。这条路能用但我一般只把它当“缝补”手段而不是首选。5.1 /tmp 清理任务从哪里来先查一下服务器上的清理服务systemctl list-timers | grep -E tmp|clean看到systemd-tmpfiles-clean.timer就按 systemd 的方式处理。看到/etc/cron.daily/tmpwatch就按 tmpwatch 的方式处理。清理规则文件在/usr/lib/tmpfiles.d/tmp.conf但不要去改它因为系统升级可能覆盖。正确姿势是在/etc/tmpfiles.d/下建一个自己的配置文件这个目录的优先级高于系统目录。5.2 systemd-tmpfiles 的配置与排除写法假设你的上传临时目录还是/tmp/app-tmp想让它不被清理就创建/etc/tmpfiles.d/app-tmp.confd /tmp/app-tmp 0775 deploy deploy - x /tmp/app-tmp第一行d表示如果目录不存在就自动创建属主、属组、权限都写清楚。第二行x是排除规则告诉systemd-tmpfiles --clean不要清理这个路径。写完以后可以手动执行一次看效果systemd-tmpfiles --clean /etc/tmpfiles.d/app-tmp.conf再确认目录还在ls -ld /tmp/app-tmp5.3 老版本 CentOS/RHEL 的 tmpwatch 排除如果是 tmpwatch 的时代编辑/etc/cron.daily/tmpwatch找到调用tmpwatch的那行在命令里加上排除参数/usr/sbin/tmpwatch -x /tmp/app-tmp 240 /tmp-x表示排除目录。注意 old version 的 tmpwatch 可能对-x后面的路径匹配规则比较死板尽量写绝对路径不要写通配符。5.4 为什么我更推荐“挪目录”而不是“改系统”修改系统清理规则看起来是针对性强实际上把问题复杂化了。首先systemd-tmpfiles的配置语法不同发行版之间有差异x规则的路径匹配行为在不同版本上并不完全一致。其次你把一个目录从清理任务里特赦出来就要承担“这个目录永远不会被系统清理”的后果垃圾文件会越积越多磁盘空间问题早晚还会找上门。而把临时目录放到/data/app这种业务目录下由应用进程和部署脚本自己管理生命周期系统清理任务根本看不到它既不违背操作系统安全策略也不污染系统目录。所以我的最终建议很明确能不跟系统清理机制纠缠就不纠缠直接移出去。6. 兜底保险应用启动自检与定时重建不管前面配置做得多到位服务器上总有一些意料之外的事某天维护人员手动清了/data或者磁盘空间满了被脚本自动删文件或者同事把目录迁移到别的地方。因此最后一道防线是在应用层面加自动重建逻辑。6.1 利用启动逻辑自动建目录在 SpringBoot 启动入口前执行目录检查是最简单可靠的做法SpringBootApplication public class DemoApplication { private static final Path TOMCAT_BASE_DIR Paths.get(/data/app/tomcat); private static final Path MULTIPART_LOCATION Paths.get(/data/app/tmp); public static void main(String[] args) { prepareTempDirs(); SpringApplication.run(DemoApplication.class, args); } private static void prepareTempDirs() { try { Files.createDirectories(TOMCAT_BASE_DIR); Files.createDirectories(MULTIPART_LOCATION); } catch (IOException e) { throw new RuntimeException(初始化临时目录失败, e); } } }关键点是在SpringApplication.run之前执行。如果放在PostConstruct里虽然大多数情况也能跑但不如启动入口这里保险。先建目录再启动内嵌 Tomcat就不会出现“Tomcat 启动时引用了不存在的 basedir”的隐患。6.2 在 systemd 或容器层面兜底如果你的 SpringBoot 应用用 systemd 管理直接在服务文件里加[Service] ExecStartPre/usr/bin/mkdir -p /data/app/tomcat /data/app/tmp ExecStart/usr/bin/java -jar /data/app/demo.jar容器场景下可以在 Dockerfile 或启动脚本里建目录或者在临时目录挂载卷时用 Kubernetes 的initContainer先建好。另一种轻量兜底是定时任务每 10 分钟检查一次目录是否存在不存在就创建*/10 * * * * root /usr/bin/test -d /data/app/tmp || /usr/bin/mkdir -p /data/app/tmp这种定时任务属于“事后悔药”只适合作为保障网别当成主方案。它不解决权限、不解决存量垃圾文件也不能阻止清理程序再次删除只能保证目录被删后最短时间内重建。6.3 别把“目录被删”和其他上传问题混为一谈实际排查中我要提醒大家别把所有上传失败都归到“临时目录被删”上。遇到过好几次误判的案例文件超过max-file-size报MaxUploadSizeExceededException和临时目录无关。目录没有写权限报Permission denied也不是目录不存在。上传过程中连接超时大文件还没传完就断了服务端日志可能根本没有这个异常。Kubernetes 里 Pod 被重新调度Pod 迁移到新节点旧节点临时目录没了但新节点上可能又是全新的现象看起来像“又恢复了”。定位时先看报错关键字。出现not valid、No such file or directory、Unable to create temporary file这类优先怀疑路径被删出现MaxUploadSizeExceededException先看配置出现Permission denied先看用户权限。方向错了查多久都白搭。根据我个人经验这类“临时目录被系统清理”的问题最有效的处理顺序是先手工重建目录恢复业务再修改server.tomcat.basedir和spring.servlet.multipart.location把目录挪到业务盘最后加一道启动自检。三层做完后面基本不会再被这个问题烦到。最后再分享一个小技巧以后如果遇到类似“跑着跑着某个目录消失”的怪问题先怀疑操作系统清理策略去/tmp、/var/tmp下翻一翻比看半天业务代码有用得多。