WatchService和SwingWorker:如何正确地做到这一点?

WatchService听起来像是一个令人兴奋的想法…不幸的是,它似乎与教程/ api中的警告一样低,并不真正适合Swing事件模型(或者我错过了一些明显的,非零概率

从教程中的 WatchDir 示例中获取代码(只是为了处理单个目录),我基本上结束了

  • 扩展SwingWorker
  • 在构造函数中执行注册工作
  • 把无限循环放在doInBackground中等待一个键
  • 通过key.pollEvents()检索时发布每个WatchEvent
  • 通过使用已删除/创建的文件作为newValue触发propertyChangeEvents来处理块

    @SuppressWarnings("unchecked") public class FileWorker extends SwingWorker<Void, WatchEvent> { 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) event); } // reset key return if directory no longer accessible boolean valid = key.reset(); if (!valid) { break; } } return null; } @Override protected void process(List<WatchEvent> chunks) { super.process(chunks); for (WatchEvent 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 model = new DefaultListModel(); 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摇摆论坛

附录

阅读WatchKey的api文档:

如果有多个线程从监视服务检索已发出信号的密钥,则应注意确保仅在处理了对象的事件之后才调用reset方法。

似乎暗示事件应该

  1. 在检索WatchKey的同一线程上处理
  2. 重置密钥后不应触摸

不完全确定,但结合(未来)要求递归观看目录(不止一个)决定遵循@Eels建议,有点 – 将很快发布我定居的代码

编辑只是接受了我自己的答案 – 如果有人有合理的反对意见,他会谦卑地回复

实际上,@ Eels的评论并没有停止在我的脑后 – 并最终注册:它是要走的路,但是没有必要任何“人工”结构,因为我们已经拥有了完美的候选者 – 它是PropertyChangeEvent本身:-)

从我的问题中得出整个过程描述,前三个子弹保持不变

  • 相同:扩展SwingWorker
  • 相同:在构造函数中执行注册工作
  • 相同:把无限循环等待doInBackground中的一个键
  • 更改:通过key.pollEvents检索时从每个WatchEvent创建相应的PropertyChangeEvent并发布PropertyChangeEvent
  • 已更改:触发先前创建的事件(块)

修改过FileWorker

 @SuppressWarnings("unchecked") public class FileWorker extends SwingWorker { 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) 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 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 chunks) { super.process(chunks); for (PropertyChangeEvent event : chunks) { getPropertyChangeSupport().firePropertyChange(event); } } } 

因为你的后台主题完全专注于观看,所以take()是正确的选择。 它有效地隐藏了平台相关的实现,可以转发或轮询。 例如,如果您的后台线程还需要检查与WatchService串联的其他队列,则其中一个poll()方法是合适的。

附录:因为WatchKey具有状态,所以它可能不会被转发到process()WatchEventcontext()是“向watch服务注册的目录与创建,删除或修改的条目之间的相对路径”。 如果目录共享一个公共根,则其中一个resolve()方法应该有效。

关于你的第二点,难道你不能创建一个同时包含WatchEvent和key并且让SwingWorker的第二个generics参数成为这种类型的类吗? 对不起,我知道你已经想过这个了,所以我想我的问题是:这样做有什么不妥之处吗?