1. Вступ
WatchService — це частина Java NIO (New I/O), яка дозволяє відстежувати зміни у файловій системі в реальному часі. Його можна уявити як сигналізацію для тек: щойно хтось додасть, видалить чи змінить файл — ви одразу отримаєте сповіщення. Ця можливість з’явилася в 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()).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