【发布时间】:2010-10-11 16:06:59
【问题描述】:
是否有一种标准且可靠的方法可以在 Java 应用程序中创建临时目录?有an entry in Java's issue database,它在 cmets 中有一些代码,但我想知道是否可以在常用库之一(Apache Commons 等)中找到标准解决方案?
【问题讨论】:
标签: java file file-io directory temporary-directory
是否有一种标准且可靠的方法可以在 Java 应用程序中创建临时目录?有an entry in Java's issue database,它在 cmets 中有一些代码,但我想知道是否可以在常用库之一(Apache Commons 等)中找到标准解决方案?
【问题讨论】:
标签: java file file-io directory temporary-directory
如果您使用的是 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)。
【讨论】:
temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();。
delete() 和mkdir() 之间存在竞争条件:恶意进程可以同时创建目标目录(取最近创建文件的名称)。请参阅Files.createTempDir() 了解替代方案。
Google Guava 库有大量有用的实用程序。这里值得注意的是Files class。它有很多有用的方法,包括:
File myTempDir = Files.createTempDir();
这正是您在一行中所要求的。如果您阅读文档here,您会发现建议的对File.createTempFile("install", "dir") 的修改通常会引入安全漏洞。
【讨论】:
如果您需要一个临时目录进行测试并且您正在使用 jUnit,@Rule 和 TemporaryFolder 可以解决您的问题:
@Rule
public TemporaryFolder folder = new TemporaryFolder();
TemporaryFolder Rule 允许创建保证在测试方法完成时删除的文件和文件夹(无论是通过还是失败)
更新:
如果您使用的是 JUnit Jupiter(5.1.1 或更高版本),您可以选择使用 JUnit Pioneer,它是 JUnit 5 扩展包。
例如,以下测试为单个测试方法注册扩展,创建文件并将其写入临时目录并检查其内容。
@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));
}
【讨论】:
getRoot() 和 newFile() 都抱怨“尚未创建临时目录”。此外,“文件夹”是丑陋的 Windows 术语。这是一个目录。
用于解决此问题的天真编写的代码会受到竞争条件的影响,包括此处的几个答案。从历史上看,您可以仔细考虑竞争条件并自己编写,或者您可以使用第三方库,如 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 问题,它只需要这样的功能。
【讨论】:
这是 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;
【讨论】:
不要使用deleteOnExit(),即使您稍后明确删除它。
Google 'deleteonexit is evil' 了解更多信息,但问题的要点是:
deleteOnExit() 仅在正常 JVM 关闭时删除,不会崩溃或杀死 JVM 进程。
deleteOnExit() 仅在 JVM 关闭时删除 - 不适合长时间运行的服务器进程,因为:
最邪恶的 - deleteOnExit() 为每个临时文件条目消耗内存。如果您的进程运行了几个月,或者在短时间内创建了大量临时文件,那么您会消耗内存并且在 JVM 关闭之前永远不会释放它。
【讨论】:
从 Java 1.7 开始,createTempDirectory(prefix, attrs) 和 createTempDirectory(dir, prefix, attrs) 包含在 java.nio.file.Files 中
示例:
File tempDir = Files.createTempDirectory("foobar").toFile();
【讨论】:
这就是我决定为自己的代码做的事情:
/**
* 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();
}
【讨论】:
好吧,“createTempFile”实际上是创建文件。那么为什么不先将其删除,然后对其执行 mkdir 呢?
【讨论】:
这段代码应该可以正常工作:
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;
}
【讨论】:
正如this RFE 及其cmets 中所讨论的,您可以先致电tempDir.delete()。或者您可以使用System.getProperty("java.io.tmpdir") 并在那里创建一个目录。无论哪种方式,您都应该记得致电tempDir.deleteOnExit(),否则您完成后不会删除该文件。
【讨论】:
只是为了完成,这是来自 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)
+ ')');
}
【讨论】:
我遇到了同样的问题,所以这只是有兴趣的人的另一个答案,它类似于上述之一:
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 之前删除所有子目录和文件,而不使用调用堆栈(这是完全可选的,此时您可以使用递归来完成),但我想在安全的一面。
【讨论】:
正如您在其他答案中看到的那样,没有出现标准方法。 因此您已经提到了 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() 方法的结果)-> 因此它返回大多数人需要什么时候他们想创建一个临时目录。
【讨论】:
试试这个小例子:
代码:
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
【讨论】:
我喜欢多次尝试创建唯一名称,但即使是这种解决方案也不能排除竞争条件。在测试exists() 和if(newTempDir.mkdirs()) 方法调用之后,另一个进程可能会进入。我不知道如何在不诉诸本机代码的情况下完全确保它的安全,我认为这是隐藏在 File.createTempFile() 中的内容。
【讨论】:
在 Java 7 之前,您还可以:
File folder = File.createTempFile("testFileUtils", ""); // no suffix
folder.delete();
folder.mkdirs();
folder.deleteOnExit();
【讨论】:
使用File#createTempFile 和delete 为目录创建唯一名称似乎没问题。您应该添加 ShutdownHook 以在 JVM 关闭时(递归地)删除目录。
【讨论】: