Windows 上可靠的 File.renameTo() 替代方案?

Posted

技术标签:

【中文标题】Windows 上可靠的 File.renameTo() 替代方案?【英文标题】:Reliable File.renameTo() alternative on Windows? 【发布时间】:2010-11-03 06:22:21 【问题描述】:

Java 的File.renameTo() 似乎存在问题,尤其是在 Windows 上。 正如API documentation 所说,

this 行为的许多方面 方法本质上是 平台相关:重命名 操作可能无法移动 文件从一个文件系统到另一个, 它可能不是原子的,它可能 如果文件带有 目标抽象路径名已经 存在。返回值应始终 检查以确保 重命名操作成功。

在我的例子中,作为升级过程的一部分,我需要移动(重命名)一个可能包含千兆字节数据的目录(许多子目录和不同大小的文件)。移动总是在同一个分区/驱动器内完成,因此实际上不需要物理移动磁盘上的所有文件。

不应该对要移动的目录的内容有任何文件锁定,但是,renameTo() 仍然经常无法完成其工作并返回 false。 (我只是猜测可能某些文件锁在 Windows 上有些随意过期。)

目前我有一个使用复制和删除的后备方法,但这很糟糕,因为它可能需要 很多 时间,具体取决于文件夹的大小。我还在考虑简单地记录用户可以手动移动文件夹以避免等待数小时的事实。但正确的道路显然是自动的和快速的。

所以我的问题是,您知道在 Windows 上使用 Java 进行快速移动/重命名的另一种可靠方法,可以使用普通的 JDK 或一些外部库。或者,如果您知道一种简单 方法来检测和释放给定文件夹及其所有内容的任何文件锁定以及 其所有内容(可能是数千个单独的文件),那也可以.


编辑:在这种特殊情况下,我们似乎只使用了renameTo() 并考虑了更多的事情;见this answer。

【问题讨论】:

你可以等待/使用 JDK 7,它有更好的文件系统支持。 @kd304,实际上我迫不及待地想使用抢先体验版,但很有趣的是,这样的东西即将推出! 【参考方案1】:

另请参阅 JDK 7 中的 Files.move() 方法。

一个例子:

String fileName = "MyFile.txt";

try 
    Files.move(new File(fileName).toPath(), new File(fileName).toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
 catch (IOException ex) 
    Logger.getLogger(SomeClass.class.getName()).log(Level.SEVERE, null, ex);

【讨论】:

不幸的是,Java7 并不总是答案(就像 42 一样) 即使在 ubuntu、JDK7 上,我们在使用 EBS 存储的 EC2 上运行代码时也遇到了这个问题。 File.renameTo 失败,File.canWrite 也是如此。 请注意,这与 File#renameTo() 一样不可靠。它只是在失败时给出一个更有用的错误。我发现的唯一合理可靠的方法是使用 Files#copy 将文件复制到新名称,然后使用 Files#delete 删除原始文件(删除本身也可能失败,原因与 Files#move 可能失败的原因相同) . 示例是否应该包含 2 个文件名,一个用于源和目标?只是一个有趣的例子,但目前看起来解决方案是覆盖你想要移入或移出的文件,如果你把这个例子放在一起。【参考方案2】:

对于它的价值,一些进一步的概念:

    在 Windows 上,如果目标目录存在,renameTo() 似乎会失败,即使它是空的。这让我感到惊讶,因为我曾在 Linux 上尝试过,如果目标存在,只要它为空,renameTo() 就会成功。

    (显然我不应该假设这种事情在不同平台上都一样;这正是 Javadoc 警告的内容。)

    如果您怀疑可能存在一些挥之不去的文件锁,请稍等片刻,移动/重命名可能会有所帮助。 (在我们的安装程序/升级程序中,我们添加了一个“睡眠”操作和一个持续约 10 秒的不确定进度条,因为可能有一项服务挂在某些文件上)。甚至可以做一个简单的重试机制,尝试renameTo(),然后等待一段时间(可能会逐渐增加),直到操作成功或达到某个超时。

在我的例子中,大多数问题似乎已经通过考虑以上两个因素得到解决,所以我们不需要进行原生内核调用,或者类似的事情,毕竟。

【讨论】:

我现在接受我自己的答案,因为它描述了对我们的案例有什么帮助。不过,如果有人对 renameTo() 更普遍的问题提出了很好的答案,请随时发布,我很乐意重新考虑接受的答案。 6.5 年后,我认为是时候接受 JDK 7 answer 了,尤其是在很多人认为它有帮助的情况下。 =)【参考方案3】:

