java.nio.file.WatchService 實時監控文件變化的示例代碼

在平時的開發過程中,會有很多場景需要實時監聽文件的變化,如下:
1、通過實時監控 mysql 的 binlog 日志實現數據同步
2、修改配置文件後,希望系統可以實時感知
3、應用系統將日志寫入文件中,日志監控系統可以實時抓取日志,分析日志內容並進行報警
4、類似 ide 工具,可以實時感知管理的工程下的文件變更

在 Java 語言中,從 JDK7 開始,新增瞭java.nio.file.WatchService類,用來實時監控文件的變化。

1.示例代碼

FileWatchedService 類:

package org.learn.file;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.List;

/**
 * 實時監控文件的變化
 *
 * @author zhibo
 * @date 2019-07-30 20:37
 */
public class FileWatchedService {
    private WatchService watchService;

    private FileWatchedListener listener;

    /**
     *
     * @param path 要監聽的目錄,註意該 Path 隻能是目錄,否則會報錯 java.nio.file.NotDirectoryException: /Users/zhibo/logs/a.log
     * @param listener 自定義的 listener,用來處理監聽到的創建、修改、刪除事件
     * @throws IOException
     */
    public FileWatchedService(Path path, FileWatchedListener listener) throws IOException {
        watchService = FileSystems.getDefault().newWatchService();
        path.register(watchService,
                /// 監聽文件創建事件
                StandardWatchEventKinds.ENTRY_CREATE,
                /// 監聽文件刪除事件
                StandardWatchEventKinds.ENTRY_DELETE,
                /// 監聽文件修改事件
                StandardWatchEventKinds.ENTRY_MODIFY);
//
//            path.register(watchService,
//                    new WatchEvent.Kind[]{
//                            StandardWatchEventKinds.ENTRY_MODIFY,
//                            StandardWatchEventKinds.ENTRY_CREATE,
//                            StandardWatchEventKinds.ENTRY_DELETE
//                    },
//                    SensitivityWatchEventModifier.HIGH);

        this.listener = listener;
    }

