java.nio.file.Files.delete(Path path) - 偶尔无法使用SimpleFileVisitor递归删除目录

19

尝试解决从 Java递归删除目录 中获取的偶发性java.nio.file.DirectoryNotEmptyException问题。

代码(由@TrevorRobinson提供):

static void removeRecursive(Path path) throws IOException {
    Files.walkFileTree(path, new SimpleFileVisitor<Path>() {

        final Logger logger = LoggerFactory.getLogger(this.getClass());
        @Override
        public FileVisitResult visitFile(Path file,
                BasicFileAttributes attrs) throws IOException {
            logger.warn("Deleting " + file.getFileName());
            Files.delete(file);
            logger.warn("DELETED " + file.getFileName());
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) {
            // try to delete the file anyway, even if its attributes could
            // not be read, since delete-only access is theoretically possible
            // I NEVER SEE THIS
            logger.warn("Delete file " + file + " failed", exc);
            try {
                Files.delete(file);
            } catch (IOException e) {
                logger.warn(
                    "Delete file " + file + " failed again", exc);
            }
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                throws IOException {
            if (exc == null) {
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
            // directory iteration failed; propagate exception
            throw exc;
        }
    });
}

呼叫:

try {
    removeRecursive(Paths.get(unzipDirPath));
} catch (IOException e) {
    String msg = "Failed to delete folder " + unzipDirPath;
    if (e instanceof java.nio.file.DirectoryNotEmptyException) {
        msg += ". Still contains : ";
        final File[] listFiles = Paths.get(unzipDirPath).toFile().listFiles();
        if (listFiles != null) for (File file : listFiles) {
            msg += file.getAbsolutePath() + "\n";
        }
    }
    log.error(msg, e);
}

打印(每20/40次迭代打印一次):

22:03:34.190 [http-bio-8080-exec-47] WARN  g.u.d.m.server.servlets.Controller$1 - Deleting batt
22:03:34.192 [http-bio-8080-exec-47] WARN  g.u.d.m.server.servlets.Controller$1 - DELETED batt
22:03:34.192 [http-bio-8080-exec-47] WARN  g.u.d.m.server.servlets.Controller$1 - Deleting wifi
22:03:34.193 [http-bio-8080-exec-47] WARN  g.u.d.m.server.servlets.Controller$1 - DELETED wifi
22:03:34.196 [http-bio-8080-exec-47] ERROR g.u.d.m.s.s.DataCollectionServlet - Failed to delete folder C:\yada\. Still contains : C:\yada\dir\wifi

java.nio.file.DirectoryNotEmptyException: C:\yada\dir
    at sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:265) ~[na:1.7.0_45]
    at sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:103) ~[na:1.7.0_45]
    at java.nio.file.Files.delete(Files.java:1077) ~[na:1.7.0_45]
    at gr.uoa.di.monitoring.server.servlets.Controller$1.postVisitDirectory(Controller.java:128) ~[Controller$1.class:na]
    at gr.uoa.di.monitoring.server.servlets.Controller$1.postVisitDirectory(Controller.java:1) ~[Controller$1.class:na]
    at java.nio.file.FileTreeWalker.walk(FileTreeWalker.java:224) ~[na:1.7.0_45]
    at java.nio.file.FileTreeWalker.walk(FileTreeWalker.java:199) ~[na:1.7.0_45]
    at java.nio.file.FileTreeWalker.walk(FileTreeWalker.java:69) ~[na:1.7.0_45]
    at java.nio.file.Files.walkFileTree(Files.java:2600) ~[na:1.7.0_45]
    at java.nio.file.Files.walkFileTree(Files.java:2633) ~[na:1.7.0_45]
    at gr.uoa.di.monitoring.server.servlets.Controller.removeRecursive(Controller.java:96) ~[Controller.class:na]
    at gr.uoa.di.monitoring.server.servlets.DataCollectionServlet.doPost(DataCollectionServlet.java:153) ~[DataCollectionServlet.class:na]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:641) [servlet-api.jar:na]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:722) [servlet-api.jar:na]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305) [catalina.jar:7.0.32]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) [catalina.jar:7.0.32]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) [catalina.jar:7.0.32]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) [catalina.jar:7.0.32]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472) [catalina.jar:7.0.32]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168) [catalina.jar:7.0.32]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99) [catalina.jar:7.0.32]
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:929) [catalina.jar:7.0.32]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) [catalina.jar:7.0.32]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407) [catalina.jar:7.0.32]
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1002) [tomcat-coyote.jar:7.0.32]
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:585) [tomcat-coyote.jar:7.0.32]
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310) [tomcat-coyote.jar:7.0.32]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [na:1.7.0_45]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [na:1.7.0_45]
    at java.lang.Thread.run(Thread.java:744) [na:1.7.0_45]

