CodeGym /Kursy /JAVA 25 SELF /Monitorowanie zmian w systemie plików: WatchService

Monitorowanie zmian w systemie plików: WatchService

JAVA 25 SELF
Poziom 40 , Lekcja 4
Dostępny

1. Wprowadzenie

WatchService to część Java NIO (New IO), która umożliwia śledzenie zmian w systemie plików w czasie rzeczywistym. Można go sobie wyobrazić jako system alarmowy dla katalogów: gdy tylko ktoś doda, usunie lub zmodyfikuje plik – od razu otrzymasz powiadomienie. Funkcjonalność pojawiła się w Java 7 wraz z NIO.2; wcześniej programiści musieli albo ręcznie odpytywać katalog (polling), albo korzystać z bibliotek zewnętrznych.

Zastosowania praktyczne WatchService są bardzo różnorodne: pomaga automatycznie przetwarzać nowe pliki, prowadzić logi i wykonywać kopie zapasowe, synchronizować katalogi z serwerem lub chmurą, a także śledzić zmiany w plikach konfiguracyjnych.

Rejestracja katalogów do monitorowania

Aby zacząć monitorować zmiany, trzeba:

  1. Uzyskać instancję WatchService.
  2. Zarejestrować wybrany katalog i wskazać, które zdarzenia nas interesują.

Uzyskanie WatchService

import java.nio.file.*;

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

Rejestrujemy katalog

Do rejestracji używamy metody register obiektu Path:

Path dir = Paths.get("data/uploads");
dir.register(
    watchService,
    StandardWatchEventKinds.ENTRY_CREATE,   // tworzenie plików/katalogów
    StandardWatchEventKinds.ENTRY_DELETE,   // usuwanie plików/katalogów
    StandardWatchEventKinds.ENTRY_MODIFY    // modyfikacja plików/katalogów
);

Wyjaśnienie:

  • ENTRY_CREATE – coś zostało dodane.
  • ENTRY_DELETE – coś zostało usunięte.
  • ENTRY_MODIFY – coś zostało zmodyfikowane (np. dopisano tekst do pliku).

Ważne! WatchService monitoruje tylko jeden katalog naraz (bez podkatalogów). Jeśli chcesz śledzić całą hierarchię – musisz zarejestrować każdy podkatalog osobno.

2. Obsługa zdarzeń: pętla oczekiwania

Teraz, gdy skonfigurowaliśmy monitorowanie (raczej jak u „sąsiada-obserwatora”, a nie w duchu „Wielkiego Brata”), można czekać na zdarzenia. WatchService implementuje wzorzec „kolejka zdarzeń”: gdy tylko coś się wydarzy – zdarzenie trafia do kolejki.

Główna pętla

while (true) {
    // Oczekujemy na pojawienie się zdarzeń (wywołanie blokujące)
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        // Typ zdarzenia: utworzenie, usunięcie, modyfikacja
        WatchEvent.Kind<?> kind = event.kind();

        // Nazwa pliku/katalogu (Path, względna względem monitorowanego katalogu)
        Path filename = (Path) event.context();

        if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
            System.out.println("Utworzono plik/katalog: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
            System.out.println("Usunięto plik/katalog: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
            System.out.println("Zmodyfikowano plik/katalog: " + filename);
        }
    }

    // Koniecznie resetujemy klucz, w przeciwnym razie monitorowanie się zakończy!
    boolean valid = key.reset();
    if (!valid) {
        break; // Katalog niedostępny, kończymy
    }
}

Jak to działa?

  • WatchService.take() – blokuje wątek do pojawienia się zdarzenia (można użyć poll() dla trybu nieblokującego).
  • key.pollEvents() – lista wszystkich nagromadzonych zdarzeń.
  • event.context() – nazwa zmienionego pliku lub katalogu (względem monitorowanego katalogu).
  • Po przetworzeniu zdarzeń koniecznie wywołujemy key.reset(). Jeśli katalog został usunięty lub stał się niedostępny, reset() zwróci false – można zakończyć pętlę.

Pełny przykład: monitorujemy katalog "data/uploads"

Dodajmy do naszej aplikacji edukacyjnej prosty „alarm” na katalog pobierania:

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("Monitorowanie katalogu " + dir.toAbsolutePath());

        while (true) {
            WatchKey key = watchService.take(); // czekamy na zdarzenia

            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("Katalog niedostępny, monitorowanie zakończone.");
                break;
            }
        }
    }
}

Wypróbuj: Uruchom ten kod i spróbuj utworzyć, usunąć lub zmodyfikować plik w katalogu "data/uploads". Program zareaguje natychmiast!

