Log4J2(七) - 觀察者模式-配置/腳本熱更新是怎麼實現的?-源碼分析

什麼情況下開啓配置/腳本熱更新?

當monitorInterval屬性的值不爲null,並且配置文件是存在的時候,Log4J2也有一套機制來實現對配置文件的熱更新,簡單說也就是當文件被改變的時候,Log4j2會動態的加載最新的配置。

以XmlConfiguration爲例:

	//省略部分解析配置的代碼
	if ("monitorInterval".equalsIgnoreCase(key)) {
       final int intervalSeconds = Integer.parseInt(value);
       if (intervalSeconds > 0) {
       		//獲取WatchManager, 並設置配置監控間隔
           getWatchManager().setIntervalSeconds(intervalSeconds);
           //如果當前配置文件不爲null,則創建配置文件觀察者
           if (configFile != null) {
               final FileWatcher watcher = new ConfiguratonFileWatcher(this, listeners);
               //添加文件監控
               getWatchManager().watchFile(configFile, watcher);
           }
      	}
   	}

關鍵類與接口

WatchManager

  • 作用:

    • 負責管理要監控的文件與文件監控器
    • 定時掃描要監控的文件,並通過FileMonitor判斷文件是否被修改
  • 監控管理

	private final ConcurrentMap<File, FileMonitor> watchers = new ConcurrentHashMap<>();

    public void watchFile(final File file, final FileWatcher watcher) {
        watchers.put(file, new FileMonitor(file.lastModified(), watcher));

    }
  • 定時監控
    public void start() {
    	//設置狀態爲已啓動
        super.start();
        //intervalSeconds即monitorInterval的值
        if (intervalSeconds > 0) {
        	//啓動定時器
            future = scheduler.scheduleWithFixedDelay(new WatchRunnable(), intervalSeconds, intervalSeconds,
                    TimeUnit.SECONDS);
        }
    }

	private class WatchRunnable implements Runnable {

        @Override
        public void run() {
        	//遍歷要監控的文件列表
            for (final Map.Entry<File, FileMonitor> entry : watchers.entrySet()) {
                final File file = entry.getKey();
                final FileMonitor fileMonitor = entry.getValue();
                //獲取文件最新的更改時間
                final long lastModfied = file.lastModified();
                //判斷文件是否修改
                if (fileModified(fileMonitor, lastModfied)) {
                    logger.info("File {} was modified on {}, previous modification was {}", file, lastModfied, fileMonitor.lastModifiedMillis);
                    fileMonitor.lastModifiedMillis = lastModfied;		//通過fileWatcher文件已被需改
                    fileMonitor.fileWatcher.fileModified(file);
                }
            }
        }
		
		//文件最近修改時間如果大於上次修改時間,則認定文件被修改
        private boolean fileModified(final FileMonitor fileMonitor, final long lastModifiedMillis) {
            return lastModifiedMillis != fileMonitor.lastModifiedMillis;
        }
    }

FileMonitor

  • 作用:存儲文件上次修改的時間,與文件對應的FileWatcher。當文件被判斷修改的時候,通知FileWatcher的實現類進行相應的操作。
  • 代碼
private class FileMonitor {
        private final FileWatcher fileWatcher;
        private long lastModifiedMillis;

        public FileMonitor(final long lastModifiedMillis, final FileWatcher fileWatcher) {
            this.fileWatcher = fileWatcher;
            this.lastModifiedMillis = lastModifiedMillis;
        }

        @Override
        public String toString() {
            return "FileMonitor [fileWatcher=" + fileWatcher + ", lastModifiedMillis=" + lastModifiedMillis + "]";
        }
    }

FileWatcher(接口)

  • 作用:文件被修改之後, 間接或者直接的執行被修改之後的動作, 譬如重新解析配置文件,然後更新配置。

