CodeGym /コース /JAVA 25 SELF /ファイルシステムの変更監視: WatchService

ファイルシステムの変更監視: WatchService

JAVA 25 SELF
レベル 40, レッスン 4
使用可能

1. はじめに

WatchService は Java NIO(New IO)の一部で、ファイルシステムの変更をリアルタイムに監視できます。フォルダー用のアラームのようなもので、誰かがファイルを追加・削除・変更すると、すぐに通知を受け取れます。この機能は Java 7 の NIO.2 で導入されました。それ以前は、開発者はフォルダーを手動でポーリング(polling)するか、サードパーティ製ライブラリを使う必要がありました。

WatchService の実用例は多岐にわたります。新規ファイルの自動処理、ログの記録やバックアップ作成、フォルダーとサーバー/クラウドの同期、設定ファイルの変更監視などに役立ちます。

監視対象ディレクトリの登録

変更の監視を始めるには、次を実行します。

  1. WatchService のインスタンスを取得する。
  2. 対象フォルダーを登録し、監視するイベントを指定する。

WatchService を取得する

import java.nio.file.*;

WatchService watchService = FileSystems.getDefault().newWatchService();

フォルダーを登録する

登録には Path オブジェクトの register メソッドを使います。

Path dir = Paths.get("data/uploads");
dir.register(
    watchService,
    StandardWatchEventKinds.ENTRY_CREATE,   // ファイル/フォルダーの作成
    StandardWatchEventKinds.ENTRY_DELETE,   // ファイル/フォルダーの削除
    StandardWatchEventKinds.ENTRY_MODIFY    // ファイル/フォルダーの変更
);

説明:

  • ENTRY_CREATE — 何かが追加されました。
  • ENTRY_DELETE — 何かが削除されました。
  • ENTRY_MODIFY — ファイルが変更されました(例: 追記されたなど)。

重要: WatchService は1回の登録で1つのフォルダーのみを監視します(サブフォルダーは含まれません)。ツリー全体を監視したい場合は、各サブフォルダーを個別に登録する必要があります。

2. イベント処理: 待機ループ

監視の設定ができたら(「ビッグ・ブラザー」というよりは「近所の見張り」程度のものですが)、あとはイベントを待ちます。WatchService は「イベントキュー」パターンを実装しており、何かが起きるとイベントがキューに積まれます。

基本ループ

while (true) {
    // イベントの発生を待つ(ブロッキング呼び出し)
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        // イベント種別: 作成・削除・変更
        WatchEvent.Kind<?> kind = event.kind();

        // ファイル/フォルダー名(監視対象フォルダーからの相対 Path)
        Path filename = (Path) event.context();

        if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
            System.out.println("ファイル/フォルダーが作成されました: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
            System.out.println("ファイル/フォルダーが削除されました: " + filename);
        } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
            System.out.println("ファイル/フォルダーが変更されました: " + filename);
        }
    }

    // 必ずキーをリセット。そうしないと監視が止まります!
    boolean valid = key.reset();
    if (!valid) {
        break; // フォルダーにアクセスできないため終了
    }
}

仕組み

  • WatchService.take() — イベントが来るまでスレッドをブロックします(ノンブロッキングにしたい場合は poll() を使えます)。
  • key.pollEvents() — 蓄積されたすべてのイベントの一覧。
  • event.context() — 変更対象のファイルまたはフォルダー名(監視ディレクトリからの相対)。
  • イベント処理後は必ず key.reset() を呼びます。フォルダーが削除されるなどして利用不能になった場合、reset() は false を返すため、ループを終了してかまいません。

完全な例: フォルダー "data/uploads" を監視する

学習用アプリにアップロード用フォルダー向けの簡単な「アラーム」を追加してみましょう。

import java.nio.file.*;
import java.io.IOException;

public class WatcherDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        Path dir = Paths.get("data/uploads");
        if (!Files.exists(dir)) {
            Files.createDirectories(dir);
        }

        WatchService watchService = FileSystems.getDefault().newWatchService();
        dir.register(
            watchService,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_DELETE,
            StandardWatchEventKinds.ENTRY_MODIFY
        );

        System.out.println("フォルダーを監視中: " + dir.toAbsolutePath());

        while (true) {
            WatchKey key = watchService.take(); // イベントを待つ

            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();
                Path filename = (Path) event.context();
                System.out.printf("[%s] %s\n", kind.name(), filename);
            }

            boolean valid = key.reset();
            if (!valid) {
                System.out.println("フォルダーにアクセスできません。監視を終了します.");
                break;
            }
        }
    }
}

試してみましょう: このコードを実行し、フォルダー "data/uploads" でファイルを作成・削除・変更してみてください。プログラムはすぐに反応します。

3. WatchService の制約と注意点

1 つのフォルダーのみ(サブフォルダーは対象外)

WatchService は登録したフォルダーのみを監視します。配下にサブフォルダーがあっても、その中の変更は検知されません。各サブフォルダーを個別に登録する必要があります。

どうすればよいか
階層全体を監視したい場合は、すべてのサブフォルダーを走査して個別に登録する必要があります。例えば、新しいサブフォルダーが作成されたら、すぐにそれも登録します。

