Java可以如何實現檔案變動的監聽

一灰灰發表於2018-02-09

Java可以如何實現檔案變動的監聽

應用中使用logback作為日誌輸出元件的話,大部分會去配置 logback.xml 這個檔案,而且生產環境下,直接去修改logback.xml檔案中的日誌級別,不用重啟應用就可以生效

那麼,這個功能是怎麼實現的呢?

I. 問題描述及分析

針對上面的這個問題,首先丟擲一個實際的case,在我的個人網站 Z+中,所有的小工具都是通過配置檔案來動態新增和隱藏的,因為只有一臺伺服器,所以配置檔案就簡化的直接放在了伺服器的某個目錄下

現在的問題時,我需要在這個檔案的內容發生變動時,應用可以感知這種變動,並重新載入檔案內容,更新應用內部快取

一個最容易想到的方法,就是輪詢,判斷檔案是否發生修改,如果修改了,則重新載入,並重新整理記憶體,所以主要需要關心的問題如下:

  • 如何輪詢?
  • 如何判斷檔案是否修改?
  • 配置異常,會不會導致服務不可用?(即容錯,這個與本次主題關聯不大,但又比較重要...)

II. 設計與實現

問題抽象出來之後,對應的解決方案就比較清晰了

  • 如何輪詢 ? --》 定時器 Timer, ScheduledExecutorService 都可以實現
  • 如何判斷檔案修改? --》根據 java.io.File#lastModified 獲取檔案的上次修改時間,比對即可

那麼一個很簡單的實現就比較容易了:

public class FileUpTest {

    private long lastTime;

