監聽檔案修改的四種方法

Howlet發表於2022-01-16

遇到了監聽配置檔案是否被修改的需求,因功能規模小,沒有加入 Apollo、config 等元件,所以得自己實現


1. 自行實現

第一想法是用定時任務去實現,下面是筆者的實現思路:FileModifyManager 來監聽管理全部檔案,要實現監聽介面 FileListener 並傳入給 FileModifyManager ,每當檔案發生變化就呼叫監聽介面的方法 doListener


1.1 FileListener

@FunctionalInterface
public interface FileListener {
    void doListener();
}

看了 Hutool 文件才知道這種設計叫鉤子函式,那筆者和 Hutool 作者思路也有相似之處



1.2 FileModifyManager

/**
 * @author Howl
 * @date 2022/01/15
 */
public class FileModifyManager {

    // 存放監聽的檔案及 FileNodeRunnable 節點
    private static ConcurrentHashMap<File, FileNodeRunnable> data = new ConcurrentHashMap<>(16);

    // 執行緒池執行定時監聽任務
    private static ScheduledExecutorService service = Executors.newScheduledThreadPool(20);

    // 單例模式--雙重校驗鎖
    private volatile static FileModifyManager instance = null;

    private FileModifyManager() {
    }

    public static FileModifyManager getInstance() {
        if (instance == null) {
            synchronized (FileModifyManager.class) {
                if (instance == null) {
                    instance = new FileModifyManager();
                }
            }
        }
        return instance;
    }

    // 開始監聽,預設 10 秒監聽一次
    public FileModifyManager startWatch(File file, FileListener fileListener) throws Exception {
        return startWatch(file, fileListener, 0, 1000 * 10, TimeUnit.MILLISECONDS);
    }

    public FileModifyManager startWatch(File file, FileListener fileListener, long delay, long period, TimeUnit timeUnit) throws Exception {
        FileNodeRunnable fileNodeRunnable = addFile(file, fileListener);
        ScheduledFuture<?> scheduledFuture = service.scheduleAtFixedRate(fileNodeRunnable, delay, period, timeUnit);
        fileNodeRunnable.setFuture(scheduledFuture);
        return instance;
    }

    // 停止監聽
    public FileModifyManager stopWatch(File file) {
        return stopWatch(file, true);
    }

    public FileModifyManager stopWatch(File file, boolean mayInterruptIfRunning) {
        FileNodeRunnable fileNodeRunnable = data.get(file);
        fileNodeRunnable.getFuture().cancel(mayInterruptIfRunning);
        removeFile(file);
        return instance;
    }

    // 是否監聽
    public boolean isWatching(File file) {
        return containsFile(file);
    }

    // 監聽列表
    public Set listWatching() {
        return getFileList();
    }


    // 管理檔案
    private FileNodeRunnable addFile(File file, FileListener fileListener) throws Exception {
        isFileExists(file);
        FileNodeRunnable fileNodeRunnable = new FileNodeRunnable(file, fileListener, file.lastModified());
        data.put(file, fileNodeRunnable);
        return fileNodeRunnable;
    }

    private void removeFile(File file) {
        data.remove(file);
    }

    private boolean containsFile(File file) {
        return data.containsKey(file);
    }

    private Set getFileList() {
        return data.keySet();
    }

    // 判斷檔案存在與否
    private void isFileExists(File file) throws Exception {
        if (!file.exists()) {
            throw new Exception("檔案或路徑不存在");
        }
    }

    // 檔案節點及其定時任務
    private class FileNodeRunnable implements Runnable {

        private File file;
        private long lastModifyTime;
        private FileListener listener;
        private ScheduledFuture future;

        FileNodeRunnable(File file, FileListener listener, long lastModifyTime) {
            this.file = file;
            this.listener = listener;
            this.lastModifyTime = lastModifyTime;
        }

        @Override
        public void run() {
            if (this.lastModifyTime != file.lastModified()) {
                System.out.println(file.toString() + " lastModifyTime is " + this.lastModifyTime);
                this.lastModifyTime = file.lastModified();
                listener.doListener();
            }
        }

        public ScheduledFuture getFuture() {
            return future;
        }

        public void setFuture(ScheduledFuture future) {
            this.future = future;
        }
    }
}

對外只暴露 startWatch、stopWatch、listWatching 三個方法,入參為 File 和 FileListener