原帖要求“另一种可靠的方法,在 Windows 上使用 Java 进行快速移动/重命名,可以使用普通的 JDK 或一些外部库。”

此处尚未提及的另一个选项是 v1.3.2 或更高版本的 apache.commons.io 库,其中包括 FileUtils.moveFile()。

它抛出一个 IOException 而不是在出错时返回 boolean false。

另见big lep在this other thread的回复。

【讨论】:

另外,看起来 JDK 1.7 将包含更好的文件系统 I/O 支持。查看 java.nio.file.Path.moveTo():java.sun.com/javase/7/docs/api/java/nio/file/Path.html JDK 1.7 没有方法java.nio.file.Path.moveTo()【参考方案4】:

在我的情况下,它似乎是我自己的应用程序中的一个死对象,它保留了该文件的句柄。所以这个解决方案对我有用:

for (int i = 0; i < 20; i++) 
    if (sourceFile.renameTo(backupFile))
        break;
    System.gc();
    Thread.yield();

优点:它非常快,因为没有带有特定硬编码时间的 Thread.sleep()。

缺点:20 的限制是一些硬编码的数字。在我所有的测试中, i=1 就足够了。但可以肯定的是,我是在 20 点离开的。

【讨论】:

我做了类似的事情,但循环中的睡眠时间为 100 毫秒。【参考方案5】:

我知道这似乎有点 hacky,但是对于我一直需要它的东西,似乎缓冲的读取器和写入器在制作文件时没有问题。

void renameFiles(String oldName, String newName)

    String sCurrentLine = "";

    try
    
        BufferedReader br = new BufferedReader(new FileReader(oldName));
        BufferedWriter bw = new BufferedWriter(new FileWriter(newName));

        while ((sCurrentLine = br.readLine()) != null)
        
            bw.write(sCurrentLine);
            bw.newLine();
        

        br.close();
        bw.close();

        File org = new File(oldName);
        org.delete();

    
    catch (FileNotFoundException e)
    
        e.printStackTrace();
    
    catch (IOException e)
    
        e.printStackTrace();
    


非常适合作为解析器的一部分的小文本文件,只需确保 oldName 和 newName 是文件位置的完整路径。

干杯 仙人掌

【讨论】:

【参考方案6】:

以下代码不是“替代品”,但在 Windows 和 Linux 环境中都可靠地为我工作:

public static void renameFile(String oldName, String newName) throws IOException 
    File srcFile = new File(oldName);
    boolean bSucceeded = false;
    try 
        File destFile = new File(newName);
        if (destFile.exists()) 
            if (!destFile.delete()) 
                throw new IOException(oldName + " was not successfully renamed to " + newName); 
            
        
        if (!srcFile.renameTo(destFile))        
            throw new IOException(oldName + " was not successfully renamed to " + newName);
         else 
                bSucceeded = true;
        
     finally 
          if (bSucceeded) 
                srcFile.delete();
          
    

【讨论】:

嗯,即使renameTo(或destFile.delete)失败并且该方法抛出IOException,此代码也会删除srcFile;我不确定这是否是个好主意。 @Jonik,Thanx,修复了重命名失败时不删除 src 文件的代码。 感谢分享这个修复了我在 Windows 上的重命名问题。【参考方案7】:

在 Windows 上,我使用Runtime.getRuntime().exec("cmd \\c "),然后使用命令行重命名功能来实际重命名文件。它更加灵活,例如,如果您想将目录中所有 txt 文件的扩展名重命名为 bak,只需将其写入输出流:

重命名 *.txt *.bak

我知道这不是一个好的解决方案,但显然它一直对我有用,比 Java 内联支持要好得多。

【讨论】:

超级棒,这个好多了!谢谢! :-)【参考方案8】:

为什么不……

import com.sun.jna.Native;
import com.sun.jna.Library;

public class RenamerByJna 
    /* Requires jna.jar to be in your path */

    public interface Kernel32 extends Library 
        public boolean MoveFileA(String existingFileName, String newFileName);
    

    public static void main(String[] args) 
        String path = "C:/yourchosenpath/";
        String existingFileName = path + "test.txt";
        String newFileName = path + "renamed.txt";

        Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
            kernel32.MoveFileA(existingFileName, newFileName);
        