    @Test
    public void testFileUpdate() {
        File file = new File("/tmp/alarmConfig");

        // 首先檔案的最近一次修改時間戳
        lastTime = file.lastModified();

        // 定時任務,每秒來判斷一下檔案是否發生變動,即判斷lastModified是否改變
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                if (file.lastModified() > lastTime) {
                    System.out.println("file update! time : " + file.lastModified());
                    lastTime = file.lastModified();
                }
            }
        },0, 1, TimeUnit.SECONDS);


        try {
            Thread.sleep(1000 * 60);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

上面這個屬於一個非常簡單,非常基礎的實現了,基本上也可以滿足我們的需求,那麼這個實現有什麼問題呢?

定時任務的執行中,如果出現了異常會怎樣?

對上面的程式碼稍作修改

public class FileUpTest {

    private long lastTime;

    private void ttt() {
        throw new NullPointerException();
    }

    @Test
    public void testFileUpdate() {
        File file = new File("/tmp/alarmConfig");

        lastTime = file.lastModified();

        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                if (file.lastModified() > lastTime) {
                    System.out.println("file update! time : " + file.lastModified());
                    lastTime = file.lastModified();
                    ttt();
                }
            }
        }, 0, 1, TimeUnit.SECONDS);


        try {
            Thread.sleep(1000 * 60 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

實際測試,發現只有首次修改的時候,觸發了上面的程式碼,但是再次修改則沒有效果了,即當丟擲異常之後,定時任務將不再繼續執行了,這個問題的主要原因是因為 ScheduledExecutorService 的原因了

直接檢視ScheduledExecutorService的原始碼註釋說明

If any execution of the task encounters an exception, subsequent executions are suppressed.Otherwise, the task will only terminate via cancellation or termination of the executor. 即如果定時任務執行過程中遇到發生異常,則後面的任務將不再執行。

所以,使用這種姿勢的時候,得確保自己的任務不會丟擲異常,否則後面就沒法玩了

對應的解決方法也比較簡單,整個catch一下就好

III. 進階版

前面是一個基礎的實現版本了,當然在java圈,基本上很多常見的需求,都是可以找到對應的開源工具來使用的,當然這個也不例外,而且應該還是大家比較屬性的apache系列

首先maven依賴

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
複製程式碼

主要是藉助這個工具中的 FileAlterationObserver, FileAlterationListener, FileAlterationMonitor 三個類來實現相關的需求場景了,當然使用也算是很簡單了,以至於都不太清楚可以再怎麼去說明了,直接看下面從我的一個開源專案quick-alarm中拷貝出來的程式碼

public class PropertiesConfListenerHelper {

    public static boolean registerConfChangeListener(File file, Function<File, Map<String, AlarmConfig>> func) {
        try {
            // 輪詢間隔 5 秒
            long interval = TimeUnit.SECONDS.toMillis(5);


            // 因為監聽是以目錄為單位進行的,所以這裡直接獲取檔案的根目錄
            File dir = file.getParentFile();

            // 建立一個檔案觀察器用於過濾
            FileAlterationObserver observer = new FileAlterationObserver(dir,
                    FileFilterUtils.and(FileFilterUtils.fileFileFilter(),
                            FileFilterUtils.nameFileFilter(file.getName())));

            //設定檔案變化監聽器
            observer.addListener(new MyFileListener(func));
            FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
            monitor.start();

            return true;
        } catch (Exception e) {
            log.error("register properties change listener error! e:{}", e);
            return false;
        }
    }


    static final class MyFileListener extends FileAlterationListenerAdaptor {

        private Function<File, Map<String, AlarmConfig>> func;

        public MyFileListener(Function<File, Map<String, AlarmConfig>> func) {
            this.func = func;
        }

        @Override
        public void onFileChange(File file) {
            Map<String, AlarmConfig> ans = func.apply(file); // 如果載入失敗,列印一條日誌
            log.warn("PropertiesConfig changed! reload ans: {}", ans);
        }
    }
}
複製程式碼

針對上面的實現,簡單說明幾點:

  • 這個檔案監聽,是以目錄為根源,然後可以設定過濾器,來實現對應檔案變動的監聽
  • 如上面registerConfChangeListener方法,傳入的file是具體的配置檔案,因此構建引數的時候,撈出了目錄,撈出了檔名作為過濾
  • 第二引數是jdk8語法,其中為具體的讀取配置檔案內容,並對映為對應的實體物件

一個問題,如果 func方法執行時,也丟擲了異常,會怎樣?

實際測試表現結果和上面一樣,丟擲異常之後,依然跪,所以依然得注意,不要跑異常

那麼簡單來看一下上面的實現邏輯,直接扣出核心模組

public void run() {
    while(true) {
        if(this.running) {
            Iterator var1 = this.observers.iterator();

            while(var1.hasNext()) {
                FileAlterationObserver observer = (FileAlterationObserver)var1.next();
                observer.checkAndNotify();
            }

            if(this.running) {
                try {
                    Thread.sleep(this.interval);
                } catch (InterruptedException var3) {
                    ;
                }
                continue;
            }
        }

        return;
    }
}
複製程式碼

從上面基本上一目瞭然,整個的實現邏輯了,和我們的第一種定時任務的方法不太一樣,這兒直接使用執行緒,死迴圈,內部採用sleep的方式來來暫停,因此出現異常時,相當於直接丟擲去了,這個執行緒就跪了


JDK版本

jdk1.7,提供了一個WatchService,也可以用來實現檔案變動的監聽,之前也沒有接觸過,看到說明,然後搜了一下使用相關,發現也挺簡單的,同樣給出一個簡單的示例demo

@Test
public void testFileUpWather() throws IOException {
    // 說明,這裡的監聽也必須是目錄
    Path path = Paths.get("/tmp");
    WatchService watcher = FileSystems.getDefault().newWatchService();
    path.register(watcher, ENTRY_MODIFY);

    new Thread(() -> {
        try {
            while (true) {
                WatchKey key = watcher.take();
                for (WatchEvent<?> event : key.pollEvents()) {
                    if (event.kind() == OVERFLOW) {
                        //事件可能lost or discarded 
                        continue;
                    }
                    Path fileName = (Path) event.context();
                    System.out.println("檔案更新: " + fileName);
                }
                if (!key.reset()) { // 重設WatchKey
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();


    try {
        Thread.sleep(1000 * 60 * 10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
複製程式碼

IV. 小結

使用Java來實現配置檔案變動的監聽,主要涉及到的就是兩個點

  • 如何輪詢: 定時器(Timer, ScheduledExecutorService), 執行緒死迴圈+sleep
  • 檔案修改: File#lastModified

整體來說,這個實現還是比較簡單的,無論是自定義實現,還是依賴 commos-io來做,都沒太大的技術成本,但是需要注意的一點是:

  • 千萬不要在定時任務 or 檔案變動的回撥方法中丟擲異常!!!

為了避免上面這個情況,一個可以做的實現是藉助EventBus的非同步訊息通知來實現,當檔案變動之後,傳送一個訊息即可,然後在具體的重新載入檔案內容的方法上,新增一個 @Subscribe註解即可,這樣既實現瞭解耦,也避免了異常導致的服務異常 (如果對這個實現有興趣的可以評論說明)

V. 其他

參考專案

宣告

盡信書則不如,已上內容,純屬一家之言,因本人能力一般,見解不全,如有問題,歡迎批評指正

掃描關注,java分享

QrCode

相關文章