JavaのWatchServiceの使い方、軽い気持ちで始めたら予想外に嵌ったのでメモ。コードはKotlinですがJavaでもやることは同じかと。
class Watcher private constructor(
val path: Path,
val listener: (e: WatchEvent<Path>, target: Path) -> Unit
) : AutoCloseable {
private val executor = Executors.newSingleThreadExecutor()
private val watchService = path.fileSystem.newWatchService()
init {
val watchKey = path.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)
executor.submit {
while (true) {
val key = watchService.take()
assert(watchKey == key, { "一つのキーしか登録しないので、必ず一致するはず" })
for (e in watchKey.pollEvents()) {
@Suppress("UNCHECKED_CAST") // WatchKeyのイベントが上記である場合、常にPathであることは保証されている
listener(
e as WatchEvent<Path>,
path.resolve(e.context())
)
}
val valid = watchKey.reset()
if (!valid) throw RuntimeException("普通は起こらないはず")
}
}
}
override fun close() {
executor.shutdownNow()
executor.awaitTermination(1, TimeUnit.SECONDS) // この値は適当
watchService.close()
}
companion object {
fun watch(
path: Path,
listener: (e: WatchEvent<Path>, target: Path) -> Unit
): Watcher {
return Watcher(path, listener)
}
}
}
ポイント
- 監視は
ExecutorService
を使い別スレッドで行う。WatchService.take()
は変更が検知されるまでブロックするため。poll()
ならブロックせずnull
を返すが、いずれにしても別スレッドで行ったほうが都合がいい。 -
WatchKey.pollEvents()
はワイルドカード型のWatchEvent
のリストを返す。しかしWatchEvent.context()
はドキュメントで、イベントがENTRY_CREATE
,ENTRY_DELETE
,ENTRY_MODIFY
のいずれかである限り、常にPath
を返すことを保証している。要するに必ずPath
になるので、無条件にキャストして問題ない。 -
WatchEvent.context()
が返すPath
は、監視しているディレクトリからの相対パスで返される。従って返されるPath
をそのまま使おうとすると、カレントディレクトリを基準に当該ファイルを探そうとする。そのため(監視ディレクトリがカレントでない限り)ファイルが見つからず、例外が発生する。この動作はかなり非直感的でしかも不便なため、リスナーには別途、監視対象ディレクトリのPath
を基準に解決したPath
を返している。 -
WatchKey.reset()
で監視を再開する。戻り値はリセットに成功したかを表すが、ドキュメントを読む限り実質的にisValid()
とほぼ同様な模様。キーが有効である条件は- そのcancelメソッドの呼出しによって明示的に取り消される
- そのオブジェクトにアクセスできなくなったために暗黙的に取り消される
- 監視サービスを閉じることで取り消される
ということで、今回のコードでは通常起こらない……はず。
ファイルを作成などした後、
WatchService.take()
が値を返すまではかなりの時間がある(自分の環境では5秒ほど)。遅い。
改善案
- コールバックの実行は別スレッドへ
- 参照がなくなった時自動
close()
を試みる - 監視を行うスレッドは各
Watcher
で共通にする?
所感
使いづらい...