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.walk phải close, trong khi nó "chỉ duyệt" chứ không mở file nào để đọc?▸
Files.walk phải close, trong khi nó "chỉ duyệt" chứ không mở file nào để đọc?Để duyệt lazy, Files.walk giữ directory stream (file descriptor của thư mục) cho từng cấp đang duyệt — OS cần FD để đọc danh sách entry. Stream trả về vì thế tham chiếu tới một chồng FD đang mở, không phải snapshot trong memory.
Không close → các directory FD giữ đến khi GC dọn stream object — timing không xác định. Tool quét hàng nghìn thư mục sẽ tích lũy FD leak nhanh hơn cả leak từ Files.lines.
Fix luôn là try-with-resources:
try (Stream<Path> paths = Files.walk(root)) {
paths.filter(...).forEach(...);
}Rule chung cho NIO.2: mọi method của Files trả Stream (walk, list, find, lines) đều giữ resource — đều phải close.
Q2Pattern atomic swap cho ghi file hoạt động thế nào, và khi nào cần dùng?▸
Pattern 3 bước:
- Tạo tmp file trong cùng thư mục của target:
Files.createTempFile(target.getParent(), "tmp-", ".tmp"). Cùng parent đảm bảo cùng filesystem —ATOMIC_MOVEchỉ đảm bảo trong 1 filesystem. - Ghi data đầy đủ vào tmp, flush/fsync nếu cần durability.
- Atomic rename tmp sang target:
Files.move(tmp, target, ATOMIC_MOVE, REPLACE_EXISTING). OSrename()syscall là atomic trong cùng filesystem.
Kết quả: reader bất cứ lúc nào cũng thấy hoặc file cũ toàn vẹn, hoặc file mới toàn vẹn. Không bao giờ thấy half-written.
Khi nào dùng:
- Config hot-reload: service đọc config định kỳ. Update config không làm service parse lỗi.
- Cache file: update cache atomic, reader không thấy inconsistent state.
- Database page / WAL: essential cho durability.
- Snapshot / checkpoint: app crash giữa save không corrupt snapshot cũ.
Cross-filesystem rename → AtomicMoveNotSupportedException. Phải tạo tmp trong cùng filesystem với target. Dùng Files.createTempFile(parent, ...) không dùng Files.createTempFile(prefix, suffix) (tạo trong /tmp có thể khác filesystem).
Q3Khi nào dùng Files.walkFileTree thay Files.walk?▸
Files.walkFileTree thay Files.walk?- Files.walk: stream đơn giản, dùng stream API filter/map. Phù hợp task nhanh "tìm file theo pattern".
- Files.walkFileTree + visitor: control chi tiết — decide visit/skip/terminate từng entry. Phù hợp tool phức tạp.
Khi nào cần visitor:
- Skip sub-directory theo điều kiện: skip
.git,node_modules— không cần duyệt content.preVisitDirectory(dir, attrs) { if (dir.getFileName().toString().equals(".git")) { return SKIP_SUBTREE; } return CONTINUE; } - Handle IO error per-file: file no permission → log warning, continue thay crash cả walk.
visitFileFailed(file, exc) { log.warn("Skip " + file, exc); return CONTINUE; } - Terminate sớm: tìm thấy result, không cần duyệt tiếp.
return TERMINATE; - Post-order processing: xử lý sau khi visit hết children (vd tính size thư mục).
Tool backup, search engine, antivirus — dùng walkFileTree. Script đơn giản — Files.walk đủ.
Q4Khi nào Files.find nhanh hơn Files.walk().filter(...), và vì sao?▸
Files.find nhanh hơn Files.walk().filter(...), và vì sao?Hai cách cho cùng kết quả, khác ở chỗ filter chạy khi nào với dữ liệu nào.
Trong lúc duyệt cây, OS đã trả về metadata (BasicFileAttributes: size, mtime, isRegularFile) cùng với mỗi entry. Files.find đưa attribute đó trực tiếp vào BiPredicate — filter theo size/mtime/type không tốn syscall thêm.
Với Files.walk().filter(...), stream chỉ chứa Path. Muốn filter theo attribute phải gọi lại Files.isRegularFile(p) / Files.size(p) — mỗi call là 1 syscall stat nữa, lặp lại công việc walk đã làm.
Quét 1 triệu file với filter "file thường, lớn hơn 1MB": find tiết kiệm ~2 triệu syscall so với walk + filter. Filter chỉ theo tên file (string match) thì hai cách tương đương — không cần attribute.
Q5WatchService dựa trên cơ chế gì của OS, và vì sao không nên tin nó 100% trong production?▸
WatchService map xuống facility native của từng OS: inotify (Linux), FSEvents (macOS), ReadDirectoryChangesW (Windows). JVM không tự polling — kernel đẩy event lên, nên gần như zero cost khi không có thay đổi.
3 lý do không tin 100%:
- Event có thể lost: kernel buffer event có giới hạn. Thay đổi dồn dập (vd giải nén 10k file) → buffer tràn → nhận event
OVERFLOWthay vì từng event — phải rescan thư mục. - Không auto-watch sub-directory: tạo thư mục con mới phải register thêm — có khoảng trống thời gian giữa "thư mục xuất hiện" và "bắt đầu watch".
- Behavior khác nhau giữa OS: độ trễ, batching, loại event — macOS FSEvents gom event theo batch, Linux inotify báo từng event. Code đa nền tảng phải test riêng từng OS.
Pattern production: watch event làm trigger chính + polling định kỳ (hoặc rescan khi OVERFLOW) làm lưới an toàn — Apache Commons IO FileAlterationMonitor đóng gói sẵn cách này.
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?
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