Java

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

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

説明

  • WatchService を使うことで、指定した Path の変更を監視できる
        WatchService watcher;
        try {
            watcher = FileSystems.getDefault().newWatchService();

            ...
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
  • まず、 FileSystems.getDefault().newWatchService()WatchService のインスタンスを取得する
import static java.nio.file.StandardWatchEventKinds.*;

...

            Watchable path = Paths.get("./build");
            path.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
  • 次に、監視したい Watchableregister() メソッドに WatchService を渡すことで監視がスタートする
    • PathWatchable を継承しているので、監視対象として使用できる
  • 第二引数以降に、監視したいイベントを指定する
    • StandardWatchEventKinds に定義された定数を指定する
    • ただし、 OVERFLOW は指定不要(詳細後述)
        while (true) {
            WatchKey watchKey;
            try {
                watchKey = watcher.take();
            } catch (InterruptedException e) {
                System.err.println(e.getMessage());
                return;
            }

            ...
        }
  • WatchServicetake() メソッドを実行すると、監視対象のイベントが発生するまで処理を待機する
  • 監視対象のイベントが発生すると、 WatchKey オブジェクトが返される
  • WatchKey は、 Watchableregister() で監視対象を登録したときに生成されるオブジェクトで、監視対象ごとに発生したイベント情報の参照や、監視のキャンセルといった監視状態の制御ができる
            for (WatchEvent<?> event : watchKey.pollEvents()) {
                Kind<?> kind = event.kind();
                Object context = event.context();
                System.out.println("kind=" + kind + ", context=" + context);
            }
  • WatchKeypollEvents() で、発生したイベントの情報(WatchEvent)を取得できる
  • WatchEvent からは、次の情報を取得できる
    • kind:発生したイベントの種類
    • context:作成・変更・削除されたエントリ(ファイルやフォルダ)への相対パスを持った Path オブジェクト
    • count:この種類のイベントが発生した回数
            if (!watchKey.reset()) {
                System.out.println("WatchKey が無効になりました");
                return;
            }
  • 最後に、 WatchKeyreset() を実行する
    • なぜ reset() を呼ばなければならないのかについては後述
  • reset() の戻り値は boolean になっており、 WatchKey がまだ有効かどうかが返るようになっている
    • false の場合、その WatchKey は無効になっており監視を続けることはできない
    • 無効になる条件は次の3つのいずれかが該当する
      1. WatchKey.cancel() で明示的にキャンセルされた
      2. 監視対象のオブジェクト(ディレクトリなど)が削除されるなどしてアクセスできなくなり、暗黙的にキャンセルされた
      3. WatchService.close()WatchService から監視が取り消された

WatchKey の状態変化

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

watchservice.png

WatchService と WatchKey の関係

watchservice.jpg

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

まとめると、

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

OVERFLOW イベント

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

説明

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

複数のフォルダを監視する

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

説明

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

参考