问题现象
在系统开发过程中,有时会发现任务执行过程中,相关文件找不到导致执行异常。该文件是用户上传保存到系统的。由于该问题偶发出现,排查起来有一定困难。
问题分析和定位
因为这里是文件无法找到,所以排查的思路是从文件的全生命周期去排查:
1.文件的上传是否成功;
2.文件路劲是否正确;
3.文件在哪些地方被使用,其中哪些地方涉及到文件删除。
经过多轮排查分析,1、2两点都没有发现问题。在第3点上也没发现有对该文件的删除操作。之后写了一个对文件删除进行监控的工具,经过多次测试,没发现问题所在。
另外,通过数次对问题出现的场景进行复现,发现该问题是否和文件上传会有某些关联,即一旦上传文件后,可能导致该问题。按着这样的想法进行复现,也没有发现问题所在。
最后,通过在文件上传的代码中发现了问题所在:
{
savePath.deleteOnExit();
(MultipartFile) file.transferTo(savePath)
}
这里上传的逻辑是先删除目标位置相同的文件(如果存在),然后再上传。于是便有了以上代码。但是其中deleteOnExit方法引起了注意,从字面上的单词意思可以猜想:在退出的时候删除文件。随后,经过查询资料有如下认识:
File.deleteOnExit() 方法的执行原理是将文件或目录标记为在 JVM 终止时删除。具体来说,这是如何工作的:
1.标记文件
当调用 File.deleteOnExit() 方法时,JVM 会将该文件或目录添加到一个待删除列表中。这个列表由 JVM 内部维护。
2.注册钩子
调用 deleteOnExit() 方法时,如果没有注册相应的钩子(hook),JVM 会自动注册一个钩子方法。这个钩子方法会在 JVM 终止之前被调>用。
3.调用钩子方法
当 JVM 准备终止时,它会调用注册的钩子方法。钩子方法会遍历待删除列表,并对每个文件执行删除操作。
4.删除操作
钩子方法会尝试删除列表中的每一个文件。如果删除操作成功,则文件被删除;如果失败,则文件不会被删除,并且可能会记录一条错误>信息。
有了这些发现后,先上传一个文件,然后再结束JVM进程,果然复现文原来的问题。至此,系统文件异常被删除的问题原因已找到。
问题的解决
考虑到,如果文件已经存在,transferTo 方法会覆盖该文件。这意味着旧文件的内容将被新文件的内容取代。 如果文件不存在,transferTo 方法会创建一个新的文件。可以将原逻辑中的代码savePath.deleteOnExit()删除。
但是如果文件在 transferTo 过程中被其他进程锁定,可能会导致 IOException。这种情况下,你需要捕获异常并处理。更安全的做法是transferTo 之前显式地检查并删除旧文件。这里使用了java.nio.file包下的Files.deleteIfExists(savePath)替换原来的savePath.deleteOnExit()。
进一步思考
出现此类偶发问题的确不好排查。但无论什么问题,即便没有任何的发现,最后都需要归结到代码层面去找原因。另外出现此类问题也说明了开发人员对api的滥用和不熟悉。作为开发人员应该在平日多积累常用api,对不熟悉和了解的api需要查阅资料,思考后判定适合当前场景才能使用。
这里,还从一个侧面展示了开发人员对Exit (退出)和Exist (存在)这两个单词的混淆。
愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!