Мониторинг файлов вместе с Java NIO

Мониторинг файлов вместе с Java NIO

Небольшой урок о том, как с помощью пакета java.nio.file написать класс для наблюдения за изменениями состояния файлов в директории.

Java NIO или Java New I/O это крайне полезный пакет, позволяющий использовать асинхронный ввод/вывод. Сегодня с помощью java.nio.file, следуя паттерну Observer, мы реализуем свой класс для наблюдения за состоянием файлов в папке. Наш план:

  1. Первым делом создадим WatchService.
  2. Потом переменную Path, указывающую на папку, которую планируем мониторить.
  3. Далее бесконечный цикл наблюдения. Когда происходит интересующее нас событие, класс 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 и передать в конструктор ссылку на файл, за котором будем наблюдать.

FileEvent.java
 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. Этот интерфейс должны реализовать все слушатели, которые будут подписываться на события нашей папки.

FileListener.java
 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, чтобы иметь возможность запускать наблюдение в виде демона, если указанная папка существует.

FileWatcher.java
 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.

FileWatcher.java
 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 будет выглядеть так:

FileWatcher.java
 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.

FileAdapter.java
 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.

FileWatcherTest.java
 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); }
} 

Несмотря на кажущуюся простоту, наблюдатели – мощный инструмент автоматизации процессов. С помощью них, например, можно автоматически перезапускать скрипты на продакшене, если один из них был изменён. Уменьшение человеческого фактора – благо для программиста.

А используете ли вы наблюдателей в своей работе? Или, может быть, реактивное программирование?

proglib.io

Добавить комментарий

Ваш e-mail не будет опубликован.

восемнадцать − 3 =