OS ごとの挙動

Windows: WatchService は比較的安定して動作しますが、ときどき複数のイベントを 1 件にまとめてしまうことがあります(大きなファイルをコピーする場合など)。

Linux/macOS: 実装は OS の機構(inotify、kqueue)に基づきます。イベントが遅延して届くこともあれば、逆に多すぎることもあります(例えば、保存のたびに ENTRY_MODIFY が発生するなど)。

名前レベルのイベントのみ

WatchService は変更されたオブジェクトの名前(監視対象フォルダーからの相対)しか知らせません。ファイル内部で何が変わったかまでは分かりません。詳細が必要な場合は、ファイルを手動で読み取って確認してください。

高負荷時のイベント取りこぼし

短時間に非常に多くの変更が発生すると(例: 何千ものファイルを一括コピー)、イベントキューがあふれて一部のイベントが失われる可能性があります。クリティカルな用途では追加の整合性チェックを併用してください。

4. 実用例

新規ファイルの自動処理

例えば、フォルダー "photos/incoming" に現れる新しい画像を自動処理するプログラムを書くとします。

Path dir = Paths.get("photos/incoming");
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);

while (true) {
    WatchKey key = watchService.take();

    for (WatchEvent<?> event : key.pollEvents()) {
        if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
            Path filename = (Path) event.context();
            if (filename.toString().endsWith(".jpg")) {
                System.out.println("新しい写真: " + filename);
                // ここで処理を追加できます(コピー、圧縮、解析など)
            }
        }
    }
    key.reset();
}

変更ログを取る簡易ロガー

すべてのイベントを別のログファイルに保存できます。

import java.nio.file.*;
import java.io.*;
import java.time.LocalDateTime;

public class SimpleLogger {
    public static void main(String[] args) throws IOException, InterruptedException {
        Path dir = Paths.get("logs/monitored");
        Files.createDirectories(dir);

        Path logFile = Paths.get("logs/changes.log");
        try (BufferedWriter writer = Files.newBufferedWriter(logFile, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
            WatchService watchService = FileSystems.getDefault().newWatchService();
            dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);

            System.out.println("監視対象: " + dir);

            while (true) {
                WatchKey key = watchService.take();

                for (WatchEvent<?> event : key.pollEvents()) {
                    String log = String.format("%s [%s] %s\n",
                        LocalDateTime.now(), event.kind().name(), event.context());
                    writer.write(log);
                    writer.flush();
                    System.out.print(log);
                }
                key.reset();
            }
        }
    }
}

新しいサブフォルダーの作成を検知し、その場で登録する

監視対象フォルダー内に新しいサブフォルダーが作成されたら、すぐにそれも監視に登録できます。

if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
    Path createdPath = dir.resolve((Path) event.context());
    if (Files.isDirectory(createdPath)) {
        createdPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                             StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
        System.out.println("新しいサブフォルダーの監視を開始: " + createdPath);
    }
}

5. 重要な注意点とありがちなミス

エラー1: key.reset() の呼び出し忘れ。 イベント処理後にキーをリセットしないと、そのフォルダーの監視は停止し、以後イベントは届きません。初心者がはまりがちな典型例で、「動いているように見えるのに、ある時点でぱったり出力が止む」という状況になります。

エラー2: 例外を無視する。 ファイルシステム操作には常に予期しない事態がつきものです。フォルダーが削除されたり、ディスクが外れたり、権限が変わることがあります。例外(IOException、ClosedWatchServiceException)を処理しないと、プログラムが異常終了する恐れがあります。

エラー3: 1 つのフォルダーだけを監視してしまう。 フォルダーを登録すれば配下のすべてのフォルダーも監視されると期待しがちですが、そうではありません。階層全体を監視したい場合は、再帰的な登録を実装してください。

エラー4: メインスレッドのブロック。 WatchService.take() はイベントが来るまでスレッドをブロックします。メインスレッドで他の処理も行う必要があるなら、監視は別スレッドで実行してください。

エラー5: 高負荷時のイベント取りこぼし。 フォルダーで変更が多発すると、イベントキューがあふれる可能性があります。クリティカルなアプリケーションでは、定期的な状態照合(例えば 1 分おきにファイル一覧を比較)を併用しましょう。

エラー6: 相対パスの扱いを誤る。 event.context() は監視フォルダーからの相対名を返します。絶対パスが必要な場合は dir.resolve((Path) event.context()) を使ってください。

1
タスク
JAVA 25 SELF, レベル 40, レッスン 4
ロック未解除
新しいフォルダの監視者の設定 👁️‍🗨️
新しいフォルダの監視者の設定 👁️‍🗨️
1
タスク
JAVA 25 SELF, レベル 40, レッスン 4
ロック未解除
重要なディレクトリでの変更および削除の監視 🚨
重要なディレクトリでの変更および削除の監視 🚨
1
アンケート/クイズ
ディレクトリの操作、レベル 40、レッスン 4
使用不可
ディレクトリの操作
ファイルとディレクトリの操作
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION