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