1. 程式人生 > 其它 >Java可以如何實現檔案變動的監聽

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

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