Мониторинг файлов вместе с Java NIO
Небольшой урок о том, как с помощью пакета java.nio.file написать класс для наблюдения за изменениями состояния файлов в директории.
Java NIO или Java New I/O это крайне полезный пакет, позволяющий использовать асинхронный ввод/вывод. Сегодня с помощью java.nio.file
, следуя паттерну Observer
, мы реализуем свой класс для наблюдения за состоянием файлов в папке. Наш план:
- Первым делом создадим WatchService.
- Потом переменную
Path
, указывающую на папку, которую планируем мониторить. - Далее бесконечный цикл наблюдения. Когда происходит интересующее нас событие, класс
WathKey
помещает его в очередь наблюдателя. После обработки события мы должны вернуть ключ в состояние готовности, вызвав методreset()
. Если метод вернётfalse
, то ключ больше не действителен, цикл можно завершить.
WatchService watchService = FileSystems.getDefault().newWatchService(); Path path = Paths.get("c:\\directory"); //будем следить за созданием, изменение и удалением файлов. path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); boolean poll = true; while (poll) { WatchKey key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { System.out.println("Event kind : " + event.kind() + " - File : " + event.context()); } poll = key.reset(); }
Данный код должен вывести в консоль следующее:
Event kind : ENTRY_CREATE - File : file.txt Event kind : ENTRY_DELETE - File : file.txt Event kind : ENTRY_CREATE - File : test.txt Event kind : ENTRY_MODIFY - File : test.txt
Watch Service API довольно низкоуровневая штука, мы реализуем на его основе свой более высокоуровневый API. Начнём с написания класса FileEvent
. Так как это объект состояния события, его необходимо унаследовать от EventObject
и передать в конструктор ссылку на файл, за котором будем наблюдать.
import java.io.File; import java.util.EventObject; public class FileEvent extends EventObject { public FileEvent(File file) { super(file); } public File getFile() { return (File) getSource(); } }
Теперь создаём интерфейс слушателя FileListener
, наследуемый от java.util.EventListener
. Этот интерфейс должны реализовать все слушатели, которые будут подписываться на события нашей папки.
import java.util.EventListener; public interface FileListener extends EventListener { public void onCreated(FileEvent event); public void onModified(FileEvent event); public void onDeleted(FileEvent event); }
Наконец, создаём класс, который будет хранить в себе список слушателей, подписанных на папку. Назовём его FileWatcher
.
public class FileWatcher { protected List<FileListener> listeners = new ArrayList<>(); protected final File folder; public FileWatcher(File folder) { this.folder = folder; } public List<FileListener> getListeners() { return listeners; } public FileWatcher setListeners(List<FileListener> listeners) this.listeners = listeners; return this; } }
Хорошей идеей будет реализовать класс FileWatcher
как Runnable
, чтобы иметь возможность запускать наблюдение в виде демона, если указанная папка существует.
public class FileWatcher implements Runnable { public void watch() { if (folder.exists()) { Thread thread = new Thread(this); thread.setDaemon(true); thread.start(); } } @Override public void run() { // пока оставим без реализации } }
Так как в методе run()
будут создаваться объекты WatchService
, использующие внешние ресурсы (ссылки на файлы), все события будем хранить в статическом списке. Такая реализация позволит вызвать метод close()
из любого потока, ждущего ключ. А также вызвать исключение ClosedWatchServiceException
, чтобы отменить наблюдение без утечки памяти.
@Override public void contextDestroyed(ServletContextEvent event) { for (WatchService watchService : FileWatcher.getWatchServices()){ try { watchService.close(); } catch (IOException e) {} } }
public class FileWatcher implements Runnable { protected static final List<WatchService> watchServices = new ArrayList<>(); @Override public void run() { try (WatchService watchService = FileSystems.getDefault().newWatchService()) { Path path = Paths.get(folder.getAbsolutePath()); path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); watchServices.add(watchService); boolean poll = true; while (poll) { poll = pollEvents(watchService); } } catch (IOException | InterruptedException | ClosedWatchServiceException e) { Thread.currentThread().interrupt(); } } protected boolean pollEvents(WatchService watchService) throws InterruptedException { WatchKey key = watchService.take(); Path path = (Path) key.watchable(); for (WatchEvent<?> event : key.pollEvents()) { notifyListeners(event.kind(), path.resolve((Path) event.context()).toFile()); } return key.reset(); } public static List<WatchService> getWatchServices() { return Collections.unmodifiableList(watchServices); } }
Когда происходит интересующее нас событие и путь корректен, мы уведомляем слушателей о событии. Если была создана новая директория, то для неё будет инициализирован новый экземпляр FileWatcher
.
public class FileWatcher implements Runnable { protected void notifyListeners(WatchEvent.Kind<?> kind, File file) { FileEvent event = new FileEvent(file); if (kind == ENTRY_CREATE) { for (FileListener listener : listeners) { listener.onCreated(event); } if (file.isDirectory()) { // создаем новый FileWatcher для отслеживания новой директории new FileWatcher(file).setListeners(listeners).watch(); } } else if (kind == ENTRY_MODIFY) { for (FileListener listener : listeners) { listener.onModified(event); } } else if (kind == ENTRY_DELETE) { for (FileListener listener : listeners) { listener.onDeleted(event); } } } }
Полная реализация класса FileWatcher
будет выглядеть так:
import static java.nio.file.StandardWatchEventKinds.*; import java.io.File; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class FileWatcher implements Runnable { protected List<FileListener> listeners = new ArrayList<>(); protected final File folder; protected static final List<WatchService> watchServices = new ArrayList<>(); public FileWatcher(File folder) { this.folder = folder; } public void watch() { if (folder.exists()) { Thread thread = new Thread(this); thread.setDaemon(true); thread.start(); } } @Override public void run() { try (WatchService watchService = FileSystems.getDefault().newWatchService()) { Path path = Paths.get(folder.getAbsolutePath()); path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); watchServices.add(watchService); boolean poll = true; while (poll) { poll = pollEvents(watchService); } } catch (IOException | InterruptedException | ClosedWatchServiceException e) { Thread.currentThread().interrupt(); } } protected boolean pollEvents(WatchService watchService) throws InterruptedException { WatchKey key = watchService.take(); Path path = (Path) key.watchable(); for (WatchEvent<?> event : key.pollEvents()) { notifyListeners(event.kind(), path.resolve((Path) event.context()).toFile()); } return key.reset(); } protected void notifyListeners(WatchEvent.Kind<?> kind, File file) { FileEvent event = new FileEvent(file); if (kind == ENTRY_CREATE) { for (FileListener listener : listeners) { listener.onCreated(event); } if (file.isDirectory()) { new FileWatcher(file).setListeners(listeners).watch(); } } else if (kind == ENTRY_MODIFY) { for (FileListener listener : listeners) { listener.onModified(event); } } else if (kind == ENTRY_DELETE) { for (FileListener listener : listeners) { listener.onDeleted(event); } } } public FileWatcher addListener(FileListener listener) { listeners.add(listener); return this; } public FileWatcher removeListener(FileListener listener) { listeners.remove(listener); return this; } public List<FileListener> getListeners() { return listeners; } public FileWatcher setListeners(List<FileListener> listeners) { this.listeners = listeners; return this; } public static List<WatchService> getWatchServices() { return Collections.unmodifiableList(watchServices); } }
Последний штрих – создание класса FileAdapter
, простейшей реализации интерфейса FileListener
.
public abstract class FileAdapter implements FileListener { @Override public void onCreated(FileEvent event) { //реализация не предусмотрена } @Override public void onModified(FileEvent event) { //реализация не предусмотрена } @Override public void onDeleted(FileEvent event) { //реализация не предусмотрена } }
Чтобы убедиться в работоспособности написанного кода, можно использовать несложный класс FileWatcherTest
.
import static org.junit.Assert.*; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.junit.Test; public class FileWatcherTest { @Test public void test() throws IOException, InterruptedException { File folder = new File("src/test/resources"); final Map<String, String> map = new HashMap<>(); FileWatcher watcher = new FileWatcher(folder); watcher.addListener(new FileAdapter() { public void onCreated(FileEvent event) { map.put("file.created", event.getFile().getName()); } public void onModified(FileEvent event) { map.put("file.modified", event.getFile().getName()); } public void onDeleted(FileEvent event) { map.put("file.deleted", event.getFile().getName()); } }).watch(); assertEquals(1, watcher.getListeners().size()); wait(2000); File file = new File(folder + "/test.txt"); try(FileWriter writer = new FileWriter(file)) { writer.write("Some String"); } wait(2000); file.delete(); wait(2000); assertEquals(file.getName(), map.get("file.created")); assertEquals(file.getName(), map.get("file.modified")); assertEquals(file.getName(), map.get("file.deleted")); } public void wait(int time) throws InterruptedException { Thread.sleep(time); } }
Несмотря на кажущуюся простоту, наблюдатели – мощный инструмент автоматизации процессов. С помощью них, например, можно автоматически перезапускать скрипты на продакшене, если один из них был изменён. Уменьшение человеческого фактора – благо для программиста.
А используете ли вы наблюдателей в своей работе? Или, может быть, реактивное программирование?