Silver 25 января 2020

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

Небольшой урок о том, как с помощью пакета java.nio.file написать класс для наблюдения за изменениями состояния файлов в директории.
Мониторинг файлов вместе с Java NIO
☕ Подтянуть свои знания по Java вы можете на нашем телеграм-канале «Библиотека Java для собеса»

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();
    }
}
    
🧩☕ Интересные задачи по Java для практики можно найти на нашем телеграм-канале «Библиотека задач по Java»

Теперь создаём интерфейс слушателя 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);
    }
}
    

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

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

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Go-разработчик
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