proglib
Сообщение

Почему специалистом по кибербезопасности быть интереснее, чем разработчиком или сисадмином? Приглашаем на вебинар от HackerU

Почему специалистом по кибербезопасности быть интереснее, чем разработчиком или сисадмином? Приглашаем на вебинар от HackerU

Silver 25 января 2020

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

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

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);
    }
}
        

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

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

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

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

BUG