文件遍历与监听
实际开发中经常需要遍历目录、查找文件、统计大小,或者监听目录变化。Java NIO.2 提供了比较完整的文件树遍历和目录监听能力。
# 1. Files.list
Files.list 只遍历当前目录一层。
try (Stream<Path> stream = Files.list(Path.of("logs"))) {
stream
.filter(Files::isRegularFile)
.forEach(System.out::println);
}
注意 Stream<Path> 需要关闭,所以要使用 try-with-resources。
# 2. Files.walk
Files.walk 可以递归遍历目录树。
try (Stream<Path> stream = Files.walk(Path.of("logs"))) {
stream
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".log"))
.forEach(System.out::println);
}
可以限制遍历深度:
Files.walk(Path.of("logs"), 2);
Files.walk 写法简洁,适合中小规模遍历。超大目录树或需要精细控制异常时,可以使用 walkFileTree。
# 3. Files.walkFileTree
walkFileTree 使用访问者模式遍历文件树。
Files.walkFileTree(Path.of("logs"), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().endsWith(".log")) {
System.out.println(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("visit failed: " + file);
return FileVisitResult.CONTINUE;
}
});
FileVisitResult 可以控制遍历行为:
CONTINUE:继续。TERMINATE:终止。SKIP_SUBTREE:跳过当前子树。SKIP_SIBLINGS:跳过同级剩余节点。
# 4. 删除非空目录
删除非空目录可以后序遍历:
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
删除操作有风险,生产环境要增加路径校验、权限控制、日志记录和保护机制。
# 5. WatchService
WatchService 可以监听目录变化。
WatchService watchService = FileSystems.getDefault().newWatchService();
Path dir = Path.of("config");
dir.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println(event.kind() + " -> " + event.context());
}
boolean valid = key.reset();
if (!valid) {
break;
}
}
# 6. WatchService 注意点
- 监听的是目录,不是单个文件。
- 默认不递归监听子目录。
- 不同操作系统实现细节不同。
- 高频变更可能合并或丢失事件。
- 生产配置热加载要做防抖和完整性校验。
# 专家实践清单
| 场景 | 推荐方式 | 注意点 |
|---|---|---|
| 列出一级目录 | Files.list | Stream 要关闭 |
| 简单递归遍历 | Files.walk | 大目录要限制深度 |
| 复杂遍历控制 | walkFileTree | 可处理失败、跳过子树 |
| 目录监听 | WatchService | 不是可靠消息队列 |
遍历与监听的关系:
首次启动
│
├─ 全量遍历建立初始状态
▼
运行期间
│
└─ WatchService 监听增量变化
# Tips 快问快答
Q:Files.list 是否递归?
A:不递归,只列出当前目录的一级内容。
Q:Files.walk 返回什么?
A:返回惰性的 Stream<Path>,需要关闭。
Q:什么时候用 walkFileTree?
A:需要精细控制递归、错误处理、跳过目录或访问文件属性时。
Q:遍历大目录最大的风险是什么? A:文件数量巨大导致耗时、内存、句柄和权限问题。
Q:WatchService 能保证不丢事件吗?
A:不能。它不是可靠消息系统,高频变更可能合并或丢失。
Q:监听子目录需要额外注册吗? A:通常需要,每个要监听的目录都要注册。
Q:配置热加载为什么要防抖? A:编辑器保存文件可能产生多次事件,防抖能避免重复加载半成品。
Q:可靠同步目录应该只靠 WatchService 吗? A:不应该,通常要结合定期全量校验或扫描补偿。
# 7. 总结
简单遍历使用 Files.list 或 Files.walk,复杂遍历使用 walkFileTree。目录变化监听可以使用 WatchService,但要理解它不是可靠消息队列,不能把它当作强一致事件系统。