134 遍历
对于遍历(或访问)路径有不同的解决方案,其中一种由 NIO.2API 通过FileVisitor
接口提供。
此接口公开了一组方法,这些方法表示访问给定路径的递归过程中的检查点。通过覆盖这些检查点,我们可以干预这个过程。我们可以处理当前访问的文件/文件夹,并通过FileVisitResult枚举决定应该进一步执行的操作,该枚举包含以下常量:
CONTINUE:遍历过程应该继续(访问下一个文件、文件夹、跳过失败等)
SKIP_SIBLINGS:遍历过程应继续,而不访问当前文件/文件夹的同级
SKIP_SUBTREE:遍历过程应继续,而不访问当前文件夹中的条目
TERMINATE:遍历应该残酷地终止
FileVisitor公开的方法如下:
FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException:对每个访问的文件/文件夹自动调用
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException:在访问文件夹内容前自动调用文件夹
FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException:在目录(包括子目录)中的内容被访问后,或在文件夹的迭代过程中,发生 I/O 错误或访问被编程中止后自动调用
FileVisitResult visitFileFailed(T file, IOException exc) throws IOException:由于不同原因(如文件属性无法读取或文件夹无法打开)无法访问(访问)文件时自动调用
好的,到目前为止,很好!让我们继续几个实际的例子。
琐碎的文件夹遍历
实现FileVisitor
接口需要覆盖它的四个方法。然而,NIO.2 附带了这个接口的一个内置的简单实现,称为SimpleFileVisitor
。对于简单的情况,扩展这个类比实现FileVisitor
更方便,因为它只允许我们覆盖必要的方法。
例如,假设我们将电子课程存储在D:/learning文件夹的子文件夹中,我们希望通过FileVisitorAPI 访问每个子文件夹。如果在子文件夹的迭代过程中出现问题,我们只会抛出报告的异常。
为了塑造这种行为,我们需要覆盖postVisitDirectory()方法,如下所示:
class PathVisitor extends SimpleFileVisitor<Path> { @Override public FileVisitResult postVisitDirectory( Path dir, IOException ioe) throws IOException { if (ioe != null) { throw ioe; } System.out.println("Visited directory: " + dir); return FileVisitResult.CONTINUE; } }
为了使用PathVisitor
类,我们只需要设置路径并调用其中一个Files.walkFileTree()
方法,如下所示(这里使用的walkFileTree()
的风格得到起始文件/文件夹和相应的FileVisitor
:
Path path = Paths.get("D:/learning"); PathVisitor visitor = new PathVisitor(); Files.walkFileTree(path, visitor);
通过使用前面的代码,我们将收到以下输出:
Visited directory: D:\learning\books\ajax Visited directory: D:\learning\books\angular ...
按名称搜索文件
在计算机上搜索某个文件是一项常见的任务。通常,我们依赖于操作系统提供的工具或其他工具,但如果我们想通过编程实现这一点(例如,我们可能想编写一个具有特殊功能的文件搜索工具),那么FileVisitor
可以帮助我们以非常简单的方式实现这一点。本申请存根如下:
public class SearchFileVisitor implements FileVisitor { private final Path fileNameToSearch; private boolean fileFound; ... private boolean search(Path file) throws IOException { Path fileName = file.getFileName(); if (fileNameToSearch.equals(fileName)) { System.out.println("Searched file was found: " + fileNameToSearch + " in " + file.toRealPath().toString()); return true; } return false; } }
让我们看看主要检查点和按名称搜索文件的实现:
visitFile()是我们的主要检查站。一旦有了控制权,就可以查询当前访问的文件的名称、扩展名、属性等。需要此信息才能与搜索文件上的相同信息进行比较。例如,我们比较名字,在第一次匹配时,我们TERMINATE搜索。但是如果我们搜索更多这样的文件(如果我们知道不止一个),那么我们可以返回CONTINUE:
@Override public FileVisitResult visitFile( Object file, BasicFileAttributes attrs) throws IOException { fileFound = search((Path) file); if (!fileFound) { return FileVisitResult.CONTINUE; } else { return FileVisitResult.TERMINATE; } }
visitFile()方法不能用于查找文件夹。改用preVisitDirectory()或postVisitDirectory()方法。
visitFileFailed()是第二个重要关卡。调用此方法时,我们知道在访问当前文件时出现了问题。我们宁愿忽略任何这样的问题和搜索。停止搜索过程毫无意义:
@Override public FileVisitResult visitFileFailed( Object file, IOException ioe) throws IOException { return FileVisitResult.CONTINUE; }
preVisitDirectory()和postVisitDirectory()方法没有任何重要的任务,因此为了简洁起见,我们可以跳过它们。
为了开始搜索,我们依赖另一种风格的Files.walkFileTree()方法。这一次,我们指定搜索的起点(例如,所有根)、搜索期间使用的选项(例如,跟随符号链接)、要访问的最大目录级别数(例如,Integer.MAX_VALUE)和FileVisitor(例如,SearchFileVisitor):
Path searchFile = Paths.get("JavaModernChallenge.pdf"); SearchFileVisitor searchFileVisitor = new SearchFileVisitor(searchFile); EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); Iterable<Path> roots = FileSystems.getDefault().getRootDirectories(); for (Path root: roots) { if (!searchFileVisitor.isFileFound()) { Files.walkFileTree(root, opts, Integer.MAX_VALUE, searchFileVisitor); } }
如果您查看本书附带的代码,前面的搜索将以递归方式遍历计算机的所有根(目录)。前面的例子可以很容易地通过扩展名、模式进行搜索,或者从一些文本中查看文件内部。
删除文件夹
在试图删除文件夹之前,我们必须删除其中的所有文件。这个语句非常重要,因为它不允许我们对包含文件的文件夹简单地调用delete()
/deleteIfExists()
方法。此问题的优雅解决方案依赖于从以下存根开始的FileVisitor
实现:
public class DeleteFileVisitor implements FileVisitor { ... private static boolean delete(Path file) throws IOException { return Files.deleteIfExists(file); } }
让我们看看主要检查点和删除文件夹的实现:
visitFile()
是从给定文件夹或子文件夹删除每个文件的理想位置(如果文件不能删除,则我们只需将其传递到下一个文件,但可以随意调整代码以满足您的需要):
@Override public FileVisitResult visitFile( Object file, BasicFileAttributes attrs) throws IOException { delete((Path) file); return FileVisitResult.CONTINUE; }
只有当文件夹为空时,才可以删除它,因此postVisitDirectory()
是执行此操作的最佳位置(我们忽略任何潜在的IOException
,但可以随意调整代码以满足您的需要(例如,记录无法删除的文件夹的名称或引发异常以停止进程)):
@Override public FileVisitResult postVisitDirectory( Object dir, IOException ioe) throws IOException { delete((Path) dir); return FileVisitResult.CONTINUE; }
在visitFileFailed()
和preVisitDirectory()
中,我们只返回CONTINUE
。
删除文件夹时,在D:/learning
中,我们可以调用DeleteFileVisitor
,如下所示:
Path directory = Paths.get("D:/learning"); DeleteFileVisitor deleteFileVisitor = new DeleteFileVisitor(); EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); Files.walkFileTree(directory, opts, Integer.MAX_VALUE, deleteFileVisitor);
通过将SearchFileVisitor
和DeleteFileVisitor
组合,我们可以得到一个搜索删除应用。
复制文件夹
为了复制一个文件,我们可以使用Path copy(Path source, Path target, CopyOption options) throws IOException方法。此方法使用指定如何执行复制的参数options将文件复制到目标文件。
通过将copy()方法与自定义FileVisitor相结合,我们可以复制整个文件夹(包括其所有内容)。本次定制FileVisitor存根代码如下:
public class CopyFileVisitor implements FileVisitor { private final Path copyFrom; private final Path copyTo; ... private static void copySubTree( Path copyFrom, Path copyTo) throws IOException { Files.copy(copyFrom, copyTo, REPLACE_EXISTING, COPY_ATTRIBUTES); } }
让我们看一下主要的检查点和复制文件夹的实现(注意,我们将通过复制任何可以复制的内容来进行操作,并避免抛出异常,但可以随意调整代码以满足您的需要):
在从源文件夹复制任何文件之前,我们需要复制源文件夹本身。复制源文件夹(空或不空)将导致目标文件夹为空。这是用preVisitDirectory()方法完成的完美任务:
@Override public FileVisitResult preVisitDirectory( Object dir, BasicFileAttributes attrs) throws IOException { Path newDir = copyTo.resolve( copyFrom.relativize((Path) dir)); try { Files.copy((Path) dir, newDir, REPLACE_EXISTING, COPY_ATTRIBUTES); } catch (IOException e) { System.err.println("Unable to create " + newDir + " [" + e + "]"); return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; }
visitFile()
方法是复制每个文件的最佳场所:
@Override public FileVisitResult visitFile( Object file, BasicFileAttributes attrs) throws IOException { try { copySubTree((Path) file, copyTo.resolve( copyFrom.relativize((Path) file))); } catch (IOException e) { System.err.println("Unable to copy " + copyFrom + " [" + e + "]"); } return FileVisitResult.CONTINUE; }
或者,我们可以保留源目录的属性。只有将文件复制到postVisitDirectory()
方法后才能完成(例如,保留上次修改的时间):
@Override public FileVisitResult postVisitDirectory( Object dir, IOException ioe) throws IOException { Path newDir = copyTo.resolve( copyFrom.relativize((Path) dir)); try { FileTime time = Files.getLastModifiedTime((Path) dir); Files.setLastModifiedTime(newDir, time); } catch (IOException e) { System.err.println("Unable to preserve the time attribute to: " + newDir + " [" + e + "]"); } return FileVisitResult.CONTINUE; }
如果无法访问文件,则调用visitFileFailed()。现在是检测循环链接并报告它们的好时机。通过以下链接(FOLLOW_LINKS),我们可以遇到文件树与父文件夹有循环链接的情况。这些病例通过visitFileFailed()中的FileSystemLoopException异常报告:
@Override public FileVisitResult visitFileFailed( Object file, IOException ioe) throws IOException { if (ioe instanceof FileSystemLoopException) { System.err.println("Cycle was detected: " + (Path) file); } else { System.err.println("Error occured, unable to copy:" + (Path) file + " [" + ioe + "]"); } return FileVisitResult.CONTINUE; }
把D:/learning/packt
文件夹复制到D:/e-courses
:
Path copyFrom = Paths.get("D:/learning/packt"); Path copyTo = Paths.get("D:/e-courses"); CopyFileVisitor copyFileVisitor = new CopyFileVisitor(copyFrom, copyTo); EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); Files.walkFileTree(copyFrom, opts, Integer.MAX_VALUE, copyFileVisitor);
通过组合CopyFileVisitor和DeleteFileVisitor,我们可以很容易地形成一个移动文件夹的应用。在本书附带的代码中,还有一个移动文件夹的完整示例。基于我们迄今为止积累的专业知识,代码应该是非常容易访问的,没有更多的细节。
在记录有关文件的信息(例如,处理异常的情况)时要注意,因为文件(例如,它们的名称、路径和属性)可能包含敏感信息,这些信息可能会被恶意利用。
JDK8,Files.walk()
从 JDK8 开始,Files
类用两个walk()
方法进行了丰富。这些方法返回一个由Path
惰性填充的Stream
。它通过使用给定的最大深度和选项遍历以给定起始文件为根的文件树来实现这一点:
public static Stream<Path> walk( Path start, FileVisitOption...options) throws IOException public static Stream<Path> walk( Path start, int maxDepth, FileVisitOption...options) throws IOException
例如,让我们显示从D:/learning
开始并以D:/learning/books/cdi
开头的所有路径:
Path directory = Paths.get("D:/learning"); Stream<Path> streamOfPath = Files.walk( directory, FileVisitOption.FOLLOW_LINKS); streamOfPath.filter(e -> e.startsWith("D:/learning/books/cdi")) .forEach(System.out::println);
现在,让我们计算一个文件夹的字节大小(例如,D:/learning):
long folderSize = Files.walk(directory) .filter(f -> f.toFile().isFile()) .mapToLong(f -> f.toFile().length()) .sum();
此方法为弱一致。它不会在迭代过程中冻结文件树。对文件树的潜在更新可能会反映出来,也可能不会反映出来。
135 监视路径
监视路径的变化只是可以通过 JDK7nio.2(低级的WatchService
API)实现的线程安全目标之一。
简而言之,可以通过以下两个主要步骤来观察路径的变化:
为不同类型的事件类型注册要监视的文件夹。
当WatchService检测到注册的事件类型时,它在单独的线程中处理,因此监视服务不会被阻塞。
在 API 级别,起点是WatchService接口。对于不同的文件/操作系统,这个接口有不同的风格。
这个接口与两个主要类一起工作。它们一起提供了一种方便的方法,您可以实现这种方法来将监视功能添加到特定上下文(例如,文件系统):
Watchable:实现此接口的任何对象都是可观察对象,因此可以观察其变化(例如Path)
StandardWatchEventKinds:这个类定义了标准的事件类型(这些是我们可以注册通知的事件类型:
ENTRY_CREATE:创建目录条目
ENTRY_DELETE:删除目录条目
ENTRY_MODIFY:目录条目已修改;被认为是修改的内容在某种程度上是特定于平台的,但实际上修改文件的内容应该始终触发此事件类型
OVERFLOW:一种特殊事件,表示事件可能已经丢失或丢弃
WatchService被称为观察者,我们说观察者观察可观察对象。在下面的示例中,WatchService将通过FileSystem类创建,并监视已注册的Path。
监视文件夹的更改
让我们从一个桩方法开始,该方法获取应该监视其更改的文件夹的Path
作为参数:
public void watchFolder(Path path) throws IOException, InterruptedException { ... }
当给定文件夹中出现ENTRY_CREATE、ENTRY_DELETE和ENTRY_MODIFY事件类型时,WatchService将通知我们。为此,我们需要遵循以下几个步骤:
创建WatchService以便我们可以监视文件系统这是通过FileSystem.newWatchService()完成的,如下所示:
WatchService watchService = FileSystems.getDefault().newWatchService();
注册应通知的事件类型这是通过Watchable.register()
完成的:
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
对于每个可监视对象,我们接收一个注册令牌作为WatchKey实例(监视键)。我们在注册时收到这个监视键,但是每次触发事件时WatchService都返回相关的WatchKey。
现在,我们需要等待传入事件。这是在无限循环中完成的(当事件发生时,观察者负责将相应的观察键排队等待以后检索,并将其状态更改为已发射:
while (true) { // process the incoming event types }
现在,我们需要检索监视键——检索监视键的方法至少有三种:
poll():返回队列中的下一个键并将其移除(或者,如果没有键,则返回null)。
poll(long timeout, TimeUnit unit):返回队列中的下一个键并将其删除;如果没有键,则等待指定的超时并重试。如果键仍然不可用,则返回null。
take():返回队列中的下一个键并将其删除;如果没有键,则等待某个键排队或无限循环停止:
WatchKey key = watchService.take();
接下来,我们需要检索监视键的未决事件。一个已发射状态的监视键至少有一个挂起事件,我们可以通过WatchKey.pollEvents()
方法检索并删除某个监视键的所有事件(每个事件由一个WatchEvent
实例表示):
for (WatchEvent<?> watchEvent : key.pollEvents()) { ... }
然后,我们检索关于事件类型的信息。对于每个事件,我们可以获得不同的信息(例如,事件类型、出现次数和上下文特定的信息(例如,导致事件的文件名),这对于处理事件很有用):
Kind<?> kind = watchEvent.kind(); WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent; Path filename = watchEventPath.context();
接下来,我们重置监视键。监视键的状态可以是就绪(创建时的初始状态)、已发射或无效。一旦发出信号,一个监视键就会保持这样,直到我们调用reset()方法,尝试将其放回就绪状态,接受事件的状态。如果已发射到就绪(恢复等待事件)转换成功,则reset()方法返回true;否则返回false,表示监视键可能无效。一个监视键如果不再处于活动状态,可能会处于无效状态(显式调用监视键的close()方法、关闭监视程序、删除目录等都会导致不活动):
boolean valid = key.reset(); if (!valid) { break; }
当有一个监视键处于无效状态时,就没有理由停留在无限循环中。只需调用break
即可跳出循环。
- 最后,我们关闭监视器。具体调用
WatchService
的close()
方法或依赖资源尝试,可实现如下:
try (WatchService watchService = FileSystems.getDefault().newWatchService()) { ... }
本书附带的代码将所有这些代码片段粘在一个名为FolderWatcher
的类中。结果将是一个观察者,它能够报告在指定路径上发生的创建、删除和修改事件。
为了观察路径,即D:/learning/packt
,我们只调用watchFolder()
方法:
Path path = Paths.get("D:/learning/packt"); FolderWatcher watcher = new FolderWatcher(); watcher.watchFolder(path);
运行应用将显示以下消息:
Watching: D:\learning\packt
现在,我们可以直接在此文件夹下创建、删除或修改文件,并检查通知。例如,如果我们简单地复制粘贴一个名为resources.txt
的文件,那么输出如下:
ENTRY_CREATE -> resources.txt ENTRY_MODIFY -> resources.txt
最后,不要忘记停止应用,因为它将无限期地运行(理论上)。
从这个应用开始,本书附带的源代码附带了另外两个应用。其中一个是视频捕获系统的模拟,而另一个是打印机托盘观察器的模拟。依靠我们在本节中积累的知识,理解这两个应用应该非常简单,无需进一步的细节。