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

Posted

技术标签:

【中文标题】如何在 Java 中创建临时目录/文件夹?【英文标题】:How to create a temporary directory/folder in Java? 【发布时间】:2010-10-11 16:06:59 【问题描述】:

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

【问题讨论】:

【参考方案1】:

如果您使用的是 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) 很常见到令人讨厌:-)【参考方案2】:

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 感谢您在此处填写空白。我一直在无所事事地想这个问题。 现在已弃用:guava.dev/releases/30.1-jre/api/docs/com/google/common/io/…【参考方案3】:

如果您需要一个临时目录进行测试并且您正在使用 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);

JavaDoc 和 JavaDoc 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 或未公开的字段。【参考方案4】:

用于解决此问题的天真编写的代码会受到竞争条件的影响,包括此处的几个答案。从历史上看,您可以仔细考虑竞争条件并自己编写,或者您可以使用第三方库,如 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 问题,它只需要这样的功能。

【讨论】:

【参考方案5】:

这是 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

【讨论】:

【参考方案6】:

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

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

    deleteOnExit() 仅在正常 JVM 关闭时删除,不会崩溃或杀死 JVM 进程。

    deleteOnExit() 仅在 JVM 关闭时删除 - 不适合长时间运行的服务器进程,因为:

    最邪恶的 - deleteOnExit() 为每个临时文件条目消耗内存。如果您的进程运行了几个月,或者在短时间内创建了大量临时文件,那么您会消耗内存并且在 JVM 关闭之前永远不会释放它。

【讨论】:

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

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

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

【讨论】:

【参考方案8】:

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

/**
 * 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 表中生成主键,自己完成此操作并且从未知道冲突),非常有信心。如果有人不确定,请查看***.com/questions/2513573/… 如果您查看 Java 的实现,它们只会生成随机名称,直到没有冲突。他们的最大尝试是无限的。因此,如果有人恶意不断猜测您的文件/目录名称,它将永远循环。这是源代码的链接:hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/… 我在想它可以以某种方式锁定文件系统,因此它可以自动生成一个唯一的名称并创建目录,但我想根据源代码它不会这样做。 【参考方案9】:

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

【讨论】:

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

这段代码应该可以正常工作:

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 不会删除非空目录。【参考方案11】:

正如this RFE 及其cmets 中所讨论的,您可以先致电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 是共享的,所以你需要做所有常见的巫术来避免踩到别人的脚趾。【参考方案12】:

只是为了完成,这是来自 google guava 库的代码。这不是我的代码,但我认为在这个线程中显示它是有价值的。

  /** 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)
            + ')');
  

【讨论】:

【参考方案13】:

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

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

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

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());
                    
                
            
        
    );

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

【讨论】:

【参考方案14】:

正如您在其他答案中看到的那样,没有出现标准方法。 因此您已经提到了 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() 方法的结果)-> 因此它返回大多数人需要什么时候他们想创建一个临时目录。

【讨论】:

【参考方案15】:

试试这个小例子:

代码:

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

【讨论】:

【参考方案16】:

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

【讨论】:

【参考方案17】:

在 Java 7 之前,您还可以:

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

【讨论】:

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

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

【讨论】:

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

以上是关于如何在 Java 中创建临时目录/文件夹?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中创建一个临时文件(用于写入)? [复制]

如何在 MySQL 中创建临时过程?

如何正确引用/检索在 AppData 中创建的临时文件以将文件上传到服务器?

如何在 Java 中创建一个新的 zip 文件并向其中添加一个大目录?

Java:如何使用 java.nio.file.FileSystem 在 zip 中创建目录

在 Windows 中创建临时目录?