Hutool 也是用了 HashMap 來存放對應的關係表,那筆者思路還是挺清晰的



1.3 使用案例

public class FileTest {
    public static void main(String[] args) throws Exception {

        File file1 = new File("C:\\Users\\Howl\\Desktop\\123.txt");
        File file2 = new File("C:\\Users\\Howl\\Desktop\\1234.txt");

        FileModifyManager manager = FileModifyManager.getInstance();

        manager.startWatch(file1,() -> System.out.println("123.txt 檔案改變了"))
                .startWatch(file2,() -> System.out.println("1234.txr 檔案改變了"));
        
    }
}






2. WatchService

WatchService 是利用本機作業系統的檔案系統來實現監控檔案目錄(監控目錄),於 JDK1.7 引入的位於 NIO 包下的新機制,所以使用方式和 NIO 也很相似


JDK 自帶的 watchService 的缺點是修改檔案會觸發兩次事件,因作業系統有不同情況:

  • 修改了檔案的 meta 資訊和日期
  • 寫時複製效果,即舊檔案改名,並將內容複製到新建的檔案裡

watchService 只能監控本目錄的內容,不能檢測子目錄裡的內容,如需監控則遍歷新增子目錄


public class WatchServiceTest {
    public static void main(String[] args) throws IOException, InterruptedException {

        // 目錄路徑,不能輸入檔案否則報錯
        Path path = Paths.get("C:\\Users\\Howl\\Desktop");

        // 獲取監聽服務
        WatchService watchService = FileSystems.getDefault().newWatchService();

        // 只註冊修改事件(還有建立和刪除)
        path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

        // 監聽
        while (true) {

            // 獲取監聽到的事件 key
            WatchKey watchKey = watchService.poll(3 * 1000, TimeUnit.MILLISECONDS);

            // poll 的返回有可能為 null
            if (watchKey == null) {
                continue;
            }

            // 遍歷這些事件
            for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
                if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    Path watchPath = (Path) watchEvent.context();
                    File watchFile = watchPath.toFile();
                    System.out.println(watchFile.toString() + "檔案修改了");
                }
            }

            // watchKey 復原,用於下次監聽
            watchKey.reset();
        }
    }
}






3. Hutool(推薦)

Hutool 是國人維護的工具集,使用別人的輪子,總比自己重複造輪子高效(但也要了解輪子的設計思路),hutool 底層還是使用 WatchService ,其解決了修改檔案會觸發兩次事件,思路是在某個毫秒數範圍內的修改視為同一個修改。還可以監控子目錄,思路是遞迴遍歷


3.1 新增依賴

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.19</version>
</dependency>

參考文件 Hutool



3.2 示例

public class HutoolTest {
    public static void main(String[] args) throws Exception {

        File file = new File("C:\\Users\\Howl\\Desktop\\123.txt");

        // 監聽檔案修改
        WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);

        // 設定鉤子函式
        watchMonitor.setWatcher(new SimpleWatcher() {
            @Override
            public void onModify(WatchEvent<?> event, Path currentPath) {
                System.out.println(((Path) event.context()).toFile().toString() + "修改了");
            }
        });

        // 設定監聽目錄的最大深入,目錄層級大於制定層級的變更將不被監聽,預設只監聽當前層級目錄
        watchMonitor.setMaxDepth(1);

        // 啟動監聽
        watchMonitor.start();
    }
}

思路是繼承 Thread 類,然後 run 方法一直迴圈監聽 watchService 事件







4. commons-io

commons-io 是 Apache 提供的實現 I/O 操作的工具集


4.1 新增依賴

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>


4.2 示例

稍微看了一下使用的是觀察者模式

public class CommonsTest {
    public static void main(String[] args) throws Exception {

        // 也是隻能寫目錄
        String filePath = "C:\\Users\\Howl\\Desktop";

        // 檔案觀察者
        FileAlterationObserver observer = new FileAlterationObserver(filePath);

        // 新增監聽
        observer.addListener(new FileAlterationListenerAdaptor() {
            @Override
            public void onFileChange(File file) {
                System.out.println(file.toString() + "檔案修改了");
            }
        });

        // 監視器
        FileAlterationMonitor monitor = new FileAlterationMonitor(10);

        // 新增觀察者
        monitor.addObserver(observer);

        // 啟動執行緒
        monitor.start();
    }
}


相關文章