Log4J2(七) - 观察者模式-配置/脚本热更新是怎么实现的?-源码分析

x33g5p2x  于2021-12-25 转载在 其他  
字(5.0k)|赞(0)|评价(0)|浏览(594)

什么情况下开启配置/脚本热更新?

当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种,其实也就是配置文件的解析类

  • 以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;
    }

小结

学习这套监控机制还是挺受益的,可以联想到其他的很多监控场景,譬如说服务挂掉之后自动重启等等,相似的套路。区别在于,要监控的是进程的状态(目标),当进程不存在的时候(事件),就重新执行进程的启动命令(事件对应的动作)。

相关文章