请注意,wifi被报告为已删除-更奇怪的是,有时我会得到以下信息:

无法删除文件夹C:\yada。仍包含:C:\yada\dir

java.nio.file.DirectoryNotEmptyException: C:\yada\dir

我倾向于得出这样的结论:有时删除需要太长时间-换句话说,问题在于java.nio.file.Files.delete(Path path)不会阻塞(因此当它的时间到来时,C:\yada\dir仍包含文件,这些文件有时会在我查询它时被删除)。那么我该如何解决这个问题?

另外java.nio.file.Files.delete(Path path)是否需要抛出异常? 文档中指出:

在某些操作系统上,当文件正在被此Java虚拟机或其他程序打开和使用时,可能无法删除该文件。

似乎不需要在这种情况下抛出异常。 java.nio.file.Files.delete(Path path)是否需要抛出异常?


你可能会在这个帖子中找到答案:https://dev59.com/iXI-5IYBdhLWcg3wsKh4 - crownjewel82
@crownjewel82:这是关于java.io.File.delete()的,它返回布尔值! - Mr_and_Mrs_D
它还会根据问题抛出不同的异常:FileNotFoundException、SecurityException等。您可以捕获这些异常并适当地做出响应。 - crownjewel82
@crownjewel82:我在询问一种不同的方法....... - Mr_and_Mrs_D
两种方法都可以完成相同的任务:http://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#delete(java.nio.file.Path) - crownjewel82
奇怪的有趣事实:我遇到了类似的行为 - 对于与OP问题中相同名称的目录 - wifi。我可以在特定的目录树中始终重现此问题,并且对于Windows上名为wifi的目录,它总是会失败。在我的情况下,解决方案是依赖于WatcherService从文件系统本身获取删除确认事件。但是,在应用程序的另一部分中,我已经利用了WatcherService - 由于dir锁可能实际上是由于此API引起的时间问题。 - predi
5个回答

15

我之前遇到了同样的问题,后来发现在代码中删除某个文件夹时,其他地方存在一个未关闭的文件夹流对象导致问题出现。下面这段代码返回的是该流对象:

Files.list(Path)

如果使用该方法,必须关闭它,因此请确保在您的代码中使用try-with-resources结构。

所以我认为删除不会花费太长时间,我尝试了等待再次尝试目录删除但没有成功。最有可能的是您自己的程序锁定了该资源。结果是尽管delete调用成功返回(看起来像Windows将在您自己的程序释放它后最终删除该文件),但它并未完成删除,然后当然包含该文件的目录无法删除,因为它确实不为空。


我通常对这种东西很小心 - “我尝试等待再次尝试删除目录,但没有成功” 意味着即使等待,该方法也没有失败吗?在这种情况下,可能是因为(文件没有关闭),虽然我不能确定,因为代码是很久以前写的 - 不过我会再次查看它。 - Mr_and_Mrs_D
2
很荒谬,对于Stream<T>的终端操作不会调用close()。在之前的Files.list(path)上使用try-with-resources解决了这个问题。 - Sipka
谢谢,这很容易被忽视。调试了30分钟后,发现这是原因。 - namero999

3

我知道这个帖子很旧,但我遇到了同样的问题,花了相当长的时间才解决。 我认为这种错误是由于时间问题(似乎只在Windows上发生),所以我在postVisitDirectory方法中加入了一个暂停。这样就可以解决了,以下是我最终得出的方法:

一个可以删除而不抛出DirectoryNotEmptyException的方法:

private boolean isDeleted(Path dir) throws IOException {
    boolean deleted = false;
    try {
        Files.delete(dir);
        deleted = true;
    } catch (DirectoryNotEmptyException e) {
    // happens sometimes if Windows is too slow to remove children of a directory
    deleted = false;
    }
    return deleted;
}

及其在循环中的使用:

public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
    if (e == null) {
        int maxTries = 5;
        int count = 0;
        boolean deleted = false;
        do {
            if ((deleted = this.isDeleted(dir))) {
                break;
            } else {
                // wait a bit and try again
                count++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e1) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        } while (count < maxTries);
        // gone?
        if (!deleted) {
            throw new DirectoryNotEmptyException(dir.toString());
        }
        // go ahead
        return FileVisitResult.CONTINUE;
    }
    throw e;
}

安迪


2

我想评论@user3485962的答案,但是我的积分不够!

这里有一个使用try with resources的例子,它在创建列表后关闭流:

    /**  
     * @param dir The directory to list.
     * @return A list of files and directories in dir.
     * @throws IOException If encountered.
     */
    public static List<Path> getList(Path dir) throws IOException {
        try (Stream<Path> s = Files.list(dir)) {
            return s.collect(Collectors.toList());
        }
    }

1
这里是与此答案相关的答案链接:https://dev59.com/Ieo6XIcBkEYKwwoYLhPO#32486055 - Andy Turner

0

你可以暂时将当前迭代的文件名存储在一个变量中,并对DirectoryNotEmptyException执行try-catch。当出现此异常时,捕获它并抛出自己的异常,指定该文件。

try {
 Files.delete(file);
} catch (DirectoryNotEmptyException  e) {
 throw new MySpecificException(file.getFileName());
}

class MySpecificException extends Exception {
 public MySpecificException() { }

 public MySpecificException(string filename) {
  super(filename);
 }  
}

通过e.getMessage();可以获取文件名。

我假设Files.delete()在遇到无法删除的文件时会继续删除目录中的文件。如果是这种情况,我的方法仍然有效:只需返回目录中所有文件的列表而不是目录的文件名。它应该只包含无法删除的文件,您仍然有解决方案。


离题了 - 你正在捕获的file是非空的目录 - 递归方法应该删除目录中的所有文件,并且这里失败了 - 我想知道哪个_文件_无法被删除以及原因! - Mr_and_Mrs_D
我想知道为什么,正如问题中明确说明的那样 - 我已经有一个列表 - 请参见我的编辑。天哪,我甚至不确定删除不会抛出异常 - 文档不清楚。 - Mr_and_Mrs_D
你可能想对那些试图帮助你的人更加礼貌。DirectoryNotEmptyException被抛出是因为你试图删除一个有文件的文件夹。这是以一种方式编程的,它只是跳过无法删除的文件:不会引发任何事件,也不会抛出异常。如果你想检查原因,可以使用File.canRead()file.CanWrite()File.canExecute()来查看是否存在权限问题和/或是否正在使用。这些应该是文件无法删除的唯一原因。 - Jeroen Vannevel
请注意,您没有回答问题 - 我认为不仅不仔细阅读问题也不礼貌(您可能想要关注修订版本,例如我说我能够列出文件自编辑0以来:http://stackoverflow.com/revisions/19935624/1-而您却回复我说我可以列出文件,完全错过了重点!)。我哪里粗鲁了?天哪,去看(不清楚的)文档吧。无论如何 - 回到问题上:“这是以一种简单跳过无法删除的文件的方式编程” - 这是_什么_? - Mr_and_Mrs_D

-1

对我上面评论的一个扩展。

当Java在执行某些操作时出现问题,它会抛出异常。异常与所有其他类型一样,都可以继承。API将指定每个方法抛出的已检查异常。在java.iojava.nio中的方法通常会抛出IOException或其子类之一。如果您想创建一个方法,以告诉您为什么文件操作(在这种情况下是删除)失败,您可以尝试以下操作:

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
       //You don't need to try to delete the file again since this method was called
       //because the deletion failed. Instead...
       if (exc instanceof NoSuchFileException) {
            System.out.println("Could not find the file: " + file);
       } else if (exc instanceof DirectoryNotEmptyException) {
            System.out.println("The directory [+ " file + "] was not empty.");
       } else {
            System.out.println("Could not delete file [" + file
                + "]. There was a problem communicating with the file system");
       }

       return FileVisitResult.CONTINUE;
}

您可以根据需要更改实际程序响应,但这应该给您一个大致的想法。


你不需要再尝试删除文件,因为已经调用了这个方法。请检查我的答案中的链接。此外,我已经完成了这个任务,与我的实际问题无关——即“如果删除失败,是否会抛出异常”?文档并不清楚。 - Mr_and_Mrs_D
好奇一下,你知道返回和抛出的区别吗? - crownjewel82

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接