Java NIO 中 File.deleteOnExit() 的替代方案?

Alternative to File.deleteOnExit() in Java NIO?

JavaIO有File.deleteOnExit(),是JVM正常终止时删除调用它的文件的方法。我发现这对于清理临时文件非常有用,尤其是在单元测试期间。

但是,我在 Java NIO 的 Files class 中没有看到同名方法。我知道我可以做到 path.toFile().deleteOnExit(),但我想知道是否有使用 NIO 的替代方法。

有其他选择吗?如果没有,为什么没有?

在幕后,File.deleteOnExit() 只会通过 Runtime.addShutdownHook() 创建一个 shutdown hook

然后,你可以用 NIO 做同样的事情:

Runtime.getRuntime().addShutdownHook(new Thread() {
  public void run() {
    Path path = ...;

    Files.delete(path);
  }
});

简答

您不能在 Java NIO 中删除任意文件,但您可以在打开新流时使用 StandardOpenOption.DELETE_ON_CLOSE,这将在流关闭后立即删除文件,或者通过调用 .close()(包括来自 try-with-resources 语句)或 JVM 终止。例如:

Files.newOutputStream(Paths.get("Foo.tmp"), StandardOpenOption.DELETE_ON_CLOSE);

长答案

经过大量的挖掘,我发现 Java NIO 确实 有办法在退出时删除,但它以不同的方式处理它Java I/O.

首先,Files.createTempFile() 的 Java 文档描述了三种删除文件的方法:

Where used as a work files [sic], the resulting file may be opened using the DELETE_ON_CLOSE option so that the file is deleted when the appropriate close method is invoked. Alternatively, a shutdown-hook, or the File.deleteOnExit() mechanism may be used to delete the file automatically.

最后的选择,File.deleteOnExit()当然是JavaI/O的方法,我们尽量避免。当您调用上述方法时,shutdown-hook 是在幕后发生的事情。但是 DELETE_ON_CLOSE 选项是纯 Java NIO.

NIO 不会删除任意文件,Java NIO 假定您只对删除实际打开的文件感兴趣。因此,创建新流的方法,例如 Files.newOutputStream() 可以选择使用多个 OpenOptions,您可以在其中输入 StandardOpenOption.DELETE_ON_CLOSE。这样做是在流关闭后立即删除文件(通过调用 .close() 或 JVM 退出)。

例如:

Files.newOutputStream(Paths.get("Foo.tmp"), StandardOpenOption.DELETE_ON_CLOSE);

...将在流关闭时删除与流关联的文件,通过显式调用 .close(),流作为 try-with-resources 语句的一部分关闭,或者JVM 终止。

更新: 在某些操作系统上,例如 Linux,StandardOpenOption.DELETE_ON_CLOSE 会在创建 OutputStream 后立即删除。如果您只需要一个 OutputStream,那可能仍然没问题。有关详细信息,请参阅 DELETE_ON_CLOSE deletes files before close on Linux

因此 Java NIO 在 Java I/O 的基础上添加了新功能,您可以在关闭流时删除文件。如果这是在 JVM 退出期间删除的足够好的替代方法,您可以在纯 Java NIO 中执行此操作。如果没有,您将不得不依靠 Java I/O 的 File.deleteOnExit() 或关闭挂钩来删除文件。

我不建议用鞋角 StandardOpenOption.DELETE_ON_CLOSE 代替 File.deleteOnExit()。正如文档中提到的,它既不是通用的,也不太可能在琐碎的情况下正常工作。

DELETE_ON_CLOSE,顾名思义,就是 to immediately clean up a no-longer-needed resource. The documentation for Files.createTempFile()在这一点上同样清楚,DELETE_ON_CLOSE可以用于"work files"只在文件打开时才需要.

Files.createTempFile() 文档建议直接编写您自己的关闭挂钩或继续使用 File.deleteOnExit()。尽管您希望使用 NIO,但如果您只使用本地文件系统,那么使用 File.deleteOnExit() 本身并没有错。如果您没有使用(或不确定您正在使用)本地文件系统,因此 不能 使用 File.deleteOnExit() 编写自己的关闭挂钩就足够简单了just like what File does:

public final class DeletePathsAtShutdown {
  private static LinkedHashSet<Path> files = new LinkedHashSet<>();

  static {
    Runtime.getRuntime().addShutdownHook(
        new Thread(DeletePathsAtShutdown::shutdownHook));
  }

  private static void shutdownHook() {
    LinkedHashSet<Path> local;
    synchronized {
      local = paths;
      paths = null;
    }

    ArrayList<Path> toBeDeleted = new ArrayList<>(theFiles);
    Collections.reverse(toBeDeleted);
    for (Path p : toBeDeleted) {
      try {
        Files.delete(p);
      } catch (IOException | RuntimeException e) {
        // do nothing - best-effort
      }
    }
  }

  public static synchronized void register(Path p) {
    if (paths == null) {
      throw new IllegalStateException("ShutdownHook already in progress.");
    }
    paths.add(p);
  }
}

当然,如果 NIO 包含一个类似的开箱即用的关闭挂钩可能会很好,但没有它并不是使用错误工具完成工作的理由。您还可以向 DeletePathsAtShutdown 添加更多功能,例如 remove() 功能,或支持 deleting directories.