實現類0:ScriptManager

  • 作用:腳本管理器
  • 我們看看fileModified的方法實現。

    @Override
    public void fileModified(final File file) {
    	//根據文件名獲取腳本的執行器
        final ScriptRunner runner = scriptRunners.get(file.toString());
        if (runner == null) {
            logger.info("{} is not a running script");
            return;
        }
        //獲取執行引擎
        final ScriptEngine engine = runner.getScriptEngine();
        //獲取腳本信息,如語言,內容,名字
        final AbstractScript script = runner.getScript();
        //根據是否有KEY_THREADING參數會有不同的運行機制
        //將更新的腳本放入到Map中,等待被執行
        if (engine.getFactory().getParameter(KEY_THREADING) == null) {		
            scriptRunners.put(script.getName(), new ThreadLocalScriptRunner(script));
        } else {
            scriptRunners.put(script.getName(), new MainScriptRunner(engine, script));
        }

    }

ps: 腳本的作用及用法可以自行看看官方解釋 - scripts 部分。

實現類1:ConfiguratonFileWatcher

  • 作用:log的配置文件觀察者。
  • 同樣,我們來看看其fileModified方法:
    public void fileModified(final File file) {
    	//遍歷watcher中的所有監聽器,並啓動相應的線程來通知監聽器執行動作,其實這裏可以理解成有ConfiguratonFileWatcher是對多個FileWtacher的一個包裝
        for (final ConfigurationListener configurationListener : configurationListeners) {
            final Thread thread = threadFactory.newThread(new ReconfigurationRunnable(configurationListener, reconfigurable));
            thread.start();
        }
    }
  • 上面線程要運行的內容,其實就是調用listener的onChange方法
    ReconfigurationRunnable
    private static class ReconfigurationRunnable implements Runnable {

        private final ConfigurationListener configurationListener;
        private final Reconfigurable reconfigurable;

        public ReconfigurationRunnable(final ConfigurationListener configurationListener, final Reconfigurable reconfigurable) {
            this.configurationListener = configurationListener;
            this.reconfigurable = reconfigurable;
        }

        @Override
        public void run() {
            configurationListener.onChange(reconfigurable);
        }
    }
  • ConfigurationListener:目前在Log4J2中只有一個實現,就是LoggerContext,我們來看看onChange方法:
    public synchronized void onChange(final Reconfigurable reconfigurable) {
        LOGGER.debug("Reconfiguration started for context {} ({})", contextName, this);
        //通過reconfigurable獲取最新的配置
        final Configuration newConfig = reconfigurable.reconfigure();
        if (newConfig != null) {
        	//更新LoggerContext的配置,也就是整個日誌系統的配置
            setConfiguration(newConfig);
            LOGGER.debug("Reconfiguration completed for {} ({})", contextName, this);
        } else {
            LOGGER.debug("Reconfiguration failed for {} ({})", contextName, this);
        }
    }

  • Reconfigurable: 實現對配置文件的重新配置
    • 實現類有以下5種,其實也就是配置文件的解析類
      Reconfigurable的實現類
    • 以XmlConfiguration爲例,我們看看看它是怎麼實現Reconfigurable接口的
    public Configuration reconfigure() {
        try {
        	//將配置源文件轉化爲輸入流
            final ConfigurationSource source = getConfigurationSource().resetInputStream();
            if (source == null) {
                return null;
            }
            //重新生成XmlConfiguration
            final XmlConfiguration config = new XmlConfiguration(getLoggerContext(), source);
            //判斷新生成的配置信息是否合法:簡單驗證一下是否有root節點
            return config.rootElement == null ? null : config;
        } catch (final IOException ex) {
            LOGGER.error("Cannot locate file {}", getConfigurationSource(), ex);
        }
        return null;
    }

小結

學習這套監控機制還是挺受益的,可以聯想到其他的很多監控場景,譬如說服務掛掉之後自動重啓等等,相似的套路。區別在於,要監控的是進程的狀態(目標),當進程不存在的時候(事件),就重新執行進程的啓動命令(事件對應的動作)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章