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.
Thử ngẫmtool của bạn dùng walkFileTree để tính tổng size một thư mục, nhưng visitFileFailed chỉ log rồi CONTINUE mà không cộng gì thêm. Con số size cuối cùng in ra có đáng tin không?
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.
Thử ngẫmservice của bạn tạo tmp bằng Files.createTempFile(prefix, suffix) (không truyền parent) rồi ATOMIC_MOVE sang /etc/myapp/config.json. Trên container mount /tmp là tmpfs riêng, điều gì xảy ra?
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.
Thử ngẫmmột job giải nén 10.000 file vào thư mục đang watch, và bạn nhận đúng một event OVERFLOW thay vì 10.000 ENTRY_CREATE. Danh sách file đã xử lý dừng ở đâu, và bước tiếp theo là gì?
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 có giúp bạn hiểu bản chất không?
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.
Path và Files — NIO.2 thay java.io.File
Bài tiếpSerialization — cơ chế, serialVersionUID và transient
Thảo luận & góp ý
Hỏi đáp về bài này