WatchService 使い方メモ

Java でフォルダやファイルの変更を監視するための `WatchService` について、使い方をメモ。

# 環境
## OS
Window 10

## Java
```
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
```

# Hello World
```java
package sample.watch;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.Watchable;

import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.WatchEvent.*;

public class Main {

    public static void main(String[] args) {
        WatchService watcher;
        try {
            watcher = FileSystems.getDefault().newWatchService();

            Watchable path = Paths.get("./build");
            path.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        while (true) {
            WatchKey watchKey;
            try {
                watchKey = watcher.take();
            } catch (InterruptedException e) {
                System.err.println(e.getMessage());
                return;
            }

            for (WatchEvent<?> event : watchKey.pollEvents()) {
                Kind<?> kind = event.kind();
                Object context = event.context();
                System.out.println("kind=" + kind + ", context=" + context);
            }
            
            if (!watchKey.reset()) {
                System.out.println("WatchKey が無効になりました");
                return;
            }
        }
    }
}
```

**実行結果**

![watchservice.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/4ddff1c0-13a9-69b9-53db-aee8e1d0d2fa.gif)

**説明**

- `WatchService` を使うことで、指定した `Path` の変更を監視できる

```java
        WatchService watcher;
        try {
            watcher = FileSystems.getDefault().newWatchService();

            ...
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
```

- まず、 `FileSystems.getDefault().newWatchService()` で `WatchService` のインスタンスを取得する

```java
import static java.nio.file.StandardWatchEventKinds.*;

...

            Watchable path = Paths.get("./build");
            path.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
```

- 次に、監視したい `Watchable` の `register()` メソッドに `WatchService` を渡すことで監視がスタートする
    - `Path` は `Watchable` を継承しているので、監視対象として使用できる
- 第二引数以降に、監視したいイベントを指定する
    - [StandardWatchEventKinds](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/StandardWatchEventKinds.html) に定義された定数を指定する
    - ただし、 `OVERFLOW` は指定不要（詳細後述）

```java
        while (true) {
            WatchKey watchKey;
            try {
                watchKey = watcher.take();
            } catch (InterruptedException e) {
                System.err.println(e.getMessage());
                return;
            }

            ...
        }
```

- `WatchService` の `take()` メソッドを実行すると、監視対象のイベントが発生するまで処理を待機する
- 監視対象のイベントが発生すると、 `WatchKey` オブジェクトが返される
- `WatchKey` は、 `Watchable` の `register()` で監視対象を登録したときに生成されるオブジェクトで、監視対象ごとに発生したイベント情報の参照や、監視のキャンセルといった監視状態の制御ができる

```java
            for (WatchEvent<?> event : watchKey.pollEvents()) {
                Kind<?> kind = event.kind();
                Object context = event.context();
                System.out.println("kind=" + kind + ", context=" + context);
            }
```

- `WatchKey` の `pollEvents()` で、発生したイベントの情報(`WatchEvent`)を取得できる
- `WatchEvent` からは、次の情報を取得できる
    - `kind`：発生したイベントの種類
    - `context`：作成・変更・削除されたエントリ（ファイルやフォルダ）への相対パスを持った `Path` オブジェクト
    - `count`：この種類のイベントが発生した回数

```java
            if (!watchKey.reset()) {
                System.out.println("WatchKey が無効になりました");
                return;
            }
```

- 最後に、 `WatchKey` の `reset()` を実行する
    - なぜ `reset()` を呼ばなければならないのかについては後述
- `reset()` の戻り値は `boolean` になっており、 `WatchKey` がまだ有効かどうかが返るようになっている
    - `false` の場合、その `WatchKey` は無効になっており監視を続けることはできない
    - 無効になる条件は次の３つのいずれかが該当する
        1. `WatchKey.cancel()` で明示的にキャンセルされた
        2. 監視対象のオブジェクト（ディレクトリなど）が削除されるなどしてアクセスできなくなり、暗黙的にキャンセルされた
        3. `WatchService.close()` で `WatchService` から監視が取り消された

# WatchKey の状態変化
- `WatchKey` には `READY` と `SIGNALLED` という２つの状態が存在する
- `Watchable.register()` で生成されたときは `READY` 状態となっている
- イベントが検知されると、 `SIGNALLED` に変わる
- `WatchKey.reset()` を実行すると、再び `READY` 状態に戻る

![watchservice.png](https://qiita-image-store.s3.amazonaws.com/0/28302/61a06285-a2bb-ebd2-093e-20db3764f866.png)

# WatchService と WatchKey の関係
![watchservice.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5a6320a8-64b1-9144-69e7-1c988c76e804.jpeg)


- `WatchService` には、 `Watchable.register()` を実行したときに作成された `WatchKey` が保存されている
- また、状態が `SIGNALLED` となった `WatchKey` だけが詰められたキューも保持している
    - 作成された直後の `WatchKey` は `READY` 状態なので、キューの中には入っていない
    - 監視対象のイベントが検知されると、該当する `WatchKey` の状態が `READY` から `SIGNALLED` に変更される
    - 状態が `SIGNALLED` となった `WatchKey` は、 `WatchService` の持つキューの中に追加される
- `WatchService` を使用しているクライアント側の実装は、 `WatchService.take()` もしくは `poll()` メソッドによってキューの先頭に存在する `WatchKey` を取り出すことができる（キューからは削除される）
    - `take()` はキューが空だと処理がブロックされ、キューに `WatchKey` が追加されるまで待機する
    - `poll()` は、キューが空の場合はブロックせずに `null` を返す
        - `poll()` については、引数でタイムアウト時間を指定して、それまでは待機させることも可能
- `take()` または `poll()` でキューから取り出された `WatchKey` は、そのままだと再びキューに入れられることはない
    - キューから取り出した `WatchKey` の状態は `SIGNALLED` のままになっている
    - `WatchKey` が `SIGNALLED` のときに追加で監視対象のイベントが発生しても、再びキューに入れられることはない
        - 発生したイベント情報自体は、ちゃんと `WatchKey` に追加される
        - `WatchKey` はスレッドセーフに作られているので、イベントの処理中にイベント情報が追加されても、内部の状態が壊れるようなことはない
            - `pollEvents()` は、その時点で `WatchKey` が持っていたイベント情報のコピーを返している
            - また、 `pollEvents()` で取得したイベントの情報は、 `WatchKey` 内部からは削除されている
    - 要は
        - 一度キューから取り出した `WatchKey` は、そのままにしていると、もう一度キューに入れられることはない
        - キューに入っていないということは、 `take()` や `poll()` で取り出せない  
          ということ
- `WatchKey` が再びキューに入るようにするためには、状態を `READY` に戻す必要がある
    - `WatchKey` の状態を `READY` に戻すには、 `WatchKey` の `reset()` メソッドを実行する
    - `WatchKey` の持つイベント情報が空になっている（未処理のイベントがない）場合は、状態が `READY` に戻る
        - その後、またイベントが発生したら、 `SIGNALLED` になってキューに追加される
    - イベント情報が空になっていない（未処理のイベントがある）場合は、 `SIGNALLED` 状態のままキューに追加される

まとめると、

- 作成した直後の `WatchKey` は、 `READY` 状態となっている
- イベントを検知して `SIGNALLED` に変更されると、 `WatchService` 内のキューに追加される
- `take()`, `poll()` で、キューから `WatchKey` を取り出すことができる
- 一度キューから出た `WatchKey` は、 `reset()` を実行しない限り、勝手にキューに戻ることは無い
    - 未処理のイベントがあれば `SIGNALLED` のままキューに入れられる  
      未処理のイベントがなければ `READY` 状態になり、次に監視対象のイベントが発生したときに再び `SENGALLED` になってキューに入れられる

# OVERFLOW イベント
```java
package sample.watch;

...

public class Main {

    public static void main(String[] args) {
        ...

        while (true) {
            WatchKey watchKey;
            try {
                watchKey = watcher.take();
                Thread.sleep(1000); ★追加
            } catch (InterruptedException e) {
                ...
            }

            ...
        }
    }
}
```

**実行結果**

![watchservice.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/67c11de6-6e6f-0baf-e082-88729268684e.gif)

**説明**

- イベントが何らかの理由により失われたり破棄された場合、 `OVERFLOW` という特殊なイベントが使用される
- 例えば、実行環境によっては `WatchKey` の中に保存されるイベントの数に上限が設けられている可能性があり、その上限を超えたイベントが発生すると、 `OVERFLOW` イベントが使用される
- 上記例は、イベントの取得(`pollEvents()`)の前にあえて１秒待機し、大量のファイル生成のイベントを全て `WatchKey` の中に保存させ強制的に `OVERFLOW` を発生させている
- `OVERFLOW` イベントは、 `Watchable.register()` での登録で監視対象のイベントとして指定していなくても、条件が満たされた場合は通知されるようになっている
- よって、 `OVERFLOW` が発生した場合の処理は常に考慮した実装にしておいたほうがいい
    - 無視していいなら無視し、エラーにするならエラーにする（どちらにすべきかは作っているアプリ次第）

# 複数のフォルダを監視する
```java
package sample.watch;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.Watchable;

import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.WatchEvent.*;

public class Main {

    public static void main(String[] args) {
        WatchService watcher;
        WatchKey fooKey;
        WatchKey barKey;
        try {
            watcher = FileSystems.getDefault().newWatchService();

            Watchable foo = Paths.get("./build/foo");
            fooKey = foo.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);

            Watchable bar = Paths.get("./build/bar");
            barKey = bar.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        while (true) {
            WatchKey watchKey;
            try {
                watchKey = watcher.take();
            } catch (InterruptedException e) {
                System.err.println(e.getMessage());
                return;
            }

            for (WatchEvent<?> event : watchKey.pollEvents()) {
                Kind<?> kind = event.kind();
                
                if (kind == OVERFLOW) {
                    continue;
                }

                Object context = event.context();

                String directory;
                if (watchKey == fooKey) {
                    directory = "foo";
                } else if (watchKey == barKey) {
                    directory = "bar";
                } else {
                    directory = "unknown";
                }

                System.out.println("directory=" + directory + ", kind=" + kind + ", context=" + context);
            }
            
            if (!watchKey.reset()) {
                System.out.println("WatchKey が無効になりました");
                return;
            }
        }
    }
}
```

**実行結果**

![watchservice.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/b49c9f9a-2e36-891d-adc6-b292d9bfc587.gif)

**説明**

- １つの `WatchService` で複数のフォルダを監視できる
- その場合、 `WatchService.take()` で取得した `WatchKey` がどちらのフォルダを指しているのかを識別する必要がある
- `Watchable.register()` の戻り値として返る `WatchKey` を保存しておけば、それと比較することでどちらのフォルダのイベントかを識別できる

# 参考
- [ディレクトリの変更監視（Java?チュートリアル > 重要なクラス > 基本的なI/O）](https://docs.oracle.com/cd/E26537_01/tutorial/essential/io/notification.html)
- [WatchDir.java](https://docs.oracle.com/javase/tutorial/displayCode.html?code=https://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java)
- [WatchService (Java Platform SE 8 )](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/WatchService.html)
- [Watchable (Java Platform SE 8 )](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Watchable.html)
- [WatchKey (Java Platform SE 8 )](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/WatchKey.html)
- [WatchEvent (Java Platform SE 8 )](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/WatchEvent.html)
- [StandardWatchEventKinds (Java Platform SE 8 )](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/StandardWatchEventKinds.html)
- [java - In jdk7 watch service API, when will the OVERFLOW event be thrown? - Stack Overflow](https://stackoverflow.com/questions/18586091/in-jdk7-watch-service-api-when-will-the-overflow-event-be-thrown)