CodeGym /Kurse /JAVA 25 SELF /Überwachung von Dateisystemänderungen: WatchService

Überwachung von Dateisystemänderungen: WatchService

JAVA 25 SELF
Level 40 , Lektion 4
Verfügbar

1. Einleitung

WatchService ist Teil von Java NIO (New I/O) und ermöglicht es, Änderungen im Dateisystem in Echtzeit zu beobachten. Man kann ihn sich wie eine Alarmanlage für Ordner vorstellen: Sobald jemand eine Datei hinzufügt, löscht oder ändert – erhalten Sie sofort eine Benachrichtigung. Diese Möglichkeit kam mit Java 7 zusammen mit NIO.2; zuvor mussten Entwickler Ordner entweder manuell abfragen (polling) oder Fremdbibliotheken verwenden.

WatchService hat vielfältige praktische Einsatzzwecke: Er hilft, neue Dateien automatisch zu verarbeiten, Logs zu führen und Backups zu erstellen, Ordner mit Server oder Cloud zu synchronisieren sowie Konfigurationsdateien auf Änderungen zu überwachen.

Verzeichnisse zur Überwachung registrieren

Um mit der Überwachung zu beginnen, müssen Sie:

  1. Einen WatchService-Instanz erhalten.
  2. Den gewünschten Ordner registrieren und angeben, welche Ereignisse Sie interessieren.

WatchService abrufen

import java.nio.file.*;

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

Ordner registrieren

Zur Registrierung verwenden wir die Methode register des Objekts Path:

Path dir = Paths.get("data/uploads");
dir.register(
    watchService,
    StandardWatchEventKinds.ENTRY_CREATE,   // Erstellen von Dateien/Ordnern
    StandardWatchEventKinds.ENTRY_DELETE,   // Löschen von Dateien/Ordnern
    StandardWatchEventKinds.ENTRY_MODIFY    // Ändern von Dateien/Ordnern
);

Erläuterung:

  • ENTRY_CREATE – es wurde etwas hinzugefügt.
  • ENTRY_DELETE – etwas wurde gelöscht.
  • ENTRY_MODIFY – eine Datei wurde geändert (z. B. Text ergänzt).

Wichtig! WatchService überwacht jeweils nur einen Ordner (ohne Unterordner). Wenn Sie die ganze Hierarchie beobachten möchten – müssen Sie jeden Unterordner separat registrieren.

2. Ereignisverarbeitung: Warteschleife

Jetzt, da die Überwachung eingerichtet ist (eher wie ein „beobachtender Nachbar“ als im Sinne des „Großen Bruders“), können wir auf Ereignisse warten. WatchService implementiert das Muster einer „Ereigniswarteschlange“: Sobald etwas passiert – wird ein Ereignis in die Warteschlange gestellt.

Hauptschleife

while (true) {
    // Warten auf Ereignisse (blockierender Aufruf)
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        // Ereignistyp: Erstellung, Löschung, Änderung
        WatchEvent.Kind<?> kind = event.kind();

        // Datei-/Ordnername (Path, relativ zum überwachten Ordner)
        Path filename = (Path) event.context();

        if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
            System.out.println("Datei/Ordner erstellt: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
            System.out.println("Datei/Ordner gelöscht: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
            System.out.println("Datei/Ordner geändert: " + filename);
        }
    }

    // Unbedingt den Schlüssel zurücksetzen, sonst stoppt die Überwachung!
    boolean valid = key.reset();
    if (!valid) {
        break; // Ordner nicht verfügbar, beenden
    }
}

Wie funktioniert das?

  • WatchService.take() – blockiert den Thread, bis ein Ereignis eintrifft (für den nicht blockierenden Modus kann poll() verwendet werden).
  • key.pollEvents() – Liste aller aufgelaufenen Ereignisse.
  • event.context() – Name der geänderten Datei bzw. des Ordners (relativ zum überwachten Verzeichnis).
  • Nach der Verarbeitung der Ereignisse unbedingt key.reset() aufrufen. Wenn der Ordner gelöscht wurde oder nicht mehr verfügbar ist, liefert reset() false – die Schleife kann beendet werden.

Vollständiges Beispiel: wir überwachen den Ordner "data/uploads"

Fügen wir unserem Übungsprogramm eine einfache „Alarmanlage“ für den Upload-Ordner hinzu:

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("Überwachung für Ordner " + dir.toAbsolutePath());

        while (true) {
            WatchKey key = watchService.take(); // auf Ereignisse warten

            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("Ordner nicht verfügbar, Überwachung beendet.");
                break;
            }
        }
    }
}

Probieren Sie es aus: Starten Sie diesen Code und versuchen Sie, im Ordner "data/uploads" eine Datei zu erstellen, zu löschen oder zu ändern. Das Programm reagiert sofort!

3. Einschränkungen und Besonderheiten von WatchService

Nur ein Ordner, keine Unterordner

WatchService überwacht nur den Ordner, den Sie registriert haben. Wenn er Unterordner enthält, werden Änderungen darin nicht bemerkt – jeder Unterordner muss separat registriert werden.

Wie geht man vor?
Wenn Sie die gesamte Hierarchie überwachen möchten, müssen Sie alle Unterordner durchlaufen und nacheinander registrieren. Beispielsweise können neu erstellte Unterordner sofort registriert werden.

Besonderheiten auf verschiedenen Betriebssystemen

Windows: WatchService arbeitet recht stabil, kann aber manchmal mehrere Ereignisse zu einem „zusammenfassen“ (z. B. beim Kopieren einer großen Datei).

Linux/macOS: Die Implementierung basiert auf Systemmechanismen (inotify, kqueue). Manchmal können Ereignisse verzögert eintreffen oder – im Gegenteil – zu viele (z. B. ENTRY_MODIFY bei jedem Speichern).

Nur Ereignisse zum Namen

WatchService liefert nur den Namen des geänderten Objekts (relativ zum überwachten Ordner), aber keine vollständigen Informationen darüber, was sich innerhalb der Datei geändert hat. Wenn Sie wissen müssen, was genau sich geändert hat – lesen Sie die Datei selbst ein.

Ereignisverlust bei Überlastung

Wenn in einem Ordner in kurzer Zeit zu viele Änderungen stattfinden (z. B. massenhaftes Kopieren von Tausenden Dateien), kann die Ereigniswarteschlange überlaufen und einige Ereignisse gehen verloren. Für kritische Aufgaben sollten zusätzliche Prüfungen implementiert werden.

4. Praxisbeispiele

Automatische Verarbeitung neuer Dateien

Angenommen, Sie schreiben ein Programm, das neu auftauchende Bilder im Ordner "photos/incoming" automatisch verarbeiten soll.

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("Neues Foto: " + filename);
                // Hier kann Verarbeitung hinzugefügt werden: Kopieren, Komprimieren, Analyse usw.
            }
        }
    }
    key.reset();
}

Einen einfachen Änderungs-Logger implementieren

Alle Ereignisse können in eine separate Logdatei geschrieben werden:

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("Überwachung von " + 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();
            }
        }
    }
}

Erkennung neu erstellter Unterordner (und deren Registrierung)

Wenn im überwachten Ordner ein neuer Unterordner entsteht, können Sie ihn sofort für die weitere Überwachung registrieren:

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("Überwachung für neuen Unterordner gestartet: " + createdPath);
    }
}

5. Wichtige Feinheiten und typische Fehler

Fehler Nr. 1: key.reset() nicht aufgerufen. Wenn der Schlüssel nach der Ereignisverarbeitung nicht zurückgesetzt wird, endet die Überwachung des Ordners und Sie erhalten keine weiteren Ereignisse. Das ist eine klassische Falle für Einsteiger: Es wirkt, als ob alles funktioniert, und dann – zack! – schweigt das Programm.

Fehler Nr. 2: Ausnahmen ignorieren. Die Arbeit mit dem Dateisystem ist immer für Überraschungen gut: Der Ordner kann gelöscht werden, das Laufwerk – entfernt, Rechte – geändert. Werden Ausnahmen nicht behandelt (IOException, ClosedWatchServiceException), kann das Programm abstürzen.

Fehler Nr. 3: Überwachung nur eines Ordners. Viele erwarten, dass beim Registrieren eines Ordners alle enthaltenen Unterordner automatisch mitüberwacht werden. Das ist nicht so! Wenn die gesamte Struktur überwacht werden soll – implementieren Sie eine rekursive Registrierung.

Fehler Nr. 4: Blockierung des Haupt-Threads. WatchService.take() blockiert den Thread, bis ein Ereignis erscheint. Wenn der Haupt-Thread des Programms noch etwas anderes tun soll, starten Sie die Überwachung in einem separaten Thread.

Fehler Nr. 5: Ereignisverlust bei hoher Last. Wenn in einem Ordner zu viele Änderungen passieren, kann die Ereigniswarteschlange überlaufen. Für kritische Anwendungen sollte ein periodischer Abgleich des Ordnerzustands implementiert werden (z. B. einmal pro Minute die Dateiliste vergleichen).

Fehler Nr. 6: Falscher Umgang mit relativen Pfaden. event.context() liefert den Dateinamen relativ zum überwachten Ordner. Wenn ein absoluter Pfad benötigt wird – verwenden Sie dir.resolve((Path) event.context()).

1
Umfrage/Quiz
Operationen mit Verzeichnissen, Level 40, Lektion 4
Nicht verfügbar
Operationen mit Verzeichnissen
Operationen mit Dateien und Verzeichnissen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION