首頁 > 軟體

Java實現監聽檔案變化的三種方案詳解

2022-05-30 14:02:00

背景

在研究規則引擎時,如果規則以檔案的形式儲存,那麼就需要監聽指定的目錄或檔案來感知規則是否變化,進而進行載入。當然,在其他業務場景下,比如想實現組態檔的動態載入、紀錄檔檔案的監聽、FTP檔案變動監聽等都會遇到類似的場景。

本文給大家提供三種解決方案,並分析其中的利弊,建議收藏,以備不時之需。

方案一:定時任務 + File#lastModified

這個方案是最簡單,最能直接想到的解決方案。通過定時任務,輪訓查詢檔案的最後修改時間,與上一次進行對比。如果發生變化,則說明檔案已經修改,進行重新載入或對應的業務邏輯處理。

在上篇文章《JDK的一個Bug,監聽檔案變更要小心了》中已經編寫了具體的範例,並且也提出了其中的不足。

這裡再把範例程式碼貼出來:

public class FileWatchDemo {
​
 /**
  * 上次更新時間
  */
 public static long LAST_TIME = 0L;
​
 public static void main(String[] args) throws IOException {
​
  String fileName = "/Users/zzs/temp/1.txt";
  // 建立檔案,僅為範例,實踐中由其他程式觸發檔案的變更
  createFile(fileName);
​
  // 執行2次
  for (int i = 0; i < 2; i++) {
   long timestamp = readLastModified(fileName);
   if (timestamp != LAST_TIME) {
    System.out.println("檔案已被更新:" + timestamp);
    LAST_TIME = timestamp;
    // 重新載入,檔案內容
   } else {
    System.out.println("檔案未更新");
   }
  }
 }
​
 public static void createFile(String fileName) throws IOException {
  File file = new File(fileName);
  if (!file.exists()) {
   boolean result = file.createNewFile();
   System.out.println("建立檔案:" + result);
  }
 }
​
 public static long readLastModified(String fileName) {
  File file = new File(fileName);
  return file.lastModified();
 }
}

對於檔案低頻變動的場景,這種方案實現簡單,基本上可以滿足需求。不過像上篇文章中提到的那樣,需要注意Java 8和Java 9中File#lastModified的Bug問題。

但該方案如果用在檔案目錄的變化上,缺點就有些明顯了,比如:操作頻繁,效率都損耗在遍歷、儲存狀態、對比狀態上了,無法充分利用OS的功能。

方案二:WatchService

在Java 7中新增了java.nio.file.WatchService,通過它可以實現檔案變動的監聽。WatchService是基於作業系統的檔案系統監控器,可以監控系統所有檔案的變化,無需遍歷、無需比較,是一種基於訊號收發的監控,效率高。

public class WatchServiceDemo {
  public static void main(String[] args) throws IOException {
    // 這裡的監聽必須是目錄
    Path path = Paths.get("/Users/zzs/temp/");
    // 建立WatchService,它是對作業系統的檔案監視器的封裝,相對之前,不需要遍歷檔案目錄,效率要高很多
    WatchService watcher = FileSystems.getDefault().newWatchService();
    // 註冊指定目錄使用的監聽器,監視目錄下檔案的變化;
    // PS:Path必須是目錄,不能是檔案;
    // StandardWatchEventKinds.ENTRY_MODIFY,表示監視檔案的修改事件
    path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
​
    // 建立一個執行緒,等待目錄下的檔案發生變化
    try {
      while (true) {
        // 獲取目錄的變化:
        // take()是一個阻塞方法,會等待監視器發出的訊號才返回。
        // 還可以使用watcher.poll()方法,非阻塞方法,會立即返回當時監視器中是否有訊號。
        // 返回結果WatchKey,是一個單例物件,與前面的register方法返回的範例是同一個;
        WatchKey key = watcher.take();
        // 處理檔案變化事件:
        // key.pollEvents()用於獲取檔案變化事件,只能獲取一次,不能重複獲取,類似佇列的形式。
        for (WatchEvent<?> event : key.pollEvents()) {
          // event.kind():事件型別
          if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
            //事件可能lost or discarded
            continue;
          }
          // 返回觸發事件的檔案或目錄的路徑(相對路徑)
          Path fileName = (Path) event.context();
          System.out.println("檔案更新: " + fileName);
        }
        // 每次呼叫WatchService的take()或poll()方法時需要通過本方法重置
        if (!key.reset()) {
          break;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
複製程式碼

上述demo展示了WatchService的基本使用方式,註解部分也說明了每個API的具體作用。

通過WatchService監聽檔案的型別也變得更加豐富:

  • ENTRY_CREATE 目標被建立
  • ENTRY_DELETE 目標被刪除
  • ENTRY_MODIFY 目標被修改
  • OVERFLOW 一個特殊的Event,表示Event被放棄或者丟失

如果檢視WatchService實現類(PollingWatchService)的原始碼,會發現,本質上就是開啟了一個獨立的執行緒來監控檔案的變化:

PollingWatchService() {
        // TBD: Make the number of threads configurable
        scheduledExecutor = Executors
            .newSingleThreadScheduledExecutor(new ThreadFactory() {
                 @Override
                 public Thread newThread(Runnable r) {
                     Thread t = new Thread(null, r, "FileSystemWatcher", 0, false);
                     t.setDaemon(true);
                     return t;
                 }});
    }

也就是說,本來需要我們手動實現的部分,也由WatchService內部幫我們完成了。

如果你編寫一個demo,進行驗證時,會很明顯的感覺到WatchService監控檔案的變化並不是實時的,有時候要等幾秒才監聽到檔案的變化。以實現類PollingWatchService為例,檢視原始碼,可以看到如下程式碼:

void enable(Set<? extends Kind<?>> var1, long var2) {
            synchronized(this) {
                this.events = var1;
                Runnable var5 = new Runnable() {
                    public void run() {
                        PollingWatchKey.this.poll();
                    }
                };
                this.poller = PollingWatchService.this.scheduledExecutor.scheduleAtFixedRate(var5, var2, var2, TimeUnit.SECONDS);
            }
        }

也就是說監聽器由按照固定時間間隔的排程器來控制的,而這個時間間隔在SensitivityWatchEventModifier類中定義:

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

該類提供了3個級別的時間間隔,分別為2秒、10秒、30秒,預設值為10秒。這個時間間隔可以在path#register時進行傳遞:

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

相對於方案一,實現起來簡單,效率高。不足的地方也很明顯,只能監聽當前目錄下的檔案和目錄,不能監視子目錄,而且我們也看到監聽只能算是準實時的,而且監聽時間只能取API預設提供的三個值。

該API在Stack Overflow上也有人提出Java 7在Mac OS下有延遲的問題,甚至涉及到Windows和Linux系統,筆者沒有進行其他作業系統的驗證,如果你遇到類似的問題,可參考對應的文章,尋求解決方案:https://www.jb51.net/article/249820.htm

方案三:Apache Commons-IO

方案一我們自己來實現,方案二藉助於JDK的API來實現,方案三便是藉助於開源的框架來實現,這就是幾乎每個專案都會引入的commons-io類庫。

引入相應依賴:

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

注意,不同的版本需要不同的JDK支援,2.7需要Java 8及以上版本。

commons-io對實現檔案監聽的實現位於org.apache.commons.io.monitor包下,基本使用流程如下:

  • 自定義檔案監聽類並繼承 FileAlterationListenerAdaptor 實現對檔案與目錄的建立、修改、刪除事件的處理;
  • 自定義檔案監控類,通過指定目錄建立一個觀察者 FileAlterationObserver
  • 向監視器新增檔案系統觀察器,並新增檔案監聽器;
  • 呼叫並執行。

第一步:建立檔案監聽器。根據需要在不同的方法內實現對應的業務邏輯處理。

public class FileListener extends FileAlterationListenerAdaptor {
​
  @Override
  public void onStart(FileAlterationObserver observer) {
    super.onStart(observer);
    System.out.println("onStart");
  }
​
  @Override
  public void onDirectoryCreate(File directory) {
    System.out.println("新建:" + directory.getAbsolutePath());
  }
​
  @Override
  public void onDirectoryChange(File directory) {
    System.out.println("修改:" + directory.getAbsolutePath());
  }
​
  @Override
  public void onDirectoryDelete(File directory) {
    System.out.println("刪除:" + directory.getAbsolutePath());
  }
​
  @Override
  public void onFileCreate(File file) {
    String compressedPath = file.getAbsolutePath();
    System.out.println("新建:" + compressedPath);
    if (file.canRead()) {
      // TODO 讀取或重新載入檔案內容
      System.out.println("檔案變更,進行處理");
    }
  }
​
  @Override
  public void onFileChange(File file) {
    String compressedPath = file.getAbsolutePath();
    System.out.println("修改:" + compressedPath);
  }
​
  @Override
  public void onFileDelete(File file) {
    System.out.println("刪除:" + file.getAbsolutePath());
  }
​
  @Override
  public void onStop(FileAlterationObserver observer) {
    super.onStop(observer);
    System.out.println("onStop");
  }
}

第二步:封裝一個檔案監控的工具類,核心就是建立一個觀察者FileAlterationObserver,將檔案路徑Path和監聽器FileAlterationListener進行封裝,然後交給FileAlterationMonitor。

public class FileMonitor {
  private FileAlterationMonitor monitor;
​
  public FileMonitor(long interval) {
    monitor = new FileAlterationMonitor(interval);
  }
  /**
   * 給檔案新增監聽
   *
   * @param path     檔案路徑
   * @param listener 檔案監聽器
   */
  public void monitor(String path, FileAlterationListener listener) {
    FileAlterationObserver observer = new FileAlterationObserver(new File(path));
    monitor.addObserver(observer);
    observer.addListener(listener);
  }
​
  public void stop() throws Exception {
    monitor.stop();
  }
​
  public void start() throws Exception {
    monitor.start();
​
  }
}

第三步:呼叫並執行:

public class FileRunner {
​
  public static void main(String[] args) throws Exception {
    FileMonitor fileMonitor = new FileMonitor(1000);
    fileMonitor.monitor("/Users/zzs/temp/", new FileListener());
    fileMonitor.start();
  }
}

執行程式,會發現每隔1秒輸入一次紀錄檔。當檔案發生變更時,也會列印出對應的紀錄檔:

onStart
修改:/Users/zzs/temp/1.txt
onStop
onStart
onStop

當然,對應的監聽時間間隔,可以通過在建立FileMonitor時進行修改。

該方案中監聽器本身會啟動一個執行緒定時處理。在每次執行時,都會先呼叫事件監聽處理類的onStart方法,然後檢查是否有變動,並呼叫對應事件的方法;比如,onChange檔案內容改變,檢查完後,再呼叫onStop方法,釋放當前執行緒佔用的CPU資源,等待下次間隔時間到了被再次喚醒執行。

監聽器是基於檔案目錄為根源的,也可以可以設定過濾器,來實現對應檔案變動的監聽。過濾器的設定可檢視FileAlterationObserver的構造方法:

public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
    this(new File(directoryName), fileFilter, caseSensitivity);
}

小結

至此,基於Java實現監聽檔案變化的三種方案便介紹完畢。經過上述分析及範例,大家已經看到,並沒有完美的解決方案,根據自己的業務情況及系統的容忍度可選擇最適合的方案。而且,在此基礎上可以新增一些其他的輔助措施,來避免具體方案中的不足之處。

到此這篇關於Java實現監聽檔案變化的三種方法的文章就介紹到這了,更多相關Java監聽檔案變化內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com