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を使うことで、指定した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);
- 次に、監視したい
Watchableのregister()メソッドにWatchServiceを渡すことで監視がスタートする-
PathはWatchableを継承しているので、監視対象として使用できる
-
- 第二引数以降に、監視したいイベントを指定する
- StandardWatchEventKinds に定義された定数を指定する
- ただし、
OVERFLOWは指定不要(詳細後述)
while (true) {
WatchKey watchKey;
try {
watchKey = watcher.take();
} catch (InterruptedException e) {
System.err.println(e.getMessage());
return;
}
...
}
-
WatchServiceのtake()メソッドを実行すると、監視対象のイベントが発生するまで処理を待機する - 監視対象のイベントが発生すると、
WatchKeyオブジェクトが返される -
WatchKeyは、Watchableのregister()で監視対象を登録したときに生成されるオブジェクトで、監視対象ごとに発生したイベント情報の参照や、監視のキャンセルといった監視状態の制御ができる
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:この種類のイベントが発生した回数
-
if (!watchKey.reset()) {
System.out.println("WatchKey が無効になりました");
return;
}
- 最後に、
WatchKeyのreset()を実行する- なぜ
reset()を呼ばなければならないのかについては後述
- なぜ
-
reset()の戻り値はbooleanになっており、WatchKeyがまだ有効かどうかが返るようになっている-
falseの場合、そのWatchKeyは無効になっており監視を続けることはできない - 無効になる条件は次の3つのいずれかが該当する
-
WatchKey.cancel()で明示的にキャンセルされた - 監視対象のオブジェクト(ディレクトリなど)が削除されるなどしてアクセスできなくなり、暗黙的にキャンセルされた
-
WatchService.close()でWatchServiceから監視が取り消された
-
-
WatchKey の状態変化
-
WatchKeyにはREADYとSIGNALLEDという2つの状態が存在する -
Watchable.register()で生成されたときはREADY状態となっている - イベントが検知されると、
SIGNALLEDに変わる -
WatchKey.reset()を実行すると、再びREADY状態に戻る
WatchService と WatchKey の関係
-
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 イベント
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) {
...
}
...
}
}
}
実行結果
説明
- イベントが何らかの理由により失われたり破棄された場合、
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;
}
}
}
}
実行結果
説明
- 1つの
WatchServiceで複数のフォルダを監視できる - その場合、
WatchService.take()で取得したWatchKeyがどちらのフォルダを指しているのかを識別する必要がある -
Watchable.register()の戻り値として返るWatchKeyを保存しておけば、それと比較することでどちらのフォルダのイベントかを識別できる
参考
- ディレクトリの変更監視(Java?チュートリアル > 重要なクラス > 基本的なI/O)
- WatchDir.java
- WatchService (Java Platform SE 8 )
- Watchable (Java Platform SE 8 )
- WatchKey (Java Platform SE 8 )
- WatchEvent (Java Platform SE 8 )
- StandardWatchEventKinds (Java Platform SE 8 )
- java - In jdk7 watch service API, when will the OVERFLOW event be thrown? - Stack Overflow