适用于 nwindows 7,如果 existingFile 不存在则不执行任何操作,但显然可以更好地解决此问题。

【讨论】:

【参考方案9】:

我遇到了类似的问题。文件在 Windows 上被复制而不是移动,但在 Linux 上运行良好。我通过在调用 renameTo() 之前关闭打开的 fileInputStream 解决了这个问题。在 Windows XP 上测试。

fis = new FileInputStream(originalFile);
..
..
..
fis.close();// <<<---- Fixed by adding this
originalFile.renameTo(newDesitnationForOriginalFile);

【讨论】:

【参考方案10】:

在我的例子中,错误出现在父目录的路径中。也许是一个错误,我不得不使用子字符串来获得正确的路径。

        try 
            String n = f.getAbsolutePath();
            **n = n.substring(0, n.lastIndexOf("\\"));**
            File dest = new File(**n**, newName);
            f.renameTo(dest);
         catch (Exception ex) 
           ...

【讨论】:

【参考方案11】:

嗯,我找到了一个非常直接的解决这个问题的方法 -

boolean retVal = targetFile.renameTo(new File("abcd.xyz"));
while(!retVal) 
    retVal= targetFile.renameTo(new File("abcd.xyz"));

按照 Argeman 的建议,您可以放置​​一个计数器并限制 while 循环将运行的次数,以便在另一个 Windows 进程正在使用某个文件的情况下它不会进入无限循环。

int counter = 0;
boolean retVal = targetFile.renameTo(new File("abcd.xyz"));
while(!retVal && counter <= 10) 
        retVal = targetFile.renameTo(new File("abcd.xyz"));
        counter = counter + 1;

【讨论】:

虽然这在 99% 的情况下都可以正常工作,但很容易发生 Windows 不允许您删除某些文件的情况。无论如何,我都会添加一些超时,也许还会让其他线程有机会做某事(还有等待或其他东西,以降低 CPU 压力会有所帮助)【参考方案12】:

我知道这很糟糕,但另一种方法是创建一个 bat 脚本,输出简单的内容,如“SUCCESS”或“ERROR”,调用它,等待它执行,然后检查其结果。

Runtime.getRuntime().exec("cmd /c start test.bat");

这个话题可能很有趣。还要检查 Process 类,了解如何读取不同进程的控制台输出。

【讨论】:

【参考方案13】:

你可以试试robocopy。这并不完全是“重命名”,但它非常可靠。

Robocopy 专为目录或目录树的可靠镜像而设计。它具有确保复制所有 NTFS 属性和属性的功能,并包含用于可能中断的网络连接的额外重启代码。

【讨论】:

谢谢。但由于 robocopy 不是 Java 库,因此从我的 Java 代码中(捆绑并)使用它可能不是很容易......【参考方案14】:

要移动/重命名文件,您可以使用此功能:

BOOL WINAPI MoveFile(
  __in  LPCTSTR lpExistingFileName,
  __in  LPCTSTR lpNewFileName
);

在 kernel32.dll 中定义。

【讨论】:

我觉得在 JNI 中包装它的麻烦比在 Process 装饰器中包装 robocopy 所需的努力更大。 是的,这是你为抽象付出的代价——当它泄漏时,它泄漏得很好 =D 谢谢,如果不太复杂,我可能会考虑这个。我没用过JNI,也找不到在SO上调用Windows内核函数的好例子,所以发了这个问题:***.com/questions/1000723/… 您可以尝试像johannburkard.de/software/nativecall 这样的通用JNI 包装器,因为这是一个非常简单的函数调用。【参考方案15】:
 File srcFile = new File(origFilename);
 File destFile = new File(newFilename);
 srcFile.renameTo(destFile);

以上是简单的代码。我已经在 Windows 7 上进行了测试,并且运行良好。

【讨论】:

在某些情况下 renameTo() 确实可靠地工作;这就是问题的重点。

以上是关于Windows 上可靠的 File.renameTo() 替代方案?的主要内容,如果未能解决你的问题,请参考以下文章

在 Windows 上可靠地暂停所有正在播放的媒体

如何在Windows 8.1 / 10上可靠地窃取/重新获得MFC /桌面应用程序的焦点?

Delphi 4 Pro 能否在 Windows XP(或更高版本)上可靠地安装和使用?

淘宝上的超级懒人版黑苹果系统可靠吗

用于调度代码重复运行的 Windows 任务调度程序有多可靠?

DEXTUpload .NET增强的上传速度和可靠性