WatchService и SwingWorker: как это правильно сделать?
WatchService звучит как захватывающая идея... к сожалению, он кажется настолько низким, как было предупреждено в руководстве / api plus, он не совсем подходит для модели событий Swing (или я упускаю что-то очевидное, ненулевую вероятность)
Взяв код из примера WatchDir из учебника (просто для обработки только одного каталога), я в итоге оказался
- расширить SwingWorker
- сделать регистрацию вещи в конструкторе
- положить бесконечный цикл в ожидании ключа в doInBackground
- публиковать каждый WatchEvent при получении через key.pollEvents()
обработать фрагменты, запустив propertyChangeEvents с удаленными / созданными файлами как 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); } } } }
Основная идея заключается в том, чтобы сделать использование кода блаженно не осведомленным о слизистых деталях: он слушает изменения свойств и обновляет произвольные модели по мере необходимости:
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) - это на самом деле не ключ, который я должен передавать вместе с событием, это каталог, в котором сообщается об изменениях. Изменил вопрос соответственно
К вашему сведению, этот вопрос находится в перекрестном сообщении на свинг-форуме OTN
добавление
Чтение API-интерфейса WatchKey:
Если есть несколько потоков, извлекающих сигнальные ключи из службы наблюдения, следует позаботиться о том, чтобы метод сброса вызывался только после обработки событий для объекта.
кажется, подразумевает, что события должны
- обрабатываться в том же потоке, который получил WatchKey
- не следует трогать после сброса ключа
Не совсем уверен, но в сочетании с (будущим) требованием рекурсивного просмотра каталогов (более одного) решил последовать совету @Eels, вроде - скоро опубликую код, на котором я остановился
РЕДАКТИРОВАТЬ только что принял мой собственный ответ - смиренно вернет, что если у кого-то есть разумные возражения
3 ответа
На самом деле, комментарий @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);
}
}
}
Потому что ваш фоновый поток полностью посвящен просмотру, take()
это правильный выбор. Он эффективно скрывает зависящую от платформы реализацию, которая может либо пересылать, либо опрашивать. Один из poll()
методы были бы уместны, если, например, ваш фоновый поток также должен был исследовать другие очереди последовательно с WatchService
,
Приложение: потому что WatchKey
имеет состояние, вероятно, его не следует пересылать process()
, context()
из WatchEvent
" относительный путь между каталогом, зарегистрированным в службе наблюдения, и записью, которая была создана, удалена или изменена". Один из resolve()
методы должны работать, если каталоги имеют общий корень.
Что касается вашего второго пункта, не могли бы вы создать класс, который бы содержал и WatchEvent, и ключ, и чтобы второй типовой параметр SwingWorker был такого типа? Извините, я знаю, что вы уже подумали об этом, поэтому я предполагаю, что мой вопрос: есть ли недостатки в этом?