CodeGym /Các khóa học /JAVA 25 SELF /Theo dõi thay đổi trong hệ thống tệp: WatchService

Theo dõi thay đổi trong hệ thống tệp: WatchService

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

WatchService là một phần của Java NIO (New IO), cho phép theo dõi thay đổi trong hệ thống tệp theo thời gian thực. Có thể hình dung nó như một hệ thống báo động cho thư mục: hễ ai đó thêm, xóa hoặc sửa tệp — bạn sẽ nhận thông báo ngay. Tính năng này xuất hiện từ Java 7 cùng với NIO.2; trước đó, các lập trình viên phải tự thăm dò thư mục (polling) hoặc dùng thư viện bên thứ ba.

WatchService có rất nhiều ứng dụng thực tế: giúp tự động xử lý tệp mới, ghi log và sao lưu, đồng bộ thư mục với máy chủ hoặc đám mây, cũng như theo dõi các thay đổi trong tệp cấu hình.

Đăng ký thư mục để theo dõi

Để bắt đầu theo dõi thay đổi, bạn cần:

  1. Lấy một thể hiện WatchService.
  2. Đăng ký thư mục cần theo dõi và chỉ ra các sự kiện bạn quan tâm.

Lấy WatchService

import java.nio.file.*;

WatchService watchService = FileSystems.getDefault().newWatchService();

Đăng ký thư mục

Để đăng ký, dùng phương thức register của đối tượng Path:

Path dir = Paths.get("data/uploads");
dir.register(
    watchService,
    StandardWatchEventKinds.ENTRY_CREATE,   // tạo tệp/thư mục
    StandardWatchEventKinds.ENTRY_DELETE,   // xóa tệp/thư mục
    StandardWatchEventKinds.ENTRY_MODIFY    // sửa đổi tệp/thư mục
);

Giải thích:

  • ENTRY_CREATE — có gì đó được thêm vào.
  • ENTRY_DELETE — có gì đó bị xóa.
  • ENTRY_MODIFY — có tệp bị sửa đổi (ví dụ, thêm nội dung).

Quan trọng! WatchService chỉ theo dõi một thư mục tại một thời điểm (không bao gồm thư mục con). Nếu muốn theo dõi toàn bộ cây thư mục — bạn phải đăng ký từng thư mục con riêng.

2. Xử lý sự kiện: vòng lặp chờ đợi

Bây giờ, khi chúng ta đã thiết lập theo dõi (giống “hàng xóm-canh chừng” hơn là kiểu “Big Brother”), ta có thể chờ các sự kiện. WatchService hiện thực mẫu “hàng đợi sự kiện”: ngay khi có điều gì xảy ra — sự kiện sẽ được đưa vào hàng đợi.

Vòng lặp chính

while (true) {
    // Chờ sự kiện xuất hiện (gọi chặn)
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        // Loại sự kiện: tạo, xóa, sửa đổi
        WatchEvent.Kind<?> kind = event.kind();

        // Tên tệp/thư mục (Path, tương đối so với thư mục được theo dõi)
        Path filename = (Path) event.context();

        if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
            System.out.println("Đã tạo tệp/thư mục: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
            System.out.println("Đã xóa tệp/thư mục: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
            System.out.println("Đã sửa đổi tệp/thư mục: " + filename);
        }
    }

    // Nhớ reset key, nếu không việc theo dõi sẽ dừng!
    boolean valid = key.reset();
    if (!valid) {
        break; // Thư mục không còn khả dụng, thoát
    }
}

Nó hoạt động như thế nào?

  • WatchService.take() — chặn luồng cho đến khi có sự kiện (có thể dùng poll() cho chế độ không chặn).
  • key.pollEvents() — danh sách tất cả sự kiện đã tích lũy.
  • event.context() — tên tệp hoặc thư mục đã thay đổi (tương đối so với thư mục được theo dõi).
  • Sau khi xử lý sự kiện, nhất định phải gọi key.reset(). Nếu thư mục đã bị xóa hoặc không còn khả dụng, reset() sẽ trả về false — có thể kết thúc vòng lặp.

Ví dụ đầy đủ: theo dõi thư mục "data/uploads"

Hãy thêm vào ứng dụng học tập của chúng ta một “báo động” đơn giản cho thư mục tải lên:

import java.nio.file.*;
import java.io.IOException;

public class WatcherDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        Path dir = Paths.get("data/uploads");
        if (!Files.exists(dir)) {
            Files.createDirectories(dir);
        }

        WatchService watchService = FileSystems.getDefault().newWatchService();
        dir.register(
            watchService,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_DELETE,
            StandardWatchEventKinds.ENTRY_MODIFY
        );

        System.out.println("Đang theo dõi thư mục " + dir.toAbsolutePath());

        while (true) {
            WatchKey key = watchService.take(); // chờ sự kiện

            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();
                Path filename = (Path) event.context();
                System.out.printf("[%s] %s\n", kind.name(), filename);
            }

            boolean valid = key.reset();
            if (!valid) {
                System.out.println("Thư mục không còn khả dụng, dừng theo dõi.");
                break;
            }
        }
    }
}

Hãy thử: Chạy đoạn mã này và thử tạo, xóa hoặc sửa một tệp trong thư mục "data/uploads". Chương trình sẽ phản hồi ngay!

3. Giới hạn và đặc điểm của WatchService

Chỉ một thư mục, không kèm thư mục con

WatchService chỉ theo dõi đúng thư mục bạn đã đăng ký. Nếu trong đó có các thư mục con, những thay đổi bên trong chúng sẽ không được phát hiện — bạn cần đăng ký từng thư mục con riêng.

