Java Internals & Concurrency/Duyệt thư mục và theo dõi file — walk, atomic swap, WatchService
41/75
Bài 41 / 75~13 phútI/O & NIOMiễn phí lượt xem

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: inotify syscall.
  • macOS: FSEvents framework.
  • Windows: ReadDirectoryChangesW Win32 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 inotify limit (default 8k watch per user) — watch nhiều folder cần tăng fs.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

📚 Deep Dive Oracle

Spec / reference chính thức:

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ới SKIP_SUBTREE, TERMINATE cho 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: BasicFileAttributes platform-independent; PosixFileAttributes/DosFileAttributes theo OS, throw UnsupportedOperationException nế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.
  • WatchService theo dõi thay đổi realtime (inotify Linux, FSEvents macOS); giới hạn: không auto-watch subdir, event có thể lost, có OVERFLOW event.
  • Production cần reliability → kết hợp watch + polling backup.

9. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao Files.walk phải close, trong khi nó "chỉ duyệt" chứ không mở file nào để đọc?
  2. Q2
    Pattern atomic swap cho ghi file hoạt động thế nào, và khi nào cần dùng?
  3. Q3
    Khi nào dùng Files.walkFileTree thay Files.walk?
  4. Q4
    Khi nào Files.find nhanh hơn Files.walk().filter(...), và vì sao?
  5. Q5
    WatchService 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

Đặt 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

Bài tiếp theo

Serialization — cơ chế, serialVersionUID và transient