Подтвердить что ты не робот

WatchService и SwingWorker: как это сделать правильно?

WatchService звучит как захватывающая идея... к сожалению, это похоже на низкий уровень, как указано в уроке /api plus, не очень вписывается в модель событий Swing (или мне не хватает чего-то очевидного, а не -размерная вероятность

Принимая код из примера WatchDir в учебнике (просто для обработки только одного каталога), я в основном закончил

  • расширить SwingWorker
  • выполните регистрацию в конструкторе
  • положить бесконечный цикл, ожидающий нажатия клавиши doInBackground
  • публикуйте каждый WatchEvent при извлечении через key.pollEvents()
  • обрабатывать куски, запустив свойствоChangeEvents с удаленными/созданными файлами как newValue

    @SuppressWarnings("unchecked")
    public class FileWorker extends SwingWorker<Void, WatchEvent<Path>> {
    
        public static final String DELETED = "deletedFile";
        public static final String CREATED = "createdFile";
    
        private Path directory;
        private WatchService watcher;
    
        public FileWorker(File file) throws IOException {
            directory = file.toPath();
            watcher = FileSystems.getDefault().newWatchService();
            directory.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        }
    
        @Override
        protected Void doInBackground() throws Exception {
            for (;;) {
                // wait for key to be signalled
                WatchKey key;
                try {
                    key = watcher.take();
                } catch (InterruptedException x) {
                    return null;
                }
    
                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();
                    // TBD - provide example of how OVERFLOW event is handled
                    if (kind == OVERFLOW) {
                        continue;
                    }
                    publish((WatchEvent<Path>) event);
                }
    
                // reset key return if directory no longer accessible
                boolean valid = key.reset();
                if (!valid) {
                    break;
                }
            }
            return null;
        }
    
        @Override
        protected void process(List<WatchEvent<Path>> chunks) {
            super.process(chunks);
            for (WatchEvent<Path> event : chunks) {
                WatchEvent.Kind<?> kind = event.kind();
                Path name = event.context();
                Path child = directory.resolve(name);
                File file = child.toFile();
                if (StandardWatchEventKinds.ENTRY_DELETE == kind) {
                    firePropertyChange(DELETED, null, file);
                } else if (StandardWatchEventKinds.ENTRY_CREATE == kind) {
                    firePropertyChange(CREATED, null, file);
                }
            }
        }
    
    }
    

Основная идея состоит в том, чтобы сделать код, блаженно не осведомленным о скользких деталях: он прослушивает изменения свойств и f.i. обновляет произвольные модели по мере необходимости:

    String testDir = "D:\\scans\\library";
    File directory = new File(testDir);
    final DefaultListModel<File> model = new DefaultListModel<File>();
    for (File file : directory.listFiles()) {
        model.addElement(file);
    }
    final FileWorker worker = new FileWorker(directory);
    PropertyChangeListener l = new PropertyChangeListener() {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (FileWorker.DELETED == evt.getPropertyName()) {
                model.removeElement(evt.getNewValue());
            } else if (FileWorker.CREATED == evt.getPropertyName()) {
                model.addElement((File) evt.getNewValue());
            }
        }
    };
    worker.addPropertyChangeListener(l);
    JXList list = new JXList(model);

Кажется, работает, но я чувствую себя некомфортно

  • Выйдя из себя как агностик потока, я: все примеры, которые я видел до сих пор, блокируют ожидающий поток, используя watcher.take(). Почему они это делают? Ожидалось бы, по крайней мере, некоторое использование watcher.poll() и немного спящего.
  • Метод публикации SwingWorker не совсем подходит: на данный момент это нормально, поскольку я смотрю только один каталог (не хотел галопп слишком далеко в неправильном направлении:) При попытке просмотра нескольких каталогов ( как в оригинальном примере WatchDir) есть несколько ключей и WatchEvent относительно одного из них. Чтобы разрешить путь, мне понадобится как событие, так и каталог [A], который просматривает ключ, но могут передавать только один. Скорее всего, неправильное распределение логики, хотя

[A] Отредактировано (вызвано комментарием @trashgods) - это фактически не тот ключ, который мне нужно передать вместе с событием, это каталог, в котором он сообщает об изменениях. Изменен вопрос соответственно

FYI, этот вопрос перекрестно помещен в OTN swing forum

Добавление

Чтение api doc WatchKey:

Если есть несколько потоков, получающих сигнальные ключи от часов следует соблюдать осторожность, чтобы гарантировать, что метод resetвызывается только после обработки событий для объекта.

означает, что события должны

  • обрабатывается в том же потоке, который извлекал WatchKey
  • не следует трогать после нажатия клавиши reset

Не совсем уверен, но в сочетании с (будущим) требованием рекурсивного просмотра каталогов (более одного) решил следовать совету @Eels, вроде - скоро опубликует код, который я установил на

ИЗМЕНИТЬ просто принял мой собственный ответ - смиренно вернется, что если у кого есть разумные возражения

4b9b3361

Ответ 1

Собственно, комментарий @Eels не прекратил стучать в затылок и, наконец, зарегистрировался: это путь, но нет никакой необходимости в какой-либо "искусственной" структуре, потому что у нас уже есть идеальный кандидат - это сам PropertyChangeEvent: -)

Взяв общее описание процесса из моего вопроса, первые три пули остаются теми же

  • : продлить SwingWorker
  • : сделать запись в конструкторе
  • : положить бесконечный цикл, ожидающий нажатия клавиши doInBackground
  • изменено: создайте соответствующий PropertyChangeEvent из каждого WatchEvent при извлечении через key.pollEvents и опубликуйте PropertyChangeEvent
  • изменено: запустите ранее созданное событие в процессе (куски)

Пересмотренный FileWorker:

@SuppressWarnings("unchecked")
public class FileWorker extends SwingWorker<Void, PropertyChangeEvent> {

    public static final String FILE_DELETED = StandardWatchEventKinds.ENTRY_DELETE.name();
    public static final String FILE_CREATED = StandardWatchEventKinds.ENTRY_CREATE.name();
    public static final String FILE_MODIFIED = StandardWatchEventKinds.ENTRY_MODIFY.name();

    // final version will keep a map of keys/directories (just as in the tutorial example) 
    private Path directory;
    private WatchService watcher;

    public FileWorker(File file) throws IOException {
        directory = file.toPath();
        watcher = FileSystems.getDefault().newWatchService();
        directory.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
    }

    @Override
    protected Void doInBackground() throws Exception {
        for (;;) {
            // wait for key to be signalled
            WatchKey key;
            try {
                key = watcher.take();
            } catch (InterruptedException x) {
                return null;
            }

            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();
                // TBD - provide example of how OVERFLOW event is handled
                if (kind == OVERFLOW) {
                    continue;
                }
                publish(createChangeEvent((WatchEvent<Path>) event, key));
            }

            // reset key return if directory no longer accessible
            boolean valid = key.reset();
            if (!valid) {
                break;
            }
        }
        return null;
    }

    /**
     * Creates and returns the change notification. This method is called from the 
     * worker thread while looping through the events as received from the Watchkey.
     * 
     * @param event
     * @param key
     */
    protected PropertyChangeEvent createChangeEvent(WatchEvent<Path> event, WatchKey key) {
        Path name = event.context();
        // real world will lookup the directory from the key/directory map
        Path child = directory.resolve(name);
        PropertyChangeEvent e = new PropertyChangeEvent(this, event.kind().name(), null, child.toFile());
        return e;
    }

    @Override
    protected void process(List<PropertyChangeEvent> chunks) {
        super.process(chunks);
        for (PropertyChangeEvent event : chunks) {
            getPropertyChangeSupport().firePropertyChange(event);
        }
    }
}

Ответ 2

Поскольку ваш фоновый поток полностью посвящен просмотру, take() является правильным выбором. Он эффективно скрывает зависимую от платформы реализацию, которая может либо перенаправляться, либо опросить. Один из методов poll() был бы уместным, если бы, например, ваш фоновый поток также потребовался для проверки очередей очередей последовательно с WatchService.

Добавление: поскольку состояние WatchKey имеет значение, его, вероятно, не следует пересылать в process(). context() WatchEvent является " относительный путь между каталогом, зарегистрированным в службе просмотра, и записью, которая была создана, удалена или изменена." Один из методов resolve() должен работать, если каталоги имеют общий корень.

Ответ 3

Что касается вашего второго момента, не мог бы вы создать класс, который содержит как WatchEvent, так и ключ, и иметь второй общий параметр SwingWorker в этом типе? Извините, я знаю, что вы уже подумали об этом, поэтому, я думаю, мой вопрос: кто-нибудь делает это?