LoginSignup
1
3

More than 5 years have passed since last update.

ディレクトリの変更監視方法メモ (Java、Kotlin)

Last updated at Posted at 2018-03-04

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()とほぼ同様な模様。キーが有効である条件は

    1. そのcancelメソッドの呼出しによって明示的に取り消される
    2. そのオブジェクトにアクセスできなくなったために暗黙的に取り消される
    3. 監視サービスを閉じることで取り消される

    ということで、今回のコードでは通常起こらない……はず。

  • ファイルを作成などした後、WatchService.take()が値を返すまではかなりの時間がある(自分の環境では5秒ほど)。遅い。

改善案

  • コールバックの実行は別スレッドへ
  • 参照がなくなった時自動close()を試みる
  • 監視を行うスレッドは各Watcherで共通にする?

所感

使いづらい...

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3