Làm thế nào?
Nếu bạn muốn theo dõi toàn bộ cây thư mục, bạn sẽ phải duyệt qua tất cả thư mục con và đăng ký từng cái một. Ví dụ, khi tạo thư mục con mới — hãy đăng ký nó ngay.

Khác biệt giữa các hệ điều hành

Windows: WatchService hoạt động khá ổn định, nhưng đôi khi có thể “gộp” nhiều sự kiện thành một (ví dụ khi sao chép tệp lớn).

Linux/macOS: Việc hiện thực dựa trên cơ chế hệ thống (inotify, kqueue). Đôi khi sự kiện có thể đến trễ, hoặc ngược lại, quá nhiều (chẳng hạn ENTRY_MODIFY ở mỗi lần lưu).

Chỉ sự kiện theo tên

WatchService chỉ cung cấp tên đối tượng bị thay đổi (tương đối so với thư mục được theo dõi), chứ không cho biết chi tiết bên trong tệp đã thay đổi ra sao. Nếu cần biết chính xác thay đổi, hãy đọc tệp thủ công.

Mất sự kiện khi quá tải

Nếu trong thư mục có quá nhiều thay đổi trong thời gian ngắn (ví dụ sao chép hàng nghìn tệp), hàng đợi sự kiện có thể bị tràn và một phần sự kiện sẽ bị mất. Với các tác vụ quan trọng, nên bổ sung các kiểm tra bổ sung.

4. Ví dụ thực tế

Xử lý tự động tệp mới

Giả sử bạn viết một chương trình cần tự động xử lý các ảnh mới xuất hiện trong thư mục "photos/incoming".

Path dir = Paths.get("photos/incoming");
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);

while (true) {
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
            Path filename = (Path) event.context();
            if (filename.toString().endsWith(".jpg")) {
                System.out.println("Ảnh mới: " + filename);
                // Ở đây có thể thêm xử lý: sao chép, nén, phân tích, v.v.
            }
        }
    }
    key.reset();
}

Hiện thực một logger thay đổi đơn giản

Có thể lưu mọi sự kiện vào một tệp log riêng:

import java.nio.file.*;
import java.io.*;
import java.time.LocalDateTime;

public class SimpleLogger {
    public static void main(String[] args) throws IOException, InterruptedException {
        Path dir = Paths.get("logs/monitored");
        Files.createDirectories(dir);

        Path logFile = Paths.get("logs/changes.log");
        try (BufferedWriter writer = Files.newBufferedWriter(logFile, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
            WatchService watchService = FileSystems.getDefault().newWatchService();
            dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);

            System.out.println("Đang theo dõi " + dir);

            while (true) {
                WatchKey key = watchService.take();

                for (WatchEvent<?> event : key.pollEvents()) {
                    String log = String.format("%s [%s] %s\n",
                        LocalDateTime.now(), event.kind().name(), event.context());
                    writer.write(log);
                    writer.flush();
                    System.out.print(log);
                }
                key.reset();
            }
        }
    }
}

Theo dõi việc tạo thư mục con mới (và đăng ký chúng)

Nếu trong thư mục được theo dõi tạo ra thư mục con mới, bạn có thể đăng ký nó ngay để tiếp tục giám sát:

if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
    Path createdPath = dir.resolve((Path) event.context());
    if (Files.isDirectory(createdPath)) {
        createdPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                             StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
        System.out.println("Bắt đầu theo dõi thư mục con mới: " + createdPath);
    }
}

5. Những điểm quan trọng và lỗi thường gặp

Lỗi số 1: quên gọi key.reset(). Nếu không reset key sau khi xử lý sự kiện, việc theo dõi thư mục sẽ dừng và bạn sẽ không nhận thêm bất kỳ sự kiện nào. Đây là cú “gài bẫy” kinh điển cho người mới: tưởng như mọi thứ hoạt động, rồi — bùm! — chương trình im re.

Lỗi số 2: bỏ qua ngoại lệ. Làm việc với hệ thống tệp luôn tiềm ẩn bất ngờ: thư mục có thể bị xóa, ổ đĩa — bị tháo, quyền — bị thay đổi. Nếu không xử lý ngoại lệ (IOException, ClosedWatchServiceException), chương trình có thể bị dừng đột ngột.

Lỗi số 3: chỉ theo dõi một thư mục. Nhiều người kỳ vọng rằng nếu đăng ký một thư mục thì tất cả thư mục con cũng sẽ được theo dõi. Không phải vậy! Nếu cần theo dõi toàn bộ cấu trúc — hãy triển khai đăng ký đệ quy.

Lỗi số 4: chặn luồng chính. WatchService.take() sẽ chặn luồng cho đến khi có sự kiện. Nếu luồng chính của chương trình còn phải làm việc khác, hãy chạy việc theo dõi trong một luồng riêng.

Lỗi số 5: mất sự kiện khi tải cao. Nếu có quá nhiều thay đổi trong thư mục, hàng đợi sự kiện có thể bị tràn. Với ứng dụng quan trọng, nên thực hiện đối chiếu định kỳ trạng thái thư mục (ví dụ, mỗi phút so sánh danh sách tệp).

Lỗi số 6: xử lý sai đường dẫn tương đối. event.context() trả về tên tệp tương đối so với thư mục được theo dõi. Nếu cần đường dẫn tuyệt đối — hãy dùng dir.resolve((Path) event.context()).

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Thao tác với thư mục
Thao tác với file và thư mục
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION