CodeGym /Corsi /JAVA 25 SELF /Monitoraggio delle modifiche nel file system: WatchServic...

Monitoraggio delle modifiche nel file system: WatchService

JAVA 25 SELF
Livello 40 , Lezione 4
Disponibile

1. Introduzione

WatchService è una parte di Java NIO (New IO) che consente di monitorare le modifiche nel file system in tempo reale. Lo si può immaginare come un sistema di allarme per le cartelle: appena qualcuno aggiunge, elimina o modifica un file — riceverete immediatamente una notifica. Questa funzionalità è apparsa in Java 7 insieme a NIO.2; prima gli sviluppatori dovevano interrogare manualmente la cartella (polling) oppure usare librerie di terze parti.

Gli usi pratici di WatchService sono molteplici: aiuta a elaborare automaticamente nuovi file, tenere log e fare backup, sincronizzare cartelle con un server o con il cloud, nonché monitorare le modifiche ai file di configurazione.

Registrazione delle directory da monitorare

Per iniziare a monitorare le modifiche, occorre:

  1. Ottenere un'istanza di WatchService.
  2. Registrare la cartella desiderata e indicare quali eventi ci interessano.

Otteniamo WatchService

import java.nio.file.*;

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

Registriamo la cartella

Per la registrazione usiamo il metodo register dell'oggetto Path:

Path dir = Paths.get("data/uploads");
dir.register(
    watchService,
    StandardWatchEventKinds.ENTRY_CREATE,   // creazione di file/cartelle
    StandardWatchEventKinds.ENTRY_DELETE,   // eliminazione di file/cartelle
    StandardWatchEventKinds.ENTRY_MODIFY    // modifica di file/cartelle
);

Spiegazione:

  • ENTRY_CREATE — qualcuno ha aggiunto qualcosa.
  • ENTRY_DELETE — qualcuno ha eliminato qualcosa.
  • ENTRY_MODIFY — qualcuno ha modificato un file (ad esempio, ha aggiunto del testo).

Importante! WatchService monitora una sola cartella alla volta (senza le sottocartelle). Se volete monitorare l’intera gerarchia — dovete registrare ogni sottocartella separatamente.

2. Gestione degli eventi: ciclo di attesa

Ora che abbiamo configurato il monitoraggio (più da «vicino curioso» che in stile «Grande Fratello»), possiamo attendere gli eventi. WatchService implementa il pattern della «coda di eventi»: non appena accade qualcosa — l’evento viene inserito in coda.

Ciclo principale

while (true) {
    // In attesa di eventi (chiamata bloccante)
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        // Tipo di evento: creazione, eliminazione, modifica
        WatchEvent.Kind<?> kind = event.kind();

        // Nome del file/cartella (Path, relativo alla cartella monitorata)
        Path filename = (Path) event.context();

        if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
            System.out.println("Creato file/cartella: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
            System.out.println("Eliminato file/cartella: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
            System.out.println("Modificato file/cartella: " + filename);
        }
    }

    // È obbligatorio reimpostare la chiave, altrimenti il monitoraggio si interromperà!
    boolean valid = key.reset();
    if (!valid) {
        break; // Cartella non disponibile, uscita
    }
}

Come funziona?

  • WatchService.take() — blocca il thread fino alla comparsa di un evento (si può usare poll() per la modalità non bloccante).
  • key.pollEvents() — l’elenco di tutti gli eventi accumulati.
  • event.context() — il nome del file o della cartella modificati (relativo alla directory monitorata).
  • Dopo aver elaborato gli eventi, è indispensabile chiamare key.reset(). Se la cartella è stata eliminata o è diventata non disponibile, reset() restituisce false — si può terminare il ciclo.

Esempio completo: monitoriamo la cartella "data/uploads"

Aggiungiamo alla nostra applicazione didattica un semplice «allarme» sulla cartella dei caricamenti:

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("Monitoraggio della cartella " + dir.toAbsolutePath());

        while (true) {
            WatchKey key = watchService.take(); // attendiamo gli eventi

            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("Cartella non disponibile, monitoraggio terminato.");
                break;
            }
        }
    }
}

Provate: Eseguite questo codice e provate a creare, eliminare o modificare un file nella cartella "data/uploads". Il programma reagirà immediatamente!

3. Limitazioni e particolarità di WatchService

Una sola cartella, senza sottocartelle

WatchService monitora solo la cartella che avete registrato. Se contiene sottocartelle, le modifiche al loro interno non verranno rilevate — bisogna registrare ogni sottocartella separatamente.

Come fare?
Se volete monitorare l’intera gerarchia, dovrete implementare la scansione di tutte le sottocartelle e registrarle una per una. Ad esempio, alla creazione di una nuova sottocartella — registrarla subito.

Specificità sui diversi sistemi operativi

Windows: WatchService funziona in modo abbastanza stabile, ma talvolta può «fondere» più eventi in uno solo (ad esempio durante la copia di un file di grandi dimensioni).

Linux/macOS: L’implementazione si basa su meccanismi di sistema (inotify, kqueue). A volte gli eventi possono arrivare con ritardo, oppure al contrario in numero eccessivo (ad esempio, ENTRY_MODIFY a ogni salvataggio).

Solo eventi per nome

WatchService riporta solo il nome dell’oggetto modificato (relativamente alla cartella monitorata), ma non fornisce informazioni complete su ciò che è cambiato dentro il file. Se occorre sapere cosa è cambiato esattamente — leggete il file manualmente.

Perdita di eventi in caso di sovraccarico

Se nella cartella avvengono troppe modifiche in poco tempo (ad esempio la copia massiva di migliaia di file), la coda degli eventi può riempirsi e parte degli eventi andare persa. Per i casi critici conviene usare controlli aggiuntivi.

4. Esempi pratici

Elaborazione automatica dei nuovi file

Supponiamo che stiate scrivendo un programma che deve elaborare automaticamente le nuove immagini che compaiono nella cartella "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("Nuova foto: " + filename);
                // Qui si può aggiungere l'elaborazione: copia, compressione, analisi, ecc.
            }
        }
    }
    key.reset();
}

Implementazione di un semplice logger delle modifiche

Si possono salvare tutti gli eventi in un file di log separato:

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

Monitoraggio della creazione di nuove sottocartelle (e loro registrazione)

Se nella cartella monitorata viene creata una nuova sottocartella, è possibile registrarla subito per il monitoraggio successivo:

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("Avviato il monitoraggio della nuova sottocartella: " + createdPath);
    }
}

5. Aspetti importanti ed errori comuni

Errore n. 1: dimenticare di chiamare key.reset(). Se non si reimposta la chiave dopo l’elaborazione degli eventi, il monitoraggio della cartella si interromperà e non riceverete più alcun evento. È un classico «tranello» per i principianti: sembra che tutto funzioni, e poi — boom! — il programma tace.

Errore n. 2: ignorare le eccezioni. Il lavoro con il file system è sempre pieno di imprevisti: la cartella può essere eliminata, il disco scollegato, i permessi modificati. Se non si gestiscono le eccezioni (IOException, ClosedWatchServiceException), il programma può terminare in modo anomalo.

Errore n. 3: monitorare solo una cartella. Molti si aspettano che registrando una cartella vengano monitorate anche tutte le cartelle annidate. Non è così! Se occorre monitorare l’intera struttura — implementate una registrazione ricorsiva.

Errore n. 4: bloccare il thread principale. WatchService.take() blocca il thread fino alla comparsa di un evento. Se il thread principale del programma deve fare anche altro, eseguite il monitoraggio in un thread separato.

Errore n. 5: perdita di eventi ad alto carico. Se nella cartella avvengono troppe modifiche, la coda degli eventi può saturarsi. Per le applicazioni critiche conviene implementare un controllo periodico dello stato della cartella (ad esempio, confrontare l’elenco dei file ogni minuto).

Errore n. 6: gestione errata dei percorsi relativi. event.context() restituisce il nome del file relativo alla cartella monitorata. Se serve un percorso assoluto — usate dir.resolve((Path) event.context()).

1
Sondaggio/quiz
Operazioni con le directory, livello 40, lezione 4
Non disponibile
Operazioni con le directory
Operazioni con i file e le directory
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION