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. 其他
參考專案
- 專案: quick-alarm
- 測試類: FileUpTest.java
宣告
盡信書則不如,已上內容,純屬一家之言,因本人能力一般,見解不全,如有問題,歡迎批評指正