1. Introducción
WatchService es parte de Java NIO (New IO), que permite vigilar cambios en el sistema de archivos en tiempo real. Puede imaginarse como una alarma para carpetas: en cuanto alguien añade, elimina o modifica un archivo — recibirá una notificación al instante. Esta capacidad apareció en Java 7 junto con NIO.2; antes, los desarrolladores tenían que sondear la carpeta manualmente (polling) o usar bibliotecas de terceros.
Los usos prácticos de WatchService son variados: ayuda a procesar automáticamente archivos nuevos, mantener logs y hacer copias de seguridad, sincronizar carpetas con un servidor o la nube, así como vigilar cambios en archivos de configuración.
Registro de directorios para la supervisión
Para empezar a vigilar los cambios, hay que:
- Obtener una instancia de WatchService.
- Registrar la carpeta deseada e indicar qué eventos nos interesan.
Obtenemos WatchService
import java.nio.file.*;
WatchService watchService = FileSystems.getDefault().newWatchService();
Registramos la carpeta
Para el registro usamos el método register del objeto Path:
Path dir = Paths.get("data/uploads");
dir.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE, // creación de archivos/carpetas
StandardWatchEventKinds.ENTRY_DELETE, // eliminación de archivos/carpetas
StandardWatchEventKinds.ENTRY_MODIFY // modificación de archivos/carpetas
);
Explicación:
- ENTRY_CREATE — alguien añadió algo.
- ENTRY_DELETE — alguien eliminó algo.
- ENTRY_MODIFY — alguien modificó un archivo (por ejemplo, añadió texto).
¡Importante! WatchService supervisa solo una carpeta a la vez (sin subcarpetas). Si desea vigilar toda la jerarquía — debe registrar cada subcarpeta por separado.
2. Procesamiento de eventos: bucle de espera
Ahora que hemos configurado la supervisión (más bien como «un vecino observador» y no al estilo de «Gran Hermano»), podemos esperar eventos. WatchService implementa el patrón de «cola de eventos»: en cuanto sucede algo — el evento se coloca en la cola.
Bucle principal
while (true) {
// Esperamos a que aparezcan eventos (llamada bloqueante)
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
// Tipo de evento: creación, eliminación, modificación
WatchEvent.Kind<?> kind = event.kind();
// Nombre de archivo/carpeta (Path relativo a la carpeta monitorizada)
Path filename = (Path) event.context();
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("Se ha creado archivo/carpeta: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("Se ha eliminado archivo/carpeta: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("Se ha modificado archivo/carpeta: " + filename);
}
}
// Es obligatorio restablecer la clave, ¡de lo contrario el monitoreo se detendrá!
boolean valid = key.reset();
if (!valid) {
break; // La carpeta no está disponible, salimos
}
}
¿Cómo funciona?
- WatchService.take() — bloquea el hilo hasta que aparezca un evento (se puede usar poll() para un modo no bloqueante).
- key.pollEvents() — la lista de todos los eventos acumulados.
- event.context() — el nombre del archivo o carpeta modificado (relativo al directorio monitorizado).
- Después de procesar los eventos llamamos obligatoriamente a key.reset(). Si la carpeta se ha eliminado o ha dejado de estar disponible, reset() devolverá false — se puede terminar el bucle.
Ejemplo completo: vigilamos la carpeta "data/uploads"
Añadamos a nuestra aplicación de práctica una sencilla «alarma» para la carpeta de descargas:
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("Monitorización de la carpeta " + dir.toAbsolutePath());
while (true) {
WatchKey key = watchService.take(); // esperamos eventos
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("La carpeta no está disponible, la monitorización ha finalizado.");
break;
}
}
}
}
Pruebe: Ejecute este código e intente crear, eliminar o modificar un archivo en la carpeta "data/uploads". ¡El programa reaccionará al instante!
3. Limitaciones y particularidades de WatchService
Solo una carpeta, sin subcarpetas
WatchService solo supervisa la carpeta que ha registrado. Si contiene subcarpetas, los cambios dentro de ellas no se detectarán — hay que registrar cada subcarpeta por separado.
¿Qué hacer?
Si desea vigilar toda la jerarquía, tendrá que recorrer todas las subcarpetas y registrarlas una a una. Por ejemplo, al crear una subcarpeta nueva — regístrela inmediatamente también.
Particularidades según el sistema operativo
Windows: WatchService funciona bastante estable, pero a veces puede «fusionar» varios eventos en uno (por ejemplo, al copiar un archivo grande).
Linux/macOS: La implementación se basa en mecanismos del sistema (inotify, kqueue). A veces los eventos pueden llegar con retraso, o por el contrario, en exceso (por ejemplo, ENTRY_MODIFY en cada guardado).
Solo eventos por nombre
WatchService solo informa del nombre del objeto modificado (relativo a la carpeta monitorizada), pero no ofrece información completa sobre lo que cambió dentro del archivo. Si necesita saber exactamente qué cambió — lea el archivo manualmente.
Pérdida de eventos bajo alta carga
Si en la carpeta ocurren demasiados cambios en poco tiempo (por ejemplo, copia masiva de miles de archivos), la cola de eventos puede desbordarse y se perderán algunos eventos. Para tareas críticas conviene implementar comprobaciones adicionales.
4. Ejemplos prácticos
Procesamiento automático de archivos nuevos
Supongamos que está escribiendo un programa que debe procesar automáticamente las nuevas imágenes que aparecen en la carpeta "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("Nueva foto: " + filename);
// Aquí se puede añadir procesamiento: copia, compresión, análisis, etc.
}
}
}
key.reset();
}
Implementación de un registrador sencillo de cambios
Se pueden guardar todos los eventos en un archivo de log aparte:
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("Monitorización de " + 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();
}
}
}
}
Detección de creación de nuevas subcarpetas (y su registro)
Si en la carpeta monitorizada se crea una subcarpeta nueva, puede registrarse de inmediato para seguir monitorizándola:
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("Se ha iniciado la monitorización de la nueva subcarpeta: " + createdPath);
}
}
5. Matices importantes y errores típicos
Error n.º 1: se olvidó llamar a key.reset(). Si no se restablece la clave después de procesar los eventos, la monitorización de la carpeta se detendrá y no recibirá ningún evento más. Es la «trampa» clásica para principiantes: parece que todo funciona y luego — ¡zas! — el programa se queda en silencio.
Error n.º 2: ignorar excepciones. Trabajar con el sistema de archivos siempre está lleno de imprevistos: la carpeta puede eliminarse, el disco — desconectarse, los permisos — cambiar. Si no se manejan las excepciones (IOException, ClosedWatchServiceException), el programa puede finalizar con error.
Error n.º 3: seguimiento solo de una carpeta. Muchos esperan que al registrar una carpeta, todas las carpetas anidadas también se supervisen. ¡No es así! Si se requiere supervisar toda la estructura — implemente el registro recursivo.
Error n.º 4: bloqueo del hilo principal. WatchService.take() bloquea el hilo hasta que aparezca un evento. Si el hilo principal del programa debe hacer algo más, ejecute la monitorización en un hilo separado.
Error n.º 5: pérdida de eventos bajo alta carga. Si en la carpeta ocurren demasiados cambios, la cola de eventos puede desbordarse. Para aplicaciones críticas conviene implementar una comprobación periódica del estado de la carpeta (por ejemplo, comparar la lista de archivos cada minuto).
Error n.º 6: manejo incorrecto de rutas relativas. event.context() devuelve el nombre del archivo relativo a la carpeta monitorizada. Si necesita la ruta absoluta — use dir.resolve((Path) event.context()).
GO TO FULL VERSION