應用中使用logback作為日誌輸出組件的話,大部分會去配置`logback.xml` 這個文件,而且生產環境下,直接去修改logback.xml文件中的日誌級別,不用重啟應用就可以生效那麼,這個功能是怎麼實現的呢?
應用中使用logback作為日誌輸出組件的話,大部分會去配置logback.xml 這個文件,而且生產環境下,直接去修改logback.xml文件中的日誌級別,不用重啟應用就可以生效
那麼,這個功能是怎麼實現的呢?
I. 問題描述及分析
針對上面的這個問題,首先拋出一個實際的case,在我的個人網站Z+中,所有的小工具都是通過配置文件來動態新增和隱藏的,因為只有一台服務器,所以配置文件就簡化的直接放在了服務器的某個目錄下
現在的問題時,我需要在這個文件的內容髮生變動時,應用可以感知這種變動,並重新加載文件內容,更新應用內部緩存
一個最容易想到的方法,就是輪詢,判斷文件是否發生修改,如果修改了,則重新加載,並刷新內存,所以主要需要關心的問題如下:
II. 設計與實現
問題抽像出來之後,對應的解決方案就比較清晰了
那麼一個很簡單的實現就比較容易了:
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); } }}針對上面的實現,簡單說明幾點:
一個問題,如果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
@Testpublic 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來實現配置文件變動的監聽,主要涉及到的就是兩個點
整體來說,這個實現還是比較簡單的,無論是自定義實現,還是依賴commos-io來做,都沒太大的技術成本,但是需要注意的一點是:
為了避免上面這個情況,一個可以做的實現是藉助EventBus的異步消息通知來實現,當文件變動之後,發送一個消息即可,然後在具體的重新加載文件內容的方法上,添加一個@Subscribe註解即可,這樣既實現了解耦,也避免了異常導致的服務異常(如果對這個實現有興趣的可以評論說明)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。