3. Ograniczenia i cechy WatchService

Tylko jeden katalog, bez podkatalogów

WatchService monitoruje tylko ten katalog, który zarejestrowałeś. Jeśli ma on podkatalogi, zmiany w nich nie zostaną zauważone – trzeba rejestrować każdy podkatalog osobno.

Co z tym zrobić?
Jeśli chcesz monitorować całą hierarchię, musisz zaimplementować przejście po wszystkich podkatalogach i rejestrować je pojedynczo. Na przykład, po utworzeniu nowego podkatalogu – od razu go zarejestrować.

Zachowanie na różnych systemach operacyjnych

Windows: WatchService działa dość stabilnie, ale czasem może „połączyć” kilka zdarzeń w jedno (np. podczas kopiowania dużego pliku).

Linux/macOS: Implementacja opiera się na mechanizmach systemowych (inotify, kqueue). Czasami zdarzenia mogą przychodzić z opóźnieniem, albo wręcz w nadmiarze (np. ENTRY_MODIFY przy każdym zapisie).

Tylko zdarzenia po nazwie

WatchService podaje tylko nazwę zmienionego obiektu (względem monitorowanego katalogu), ale nie daje pełnej informacji o tym, co zmieniło się wewnątrz pliku. Jeśli trzeba wiedzieć dokładnie, co się zmieniło – należy odczytać plik samodzielnie.

Utrata zdarzeń przy przeciążeniu

Jeśli w katalogu zachodzi zbyt wiele zmian w krótkim czasie (np. masowe kopiowanie tysięcy plików), kolejka zdarzeń może się przepełnić i część zdarzeń zostanie utracona. Dla krytycznych zadań warto stosować dodatkowe sprawdzenia.

4. Praktyczne przykłady

Automatyczne przetwarzanie nowych plików

Załóżmy, że piszesz program, który powinien automatycznie przetwarzać nowe obrazy pojawiające się w katalogu "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("Nowe zdjęcie: " + filename);
                // Tutaj można dodać przetwarzanie: kopiowanie, kompresję, analizę itp.
            }
        }
    }
    key.reset();
}

Implementacja prostego loggera zmian

Można zapisywać wszystkie zdarzenia do oddzielnego pliku logu:

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("Monitorowanie " + 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();
            }
        }
    }
}

Monitorowanie tworzenia nowych podkatalogów (i ich rejestracja)

Jeśli w monitorowanym katalogu tworzy się nowy podkatalog, można od razu zarejestrować go do dalszego monitorowania:

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("Rozpoczęto monitorowanie nowego podkatalogu: " + createdPath);
    }
}

5. Ważne szczegóły i typowe błędy

Błąd nr 1: zapomniano wywołać key.reset(). Jeśli nie resetujesz klucza po przetworzeniu zdarzeń, monitorowanie katalogu zakończy się i nie otrzymasz już żadnego zdarzenia. To klasyczna pułapka dla początkujących: wydaje się, że wszystko działa, a potem – bach! – program milczy.

Błąd nr 2: ignorowanie wyjątków. Praca z systemem plików zawsze bywa nieprzewidywalna: katalog może zostać usunięty, dysk – odłączony, uprawnienia – zmienione. Jeśli nie obsłużysz wyjątków (IOException, ClosedWatchServiceException), program może zakończyć się awaryjnie.

Błąd nr 3: monitorowanie tylko jednego katalogu. Wielu oczekuje, że po zarejestrowaniu katalogu wszystkie zagnieżdżone katalogi też będą monitorowane. To nie tak! Jeśli potrzebujesz monitorowania całej struktury – zaimplementuj rekurencyjną rejestrację.

Błąd nr 4: blokowanie głównego wątku. WatchService.take() blokuje wątek do pojawienia się zdarzenia. Jeśli główny wątek programu ma robić coś jeszcze, uruchom monitorowanie w osobnym wątku.

Błąd nr 5: utrata zdarzeń przy wysokim obciążeniu. Jeśli w katalogu zachodzi zbyt wiele zmian, kolejka zdarzeń może się przepełnić. Dla krytycznych aplikacji warto okresowo weryfikować stan katalogu (np. co minutę porównywać listę plików).

Błąd nr 6: nieprawidłowa obsługa ścieżek względnych. event.context() zwraca nazwę pliku względną do monitorowanego katalogu. Jeśli potrzebujesz ścieżki bezwzględnej – użyj dir.resolve((Path) event.context()).

1
Ankieta/quiz
Operacje z katalogami, poziom 40, lekcja 4
Niedostępny
Operacje z katalogami
Operacje z plikami i katalogami
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION