1. Введение
WatchService — это часть Java NIO (New IO), которая позволяет отслеживать изменения в файловой системе в реальном времени. Его можно представить как сигнализацию для папок: стоит кому-то добавить, удалить или изменить файл — вы тут же получите уведомление. Эта возможность появилась в Java 7 вместе с NIO.2, и до того разработчикам приходилось либо вручную опрашивать папку (polling), либо использовать сторонние библиотеки.
Практическое применение у WatchService самое разное: он помогает автоматически обрабатывать новые файлы, вести логи и делать резервные копии, синхронизировать папки с сервером или облаком, а также следить за изменениями в конфигурационных файлах.
Регистрация директорий для наблюдения
Чтобы начать следить за изменениями, нужно:
- Получить экземпляр WatchService.
- Зарегистрировать нужную папку и указать, какие события нас интересуют.
Получаем WatchService
import java.nio.file.*;
WatchService watchService = FileSystems.getDefault().newWatchService();
Регистрируем папку
Для регистрации используем метод register у объекта Path:
Path dir = Paths.get("data/uploads");
dir.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE, // создание файлов/папок
StandardWatchEventKinds.ENTRY_DELETE, // удаление файлов/папок
StandardWatchEventKinds.ENTRY_MODIFY // изменение файлов/папок
);
Пояснение:
- ENTRY_CREATE — кто-то что-то добавил.
- ENTRY_DELETE — кто-то что-то удалил.
- ENTRY_MODIFY — кто-то изменил файл (например, дописал текст).
Важно! WatchService отслеживает только одну папку за раз (без подпапок). Если хотите следить за всей иерархией — нужно регистрировать каждую подпапку отдельно.
2. Обработка событий: цикл ожидания
Теперь, когда мы настроили слежку (скорее как у «соседа-наблюдателя», а не в духе «Большого брата»), можно ждать событий. WatchService реализует паттерн «очередь событий»: как только что-то произошло — событие помещается в очередь.
Основной цикл
while (true) {
// Ожидаем появления событий (блокирующий вызов)
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
// Тип события: создание, удаление, изменение
WatchEvent.Kind<?> kind = event.kind();
// Имя файла/папки (Path, относительный к отслеживаемой папке)
Path filename = (Path) event.context();
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("Создан файл/папка: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("Удалён файл/папка: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("Изменён файл/папка: " + filename);
}
}
// Обязательно сбрасываем ключ, иначе слежка прекратится!
boolean valid = key.reset();
if (!valid) {
break; // Папка недоступна, выходим
}
}
Как это работает?
- WatchService.take() — блокирует поток до появления события (можно использовать poll() для неблокирующего режима).
- key.pollEvents() — список всех событий, которые накопились.
- event.context() — имя изменённого файла или папки (относительно отслеживаемой директории).
- После обработки событий обязательно вызываем key.reset(). Если папка была удалена или стала недоступна, reset() вернёт false — можно завершать цикл.
Полный пример: отслеживаем папку "data/uploads"
Давайте добавим в наше учебное приложение простую «сигнализацию» на папку загрузок:
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("Слежение за папкой " + dir.toAbsolutePath());
while (true) {
WatchKey key = watchService.take(); // ждём события
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("Папка недоступна, слежение завершено.");
break;
}
}
}
}
Попробуйте: Запустите этот код и попробуйте создать, удалить или изменить файл в папке "data/uploads". Программа тут же отреагирует!
3. Ограничения и особенности WatchService
Только одна папка, без подпапок
WatchService отслеживает только ту папку, которую вы зарегистрировали. Если в ней есть подпапки, изменения внутри них не будут замечены — нужно регистрировать каждую подпапку отдельно.
Как быть?
Если вы хотите отслеживать всю иерархию, придётся реализовать обход всех подпапок и регистрировать их по одной. Например, при создании новой подпапки — сразу регистрировать и её.
Особенности на разных ОС
Windows: WatchService работает довольно стабильно, но иногда может «сливать» несколько событий в одно (например, при копировании большого файла).
Linux/macOS: Реализация основана на системных механизмах (inotify, kqueue). Иногда события могут приходить с задержкой, или наоборот, слишком много (например, ENTRY_MODIFY при каждом сохранении).
Только события по имени
WatchService сообщает только имя изменённого объекта (относительно отслеживаемой папки), но не даёт полной информации о том, что изменилось внутри файла. Если нужно узнать, что именно поменялось — читайте файл вручную.
Потеря событий при перегрузке
Если в папке происходит слишком много изменений за короткое время (например, массовое копирование тысяч файлов), очередь событий может переполниться, и часть событий будет потеряна. Для критичных задач стоит использовать дополнительные проверки.
4. Практические примеры
Автоматическая обработка новых файлов
Допустим, вы пишете программу, которая должна автоматически обрабатывать новые изображения, появляющиеся в папке "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("Новое фото: " + filename);
// Здесь можно добавить обработку: копирование, сжатие, анализ и т.д.
}
}
}
key.reset();
}
Реализация простого логгера изменений
Можно сохранять все события в отдельный лог-файл:
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("Слежение за " + 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();
}
}
}
}
Отслеживание создания новых подпапок (и их регистрация)
Если в отслеживаемой папке создаётся новая подпапка, можно тут же зарегистрировать её для дальнейшего мониторинга:
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("Начато слежение за новой подпапкой: " + createdPath);
}
}
5. Важные нюансы и типичные ошибки
Ошибка №1: забыли вызвать key.reset(). Если не сбрасывать ключ после обработки событий, слежка за папкой прекратится, и вы больше не получите ни одного события. Это классическая «подстава» для новичков: кажется, что всё работает, а потом — бац! — и программа молчит.
Ошибка №2: игнорирование исключений. Работа с файловой системой всегда чревата неожиданностями: папка может быть удалена, диск — отключён, права — изменены. Если не обрабатывать исключения (IOException, ClosedWatchServiceException), программа может аварийно завершиться.
Ошибка №3: слежка только за одной папкой. Многие ожидают, что если зарегистрировать папку, то все вложенные папки тоже будут отслеживаться. Это не так! Если требуется слежение за всей структурой — реализуйте рекурсивную регистрацию.
Ошибка №4: блокировка основного потока. WatchService.take() блокирует поток до появления события. Если основной поток программы должен делать что-то ещё, запускайте слежку в отдельном потоке.
Ошибка №5: потеря событий при высокой нагрузке. Если в папке происходит слишком много изменений, очередь событий может быть переполнена. Для критичных приложений стоит реализовать периодическую сверку состояния папки (например, раз в минуту сравнивать список файлов).
Ошибка №6: неправильная обработка относительных путей. event.context() возвращает имя файла относительно отслеживаемой папки. Если нужен абсолютный путь — используйте dir.resolve((Path) event.context()).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