ChatGPT解决这个技术问题 Extra ChatGPT

如何在 Java 中创建临时目录/文件夹?

在 Java 应用程序中创建临时目录是否有标准且可靠的方法?有 an entry in Java's issue database,在评论中有一些代码,但我想知道在常用库之一(Apache Commons 等)中是否有标准解决方案?


G
GreenGiant

如果您使用的是 JDK 7,请使用新的 Files.createTempDirectory 类来创建临时目录。

Path tempDirWithPrefix = Files.createTempDirectory(prefix);

在 JDK 7 之前应该这样做:

public static File createTempDirectory()
    throws IOException
{
    final File temp;

    temp = File.createTempFile("temp", Long.toString(System.nanoTime()));

    if(!(temp.delete()))
    {
        throw new IOException("Could not delete temp file: " + temp.getAbsolutePath());
    }

    if(!(temp.mkdir()))
    {
        throw new IOException("Could not create temp directory: " + temp.getAbsolutePath());
    }

    return (temp);
}

如果你愿意,你可以做出更好的例外(子类 IOException)。


这是危险的。众所周知,Java 不会立即删除文件,因此 mkdir 有时可能会失败
@Demiurg唯一没有立即删除文件的情况是在Windows上,当文件已经打开时(例如,它可能被病毒扫描程序打开)。您是否还有其他文件要显示(我对这样的事情很好奇:-)?如果它经常发生,那么上面的代码将不起作用,如果它很少发生,那么在删除发生(或达到一些最大尝试)之前停止对上面代码的调用将起作用。
众所周知,@Demiurg Java 不会立即删除文件。这是真的,即使你不打开它。因此,更安全的方法是temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();
此代码在 delete()mkdir() 之间存在竞争条件:恶意进程可以同时创建目标目录(使用最近创建的文件的名称)。请参阅 Files.createTempDir() 了解替代方案。
我喜欢 !想要脱颖而出,太容易错过了。我读了很多学生写的代码...... if(!i) 很常见,很烦人:-)
J
Jared Rummler

Google Guava 库有很多有用的实用程序。这里要注意的一点是 Files class。它有很多有用的方法,包括:

File myTempDir = Files.createTempDir();

这正是您在一行中所要求的。如果您阅读文档 here,您会发现建议的 File.createTempFile("install", "dir") 改编通常会引入安全漏洞。


我想知道你指的是什么漏洞。这种方法似乎不会创建竞争条件,因为如果这样的目录已经存在(由攻击者创建),则 File.mkdir() 会失败。我认为这个调用也不会通过恶意符号链接进行。你能澄清你的意思吗?
@abb:我不知道 Guava 文档中提到的竞争条件的详细信息。我怀疑文档是正确的,因为它特别指出了问题。
@abb你是对的。只要检查 mkdir() 的返回,它就会是安全的。 Spina 指向的代码使用了这个 mkdir() 方法。 grepcode.com/file/repo1.maven.org/maven2/com.google.guava/guava/… 。这只是 Unix 系统上使用 /tmp 目录时的一个潜在问题,因为它启用了粘滞位。
@SarelBotha 感谢您在这里填写空白。很长一段时间以来,我一直在无所事事地思考这个问题。
m
matsev

如果您需要一个临时目录进行测试并且您正在使用 jUnit,那么 @RuleTemporaryFolder 可以解决您的问题:

@Rule
public TemporaryFolder folder = new TemporaryFolder();

documentation

TemporaryFolder Rule 允许创建保证在测试方法完成时删除的文件和文件夹(无论是通过还是失败)

更新:

如果您使用的是 JUnit Jupiter(5.1.1 或更高版本),您可以选择使用 JUnit Pioneer,它是 JUnit 5 扩展包。

project documentation 复制:

例如,以下测试为单个测试方法注册扩展,创建文件并将其写入临时目录并检查其内容。

@Test
@ExtendWith(TempDirectory.class)
void test(@TempDir Path tempDir) {
    Path file = tempDir.resolve("test.txt");
    writeFile(file);
    assertExpectedFileContent(file);
}

JavaDocJavaDoc of TempDirectory 中的更多信息

摇篮:

dependencies {
    testImplementation 'org.junit-pioneer:junit-pioneer:0.1.2'
}

马文:

<dependency>
   <groupId>org.junit-pioneer</groupId>
   <artifactId>junit-pioneer</artifactId>
   <version>0.1.2</version>
   <scope>test</scope>
</dependency>

更新 2:

@TempDir 注释已作为实验性功能添加到 JUnit Jupiter 5.4.0 版本中。从 JUnit 5 User Guide 复制的示例:

@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
    Path file = tempDir.resolve("test.txt");

    new ListWriter(file).write("a", "b", "c");

    assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}

在 Windows 7 上的 JUnit 4.8.2 中不起作用! (权限问题)
@CraigRinger:为什么依赖这个是不明智的?
@AdamParkin 老实说,我不记得了。解释失败!
在 4.11 中不起作用。 getRoot()newFile() 都抱怨“尚未创建临时目录”。此外,“文件夹”是丑陋的 Windows 术语。这是一个目录。
这种方法的主要好处是目录由 JUnit 管理(在测试之前创建并在测试之后递归删除)。它确实有效。如果您收到“尚未创建临时目录”,可能是因为您忘记了 @Rule 或未公开的字段。
G
Greg Price

天真地编写代码来解决这个问题会受到竞争条件的影响,包括这里的几个答案。从历史上看,您可以仔细考虑竞争条件并自己编写,或者您可以使用第三方库,如 Google 的 Guava(如 Spina 的回答所建议的那样)。或者您可以编写有缺陷的代码。

但是从 JDK 7 开始,有好消息! Java 标准库本身现在为这个问题提供了一个正常工作(非竞争性)的解决方案。你想要java.nio.file.Files#createTempDirectory()。从 documentation

public static Path createTempDirectory(Path dir,
                       String prefix,
                       FileAttribute<?>... attrs)
                                throws IOException

在指定目录中创建一个新目录,使用给定的前缀生成其名称。生成的 Path 与给定目录相同的 FileSystem 相关联。关于如何构造目录名称的细节取决于实现,因此未指定。在可能的情况下,前缀用于构造候选名称。

这有效地解决了 Sun 错误跟踪器中的 embarrassingly ancient bug report 问题,它只需要这样的功能。


A
Andres Kievsky

这是 Guava 库的 Files.createTempDir() 的源代码。它没有你想象的那么复杂:

public static File createTempDir() {
  File baseDir = new File(System.getProperty("java.io.tmpdir"));
  String baseName = System.currentTimeMillis() + "-";

  for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
    File tempDir = new File(baseDir, baseName + counter);
    if (tempDir.mkdir()) {
      return tempDir;
    }
  }
  throw new IllegalStateException("Failed to create directory within "
      + TEMP_DIR_ATTEMPTS + " attempts (tried "
      + baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
}

默认:

private static final int TEMP_DIR_ATTEMPTS = 10000;

See here


J
Joachim Sauer

即使您稍后明确删除它,也不要使用 deleteOnExit()

Google 'deleteonexit is evil' 了解更多信息,但问题的要点是:

deleteOnExit() 仅删除正常 JVM 关闭,不会崩溃或终止 JVM 进程。 deleteOnExit() 仅在 JVM 关闭时删除 - 不适合长时间运行的服务器进程,因为: 最邪恶的 - deleteOnExit() 会为每个临时文件条目消耗内存。如果您的进程运行了几个月,或者在短时间内创建了大量临时文件,那么您会消耗内存并且在 JVM 关闭之前永远不会释放它。


我们有一个 JVM,其中类和 jar 文件获取 JVM 创建的隐藏文件,这些额外信息需要很长时间才能删除。在对爆炸 WAR 的 Web 容器进行热重新部署时,JVM 在完成后实际上可能需要几分钟来清理,但在运行几个小时后退出之前。
s
seeker

从 Java 1.7 开始,createTempDirectory(prefix, attrs)createTempDirectory(dir, prefix, attrs) 包含在 java.nio.file.Files

示例:File tempDir = Files.createTempDirectory("foobar").toFile();


K
Keith

这就是我决定为自己的代码做的事情:

/**
 * Create a new temporary directory. Use something like
 * {@link #recursiveDelete(File)} to clean this directory up since it isn't
 * deleted automatically
 * @return  the new directory
 * @throws IOException if there is an error creating the temporary directory
 */
public static File createTempDir() throws IOException
{
    final File sysTempDir = new File(System.getProperty("java.io.tmpdir"));
    File newTempDir;
    final int maxAttempts = 9;
    int attemptCount = 0;
    do
    {
        attemptCount++;
        if(attemptCount > maxAttempts)
        {
            throw new IOException(
                    "The highly improbable has occurred! Failed to " +
                    "create a unique temporary directory after " +
                    maxAttempts + " attempts.");
        }
        String dirName = UUID.randomUUID().toString();
        newTempDir = new File(sysTempDir, dirName);
    } while(newTempDir.exists());

    if(newTempDir.mkdirs())
    {
        return newTempDir;
    }
    else
    {
        throw new IOException(
                "Failed to create temp dir named " +
                newTempDir.getAbsolutePath());
    }
}

/**
 * Recursively delete file or directory
 * @param fileOrDir
 *          the file or dir to delete
 * @return
 *          true iff all files are successfully deleted
 */
public static boolean recursiveDelete(File fileOrDir)
{
    if(fileOrDir.isDirectory())
    {
        // recursively delete contents
        for(File innerFile: fileOrDir.listFiles())
        {
            if(!FileUtilities.recursiveDelete(innerFile))
            {
                return false;
            }
        }
    }

    return fileOrDir.delete();
}

这是不安全的。请参阅 Joachim Sauer 在第一个(同样不安全)选项中的评论。检查文件或目录是否存在而不是自动获取文件名的正确方法是创建文件或目录。
@zbyszek javadocs 说“UUID 是使用加密强的伪随机数生成器生成的。”鉴于恶意进程如何在exists() 和mkdirs() 之间创建同名目录。事实上,现在看这个,我认为我的 exists() 测试可能有点傻。
Keith:在这种情况下,UUID 是否安全并不重要。对于您查询的名称的信息以某种方式“泄漏”就足够了。例如,假设正在创建的文件位于 NFS 文件系统上,攻击者可以(被动地)监听数据包。或者随机生成器状态已经泄露。在我的评论中,我说您的解决方案与公认的答案同样不安全,但这不公平:被 inotify 击败的被接受的解决方案微不足道,而这个解决方案更难被击败。然而,在某些情况下,这当然是可能的。
我有同样的想法,并使用像这样的随机 UUID 实现了一个解决方案。不检查是否存在,只需一次尝试创建 - randomUUID 方法使用的强 RNG 几乎可以保证没有冲突(可用于在 DB 表中生成主键,自己完成此操作并且从未知道冲突),非常有信心。如果有人不确定,请查看stackoverflow.com/questions/2513573/…
如果您查看 Java 的实现,它们只会生成随机名称,直到没有冲突。他们的最大尝试是无限的。因此,如果有人恶意不断猜测您的文件/目录名称,它将永远循环。这是源代码的链接:hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/… 我在想它可以以某种方式锁定文件系统,因此它可以自动生成一个唯一的名称并创建目录,但我想根据源代码它不会这样做。
P
Paul Tomblin

好吧,“createTempFile”实际上是创建文件。那么为什么不先删除它,然后在上面执行 mkdir 呢?


您应该始终检查 mkdir() 的返回值。如果为假,则表示该目录已经存在。这可能会导致安全问题,因此请考虑这是否会在您的应用程序中引发错误。
请参阅其他答案中有关竞争条件的说明。
我喜欢这个,除非比赛
m
matt b

这段代码应该可以很好地工作:

public static File createTempDir() {
    final String baseTempPath = System.getProperty("java.io.tmpdir");

    Random rand = new Random();
    int randomInt = 1 + rand.nextInt();

    File tempDir = new File(baseTempPath + File.separator + "tempDir" + randomInt);
    if (tempDir.exists() == false) {
        tempDir.mkdir();
    }

    tempDir.deleteOnExit();

    return tempDir;
}

如果目录已经存在并且您没有对它的读/写访问权限或者如果它是一个常规文件怎么办?你在那里也有一个竞争条件。
此外,deleteOnExit 不会删除非空目录。
M
Michael Myers

this RFE 及其评论中所述,您可以先调用 tempDir.delete()。或者您可以使用 System.getProperty("java.io.tmpdir") 并在那里创建一个目录。无论哪种方式,您都应该记得调用 tempDir.deleteOnExit(),否则完成后文件不会被删除。


这个属性不是叫做“java.io.tmpdir”,而不是“...temp”吗?请参阅java.sun.com/j2se/1.4.2/docs/api/java/io/File.html
这么。在重复我阅读的内容之前,我应该进行验证。
java.io.tmpdir 是共享的,所以你需要做所有常见的巫术来避免踩到别人的脚趾。
A
Arne

只是为了完成,这是来自谷歌番石榴库的代码。这不是我的代码,但我认为在这个线程中展示它是有价值的。

  /** Maximum loop count when creating temp directories. */
  private static final int TEMP_DIR_ATTEMPTS = 10000;

  /**
   * Atomically creates a new directory somewhere beneath the system's temporary directory (as
   * defined by the {@code java.io.tmpdir} system property), and returns its name.
   *
   * <p>Use this method instead of {@link File#createTempFile(String, String)} when you wish to
   * create a directory, not a regular file. A common pitfall is to call {@code createTempFile},
   * delete the file and create a directory in its place, but this leads a race condition which can
   * be exploited to create security vulnerabilities, especially when executable files are to be
   * written into the directory.
   *
   * <p>This method assumes that the temporary volume is writable, has free inodes and free blocks,
   * and that it will not be called thousands of times per second.
   *
   * @return the newly-created directory
   * @throws IllegalStateException if the directory could not be created
   */
  public static File createTempDir() {
    File baseDir = new File(System.getProperty("java.io.tmpdir"));
    String baseName = System.currentTimeMillis() + "-";

    for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
      File tempDir = new File(baseDir, baseName + counter);
      if (tempDir.mkdir()) {
        return tempDir;
      }
    }
    throw new IllegalStateException(
        "Failed to create directory within "
            + TEMP_DIR_ATTEMPTS
            + " attempts (tried "
            + baseName
            + "0 to "
            + baseName
            + (TEMP_DIR_ATTEMPTS - 1)
            + ')');
  }

i
immoteous

我遇到了同样的问题,所以这只是对那些感兴趣的人的另一个答案,它类似于上述之一:

public static final String tempDir = System.getProperty("java.io.tmpdir")+"tmp"+System.nanoTime();
static {
    File f = new File(tempDir);
    if(!f.exists())
        f.mkdir();
}

对于我的应用程序,我决定添加一个选项以清除退出时的温度,因此我添加了一个关闭挂钩:

Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run() {
            //stackless deletion
            String root = MainWindow.tempDir;
            Stack<String> dirStack = new Stack<String>();
            dirStack.push(root);
            while(!dirStack.empty()) {
                String dir = dirStack.pop();
                File f = new File(dir);
                if(f.listFiles().length==0)
                    f.delete();
                else {
                    dirStack.push(dir);
                    for(File ff: f.listFiles()) {
                        if(ff.isFile())
                            ff.delete();
                        else if(ff.isDirectory())
                            dirStack.push(ff.getPath());
                    }
                }
            }
        }
    });

该方法在删除临时文件之前删除所有子目录和文件,而不使用调用堆栈(这完全是可选的,此时您可以使用递归来完成),但我想安全起见。


C
Carsten Engelke

正如您在其他答案中看到的那样,没有出现标准方法。因此,您已经提到了 Apache Commons,我建议使用 Apache Commons IO 中的 FileUtils 以下方法:

/**
 * Creates a temporary subdirectory in the standard temporary directory.
 * This will be automatically deleted upon exit.
 * 
 * @param prefix
 *            the prefix used to create the directory, completed by a
 *            current timestamp. Use for instance your application's name
 * @return the directory
 */
public static File createTempDirectory(String prefix) {

    final File tmp = new File(FileUtils.getTempDirectory().getAbsolutePath()
            + "/" + prefix + System.currentTimeMillis());
    tmp.mkdir();
    Runtime.getRuntime().addShutdownHook(new Thread() {

        @Override
        public void run() {

            try {
                FileUtils.deleteDirectory(tmp);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
    return tmp;

}

这是首选,因为 apache 共享最接近所要求的“标准”的库,并且适用于 JDK 7 和旧版本。这也返回一个“旧” File 实例(基于流)而不是“新” Path 实例(基于缓冲区,将是 JDK7 的 getTemporaryDirectory() 方法的结果)-> 因此它返回大多数人需要什么时候他们想创建一个临时目录。


D
DigitShifter

试试这个小例子:

代码:

try {
    Path tmpDir = Files.createTempDirectory("tmpDir");
    System.out.println(tmpDir.toString());
    Files.delete(tmpDir);
} catch (IOException e) {
    e.printStackTrace();
}

导入:java.io.IOException java.nio.file.Files java.nio.file.Path

Windows 机器上的控制台输出:C:\Users\userName\AppData\Local\Temp\tmpDir2908538301081367877

评论:
Files.createTempDirectory 自动生成唯一 ID - 2908538301081367877。

注意:
阅读以下内容以递归删除目录:
Delete directories recursively in Java


b
bluish

我喜欢多次尝试创建唯一名称,但即使是这个解决方案也不排除竞争条件。在测试 exists()if(newTempDir.mkdirs()) 方法调用之后,另一个进程可能会进入。我不知道如何在不诉诸本机代码的情况下完全确保它的安全,我认为这是隐藏在 File.createTempFile() 中的内容。


g
geri

在 Java 7 之前,您还可以:

File folder = File.createTempFile("testFileUtils", ""); // no suffix
folder.delete();
folder.mkdirs();
folder.deleteOnExit();

不错的代码。但不幸的是,“deleteOnExit()”不起作用,因为 Java 不能一次删除整个文件夹。您必须递归删除所有文件:/
o
ordnungswidrig

使用 File#createTempFiledelete 为目录创建唯一名称似乎没问题。您应该添加一个 ShutdownHook 以在 JVM 关闭时(递归地)删除目录。


关闭挂钩很麻烦。 File#deleteOnExit 也不起作用吗?
#deleteOnExit 对我不起作用 - 我相信它不会删除非空目录。
我实现了一个使用 Java 8 运行的快速测试,但临时文件夹没有被删除,请参阅 pastebin.com/mjgG70KG