Java能夠如何實現文件變更的監聽

Java能夠如何實現文件變更的監聽

應用中使用logback做爲日誌輸出組件的話,大部分會去配置 logback.xml 這個文件,並且生產環境下,直接去修改logback.xml文件中的日誌級別,不用重啓應用就能夠生效java

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

I. 問題描述及分析

針對上面的這個問題,首先拋出一個實際的case,在個人我的網站 Z+中,全部的小工具都是經過配置文件來動態新增和隱藏的,由於只有一臺服務器,因此配置文件就簡化的直接放在了服務器的某個目錄下github

如今的問題時,我須要在這個文件的內容發生變更時,應用能夠感知這種變更,並從新加載文件內容,更新應用內部緩存web

一個最容易想到的方法,就是輪詢,判斷文件是否發生修改,若是修改了,則從新加載,並刷新內存,因此主要須要關心的問題以下:apache

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

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();
        }
    }
}

上面這個屬於一個很是簡單,很是基礎的實現了,基本上也能夠知足咱們的需求,那麼這個實現有什麼問題呢?app

定時任務的執行中,若是出現了異常會怎樣?異步

對上面的代碼稍做修改maven

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

相關文章
相關標籤/搜索