    private void watch() throws InterruptedException {
        while (true) {
            WatchKey watchKey = watchService.take();
            List<WatchEvent<?>> watchEventList = watchKey.pollEvents();
            for (WatchEvent<?> watchEvent : watchEventList) {
                WatchEvent.Kind kind = watchEvent.kind();

                WatchEvent<Path> curEvent = (WatchEvent<Path>) watchEvent;
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    listener.onOverflowed(curEvent);
                    continue;
                } else if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
                    listener.onCreated(curEvent);
                    continue;
                } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
                    listener.onModified(curEvent);
                    continue;
                } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                    listener.onDeleted(curEvent);
                    continue;
                }
            }

            /**
             * WatchKey 有兩個狀態:
             * {@link sun.nio.fs.AbstractWatchKey.State.READY ready} 就緒狀態:表示可以監聽事件
             * {@link sun.nio.fs.AbstractWatchKey.State.SIGNALLED signalled} 有信息狀態:表示已經監聽到事件,不可以接續監聽事件
             * 每次處理完事件後,必須調用 reset 方法重置 watchKey 的狀態為 ready,否則 watchKey 無法繼續監聽事件
             */
            if (!watchKey.reset()) {
                break;
            }

        }
    }

    public static void main(String[] args) {
        try {
            Path path = Paths.get("/Users/zhibo/logs/");
            FileWatchedService fileWatchedService = new FileWatchedService(path, new FileWatchedAdapter());
            fileWatchedService.watch();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

FileWatchedListener 類:

package org.learn.file;

import java.nio.file.Path;
import java.nio.file.WatchEvent;

public interface FileWatchedListener {
    void onCreated(WatchEvent<Path> watchEvent);

    void onDeleted(WatchEvent<Path> watchEvent);

    void onModified(WatchEvent<Path> watchEvent);

    void onOverflowed(WatchEvent<Path> watchEvent);
}

FileWatchedAdapter 類:

package org.learn.file;

import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;

/**
 * 文件監聽適配器
 *
 * @author zhibo
 * @date 2019-07-31 11:07
 */
public class FileWatchedAdapter implements FileWatchedListener {

    @Override
    public void onCreated(WatchEvent<Path> watchEvent) {
        Path fileName = watchEvent.context();
        System.out.println(String.format("文件【%s】被創建,時間:%s", fileName, now()));
    }

    @Override
    public void onDeleted(WatchEvent<Path> watchEvent) {
        Path fileName = watchEvent.context();
        System.out.println(String.format("文件【%s】被刪除,時間:%s", fileName, now()));
    }

    @Override
    public void onModified(WatchEvent<Path> watchEvent) {
        Path fileName = watchEvent.context();
        System.out.println(String.format("文件【%s】被修改,時間:%s", fileName, now()));
    }

    @Override
    public void onOverflowed(WatchEvent<Path> watchEvent) {
        Path fileName = watchEvent.context();
        System.out.println(String.format("文件【%s】被丟棄,時間:%s", fileName, now()));
    }

    private String now(){
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        return dateFormat.format(Calendar.getInstance().getTime());
    }
}

執行以上代碼,啟動監控任務,然後我在/Users/zhibo/logs/目錄中創建、修改、刪除文件,命令如下:

在這裡插入圖片描述

應用程序感知到文件變化,打印日志如下:

在這裡插入圖片描述

2.其實並沒有實時

大傢可以看到,監控任務基本上是以 10 秒為單位進行日志打印的,也就是說修改一個文件,WatchService 10秒之後才能感知到文件的變化,沒有想象中的那麼實時。根據以上的經驗,推測可能是 WatchService 做瞭定時的操作,時間間隔為 10 秒。通過翻閱源代碼發現,在 PollingWatchService 中確實存在一個固定時間間隔的調度器,如下圖:

在這裡插入圖片描述

該調度器的時間間隔有 SensitivityWatchEventModifier 進行控制,該類提供瞭 3 個級別的時間間隔,分別為2秒、10秒、30秒,默認值為 10秒。SensitivityWatchEventModifier 源碼如下:

package com.sun.nio.file;

import java.nio.file.WatchEvent.Modifier;

public enum SensitivityWatchEventModifier implements Modifier {
    HIGH(2),
    MEDIUM(10),
    LOW(30);

    private final int sensitivity;

    public int sensitivityValueInSeconds() {
        return this.sensitivity;
    }

    private SensitivityWatchEventModifier(int var3) {
        this.sensitivity = var3;
    }
}

通過改變時間間隔來進行驗證,將

        path.register(watchService,
                /// 監聽文件創建事件
                StandardWatchEventKinds.ENTRY_CREATE,
                /// 監聽文件刪除事件
                StandardWatchEventKinds.ENTRY_DELETE,
                /// 監聽文件修改事件
                StandardWatchEventKinds.ENTRY_MODIFY);

修改為:

    path.register(watchService,
                    new WatchEvent.Kind[]{
                            StandardWatchEventKinds.ENTRY_MODIFY,
                            StandardWatchEventKinds.ENTRY_CREATE,
                            StandardWatchEventKinds.ENTRY_DELETE
                    },
                    SensitivityWatchEventModifier.HIGH);

查看日志,發現正如我們的推斷,WatchService 正以每 2 秒的時間間隔感知文件變化。
在 stackoverflow 中也有人提出瞭該問題,問題:Is Java 7 WatchService Slow for Anyone Else,我的 mac 系統中確實存在該問題,由於手頭沒有 windows、linux 系統,因此無法進行這兩個系統的驗證。

到此這篇關於java.nio.file.WatchService 實時監控文件變化的文章就介紹到這瞭,更多相關java.nio.file.WatchService 監控文件變化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: