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:
- Uzyskać instancję WatchService.
- 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()).
GO TO FULL VERSION