Duyệt thư mục và theo dõi file — walk, atomic swap, WatchService
Files.walk/find/list duyệt cây thư mục, walkFileTree visitor pattern, file attribute theo OS. Pattern atomic swap cho ghi file an toàn và WatchService theo dõi thay đổi realtime.
TL;DR: NIO.2 có 4 cách duyệt cây thư mục: Files.walk (stream recursive), Files.find (walk + filter dùng sẵn attribute), Files.list (1 level), và Files.walkFileTree (visitor pattern — control skip/terminate từng entry). Mọi stream của Files giữ file handle — luôn try-with-resources. Hai pattern production quan trọng: atomic swap (ghi tmp cùng thư mục rồi ATOMIC_MOVE — reader không bao giờ thấy file half-written) và WatchService (nhận event create/modify/delete qua inotify/FSEvents — nhớ giới hạn: không auto-watch sub-directory, event có thể lost dưới load cao).
Ba task xuất hiện ở hầu hết tool và service thực tế: quét toàn bộ repo tìm file theo pattern (build tool, indexer), update file config mà service khác đang đọc định kỳ (hot-reload), và phản ứng ngay khi có file mới xuất hiện trong thư mục (file processor). Cả ba đều dùng java.nio.file — nhưng mỗi task có cạm bẫy riêng: stream giữ file handle, rename cross-filesystem không atomic, watch event có thể lost.
Bài này tiếp nối bài Path và Files: duyệt cây thư mục (walk, find, walkFileTree), đọc attribute theo OS, pattern atomic swap cho ghi file an toàn, và WatchService.
1. Walking directory — đi qua tree
Files.walk — recursive DFS
try (Stream<Path> paths = Files.walk(root)) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
Files.walk(root) trả stream tất cả path dưới root theo DFS (depth-first search — đi sâu hết một nhánh rồi mới sang nhánh kế). Bao gồm root, subdirectory, và file trong đó.
Giới hạn depth:
Files.walk(root, 3); // Toi da depth 3
Files.find — walk + filter trong 1 call
try (Stream<Path> java = Files.find(root, 10,
(p, attrs) -> p.toString().endsWith(".java") && attrs.isRegularFile())) {
java.forEach(System.out::println);
}
BiPredicate nhận (Path, BasicFileAttributes) — filter dùng attribute mà không cần syscall riêng (walk đã lấy attribute rồi).
Nhanh hơn Files.walk().filter(...) cho filter phức tạp.
Files.list — 1 level
try (Stream<Path> children = Files.list(dir)) {
children.forEach(System.out::println);
}
Files.list chỉ liệt kê con trực tiếp của dir, không đệ quy xuống sâu hơn — tương đương lệnh ls trong shell.
Quan trọng: mọi stream method của Files giữ file handle. Luôn try-with-resources.
2. Files.walkFileTree — visitor pattern
Control tinh vi hơn walk — decide visit/skip từng entry:
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().startsWith(".")) {
return FileVisitResult.SKIP_SUBTREE; // skip hidden directory
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Fail: " + file + " - " + exc);
return FileVisitResult.CONTINUE; // Skip file loi, tiep tuc
}
});
Dùng cho: build index, search engine, backup tool — cần control điều kiện skip/stop.
3. File attribute
Ngoài basic (size, mtime), NIO.2 hỗ trợ attribute theo OS:
// Basic - platform independent
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.creationTime();
attrs.lastModifiedTime();
attrs.size();
attrs.isRegularFile();
// POSIX (Linux, macOS)
PosixFileAttributes posix = Files.readAttributes(path, PosixFileAttributes.class);
posix.owner(); // UserPrincipal
posix.group();
Set<PosixFilePermission> perms = posix.permissions();
PosixFilePermissions.toString(perms); // "rwxr-x---"
// Set permission
Files.setPosixFilePermissions(path,
PosixFilePermissions.fromString("rw-r--r--"));
// DOS (Windows)
DosFileAttributes dos = Files.readAttributes(path, DosFileAttributes.class);
dos.isReadOnly();
dos.isHidden();
dos.isArchive();
Platform-specific attribute throw UnsupportedOperationException nếu gọi trên filesystem không hỗ trợ (vd POSIX trên NTFS).
4. Pattern atomic swap — ghi file an toàn
Use case: service đọc file config định kỳ. Bạn cần update config. Nếu reader đọc đúng lúc writer ghi giữa chừng, reader thấy half-written file → parse lỗi.
Giải pháp: ghi file tmp rồi atomic rename.
Path config = Path.of("/etc/myapp/config.json");
// 1. Ghi tmp trong cung thu muc
Path tmp = Files.createTempFile(config.getParent(), "cfg-", ".tmp");
try {
Files.writeString(tmp, newConfigJson, StandardCharsets.UTF_8);
// 2. Force xuong disk (optional, cho durability)
try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.WRITE)) {
ch.force(true);
}
// 3. Atomic rename
Files.move(tmp, config, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Files.deleteIfExists(tmp); // Clean up khi fail
throw e;
}
Cơ chế:
- Reader hoặc đọc file cũ toàn vẹn (nếu đọc trước rename), hoặc file mới toàn vẹn (nếu đọc sau). Không bao giờ half-written.
- OS
rename()syscall là atomic trong cùng filesystem.
Điều kiện: tmp và target cùng filesystem. Cross-filesystem rename không atomic — throw AtomicMoveNotSupportedException. Tạo tmp trong parent của target (config.getParent()) đảm bảo cùng filesystem.
Pattern này quan trọng cho: config hot-reload, cache file, database page, snapshot.
5. Watch service — theo dõi thay đổi
Đăng ký watch một thư mục, nhận event khi file trong đó thay đổi:
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
Path dir = Path.of("data");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watcher.take(); // Block cho event
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path filename = (Path) event.context();
System.out.println(kind + ": " + filename);
}
if (!key.reset()) break; // key invalid -> dung
}
}
Cơ chế bên dưới:
- Linux:
inotifysyscall. - macOS:
FSEventsframework. - Windows:
ReadDirectoryChangesWWin32 API.
Use case:
- Hot reload config: watch
config/→ reload khi file đổi. - File processor: watch incoming directory → process mỗi file mới upload.
- Sync tool: monitor 2 thư mục, sync thay đổi.
Giới hạn:
- Linux
inotifylimit (default 8k watch per user) — watch nhiều folder cần tăngfs.inotify.max_user_watches. - Event có thể lost dưới load cao — không guarantee 100% delivery.
- Sub-directory không auto-watch — đăng ký riêng cho từng sub-directory.
Cho production reliable, consider library như Apache Commons IO FileAlterationMonitor — wrap watch service với polling backup.
6. Pitfall tổng hợp
❌ Nhầm 1: Files.walk không close.
Files.walk(root).forEach(...); // Stream giu file handle (directory stream)
✅ Try-with-resources:
try (Stream<Path> paths = Files.walk(root)) {
paths.forEach(...);
}
❌ Nhầm 2: Atomic move cross-filesystem.
Files.move(/tmp/x, /home/user/x, ATOMIC_MOVE); // AtomicMoveNotSupportedException neu khac FS
✅ Tạo tmp trong cùng parent của target:
Path tmp = Files.createTempFile(target.getParent(), "tmp-", ".tmp");
❌ Nhầm 3: Ghi đè trực tiếp file mà reader khác đang đọc định kỳ.
Files.writeString(config, newJson); // Reader co the thay file dang ghi do
✅ Atomic swap: ghi tmp → Files.move(tmp, config, ATOMIC_MOVE, REPLACE_EXISTING).
❌ Nhầm 4: Coi WatchService là nguồn event tin cậy 100%.
// Dua hoan toan vao event de sync 2 thu muc -> mat event khi load cao
✅ Kết hợp polling định kỳ làm backup (vd FileAlterationMonitor), hoặc rescan khi key.reset() báo overflow (OVERFLOW event).
7. 📚 Deep Dive Oracle
Spec / reference chính thức:
- Files class — javadoc walk/find/list ghi rõ close requirement.
- FileVisitor — visitor contract đầy đủ (preVisit/postVisit/failed).
- WatchService — API + lưu ý OVERFLOW event.
- Tutorial: Walking the File Tree — Oracle tutorial với pattern visitor chi tiết.
- Tutorial: Watching a Directory for Changes — hands-on WatchService.
Ghi chú: Javadoc WatchService nói rõ implementation "is intended to map directly on to the native file event notification facility where available" — tức behavior (độ trễ, batching event) khác nhau giữa Linux/macOS/Windows. Tool cần chạy đa nền tảng phải test trên từng OS, không assume event đến ngay lập tức.
8. Tóm tắt
Files.walk(root)recursive DFS;Files.list(dir)1 level;Files.find(root, depth, predicate)walk + filter dùng sẵn attribute.Files.walkFileTree(root, visitor)— visitor pattern vớiSKIP_SUBTREE,TERMINATEcho control chi tiết (skip.git, handle lỗi per-file, dừng sớm).- Mọi stream của
Files(walk/list/find/lines) giữ file handle — luôn try-with-resources. - Attribute:
BasicFileAttributesplatform-independent;PosixFileAttributes/DosFileAttributestheo OS, throwUnsupportedOperationExceptionnếu filesystem không hỗ trợ. - Atomic swap: tạo tmp trong cùng parent → ghi + fsync →
Files.move(ATOMIC_MOVE). Reader không bao giờ thấy half-written. - Atomic move chỉ trong cùng filesystem — cross-filesystem throw
AtomicMoveNotSupportedException. WatchServicetheo dõi thay đổi realtime (inotify Linux, FSEvents macOS); giới hạn: không auto-watch subdir, event có thể lost, cóOVERFLOWevent.- Production cần reliability → kết hợp watch + polling backup.
9. Tự kiểm tra
- Q1Vì sao
Files.walkphải close, trong khi nó "chỉ duyệt" chứ không mở file nào để đọc? - Q2Pattern atomic swap cho ghi file hoạt động thế nào, và khi nào cần dùng?
- Q3Khi nào dùng
Files.walkFileTreethayFiles.walk? - Q4Khi nào
Files.findnhanh hơnFiles.walk().filter(...), và vì sao? - Q5WatchService dựa trên cơ chế gì của OS, và vì sao không nên tin nó 100% trong production?
Bài tiếp theo: Serialization — cơ chế, serialVersionUID và transient
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên