应用场景与问题
当项目在运行时,我们如果需要修改log4j 1.X或者log4j2的配置文件,一般来说我们是不能直接将项目停止运行再来修改文件重新部署的。于是就有这样一个问题:如何在不停止当前项目的运行的情况下,让系统能够自动地监控配置文件的修改状况,从而实现动态加载配置文件的功能?而log4j 1.X和log4j2的差别略大,各自应该怎么实现这个功能?
log4j 1.X怎么动态加载配置文件
log4j 1.X提供了动态加载配置文件的方法:
DOMConfigurator#configureAndWatch()
PropertyConfigurator#configure()
DOMConfigurator
对应的是xml配置文件,PropertyConfigurator
对应的是properties配置文件。这两个类都有configureAndWatch
这个方法,该方法有个重载方法,如下:
configureAndWatch(String configFilename)
configureAndWatch(String configFilename, long delay)
configureAndWatch
方法用来监控配置文件是否被改动,监控的时间间隔是delay
参数来决定,如果不传入该参数则使用默认的时间间隔1分钟(60000L)。configureAndWatch(String configFilename)
实际上还是调用的configureAndWatch(String configFilename, long delay)
。
DOMConfigurator#configureAndWatch源码解析
org.apache.log4j.xml.DOMConfigurator#configureAndWatch源码如下:
static public void configureAndWatch(String configFilename, long delay) {
XMLWatchdog xdog = new XMLWatchdog(configFilename);
xdog.setDelay(delay);
xdog.start();
}
这里new了一个XMLWatchdog对象,接着设置了delay参数,最后调用了start()方法。
watchdog是看门狗、检查者的意思,XMLWatchdog继承了FileWatchdog这个类,在XMLWatchdog中仅仅重写了doOnChange方法:
public void doOnChange() {
new DOMConfigurator().doConfigure(filename, LogManager.getLoggerRepository());
}
从方法名就可以看出来,如果XMLWatchdog监控到配置文件被改动了,就会调用这个doOnChange方法,用来重新加载配置文件。那么它又是怎么知道配置文件被改动过了呢?接着看其父类FileWatchdog的源码:
public abstract class FileWatchdog extends Thread {
/**
The default delay between every file modification check, set to 60
seconds. */
static final public long DEFAULT_DELAY = 60000;
/**
The name of the file to observe for changes.
*/
protected String filename;
/**
The delay to observe between every check. By default set {@link
#DEFAULT_DELAY}. */
protected long delay = DEFAULT_DELAY;
File file;
long lastModif = 0;
boolean warnedAlready = false;
boolean interrupted = false;
protected FileWatchdog(String filename) {
super("FileWatchdog");
this.filename = filename;
file = new File(filename);
setDaemon(true);
checkAndConfigure();
}
/**
Set the delay to observe between each check of the file changes.
*/
public void setDelay(long delay) {
this.delay = delay;
}
abstract protected void doOnChange();
protected void checkAndConfigure() {
boolean fileExists;
try {
fileExists = file.exists();
} catch(SecurityException e) {
LogLog.warn("Was not allowed to read check file existance, file:["+
filename+"].");
interrupted = true; // there is no point in continuing
return;
}
if(fileExists) {
long l = file.lastModified(); // this can also throw a SecurityException
if(l > lastModif) { // however, if we reached this point this
lastModif = l; // is very unlikely.
doOnChange();
warnedAlready = false;
}
} else {
if(!warnedAlready) {
LogLog.debug("["+filename+"] does not exist.");
warnedAlready = true;
}
}
}
public void run() {
while(!interrupted) {
try {
Thread.sleep(delay);
} catch(InterruptedException e) {
// no interruption expected
}
checkAndConfigure();
}
}
}
可以看到,FileWatchdog继承了Thread类,类里定义了几个成员变量,比如默认的监控时间间隔等。而在该类的构造方法中可以看到,首先该线程类将名字设定成FileWatchdog
,接着根据传入的配置文件的路径new了一个File对象,然后该线程类又设置成了守护线程(daemon thread),最后调用了checkAndConfigure()
。
在checkAndConfigure()
中,则是对new出来的配置文件File对象进行检查是否存在该文件,若不存在该文件则会设置成员变量的值,这样就不会去监控不存在的配置文件了。如果该配置文件存在,则通过lastModified()
来获取文件的最后更新时间,和上次的更新时间作对比,如果比上次更新时间大则会调用doOnChange()
来重新加载配置文件。
而在FileWatchdog的run方法中,则是在无限循环中先让线程睡眠设置好的监控时间间隔,然后调用checkAndConfigure()
。
总结
可以看出,在log4j 1.X的DOMConfigurator中,是通过创建一个守护线程来不停地扫描配置文件的最后更新时间,并和上次的更新时间进行对比,如果最后更新时间大于上次更新时间则会重新加载配置文件。
PropertyConfigurator#configureAndWatch源码解析
PropertyConfigurator的configureAndWatch()
其实和DOMConfigurator差不多,区别是PropertyConfigurator在方法里new了一个PropertyWatchdog对象,PropertyWatchdog和XMLWatchdog一样继承了FileWatchdog,一样重写了doOnChange()方法。只是PropertyWatchdog是通过new PropertyConfigurator().doConfigure()来加载配置文件的。
从源码实现来看,无论是使用xml配置文件,还是使用properties配置文件,其动态加载配置文件的底层实现是基本一样的。可以通过解析配置文件的文件后缀来判断是xml还是properties文件,然后调用对应的方法即可,大概的思路如下:
boolean flag = true;
boolean isXml = StringUtils.equalsIgnoreCase("xml", StringUtils.substringAfterLast(filepath, "."));
ling delay = 30000;
if (isXml) {
if (flag) {
DOMConfigurator.configureAndWatch(filepath, delay);
} else {
DOMConfigurator.configure(filepath);
}
} else {
if (flag) {
PropertyConfigurator.configureAndWatch(filepath, delay);
} else {
PropertyConfigurator.configure(filepath);
}
}
log4j2怎么动态加载配置文件
和log4j 1.X比起来,log4j2的动态加载配置很简单就能实现,不需要另外在代码中调用api,方法如下:
<configuration monitorInterval="30">
...
</configuration>
在log4j2.xml配置文件中的configuration
节点添加monitorInterval
的值,单位是秒,如果配置的值大于0,则会按照时间间隔来自动扫描配置文件是否被修改,并在修改后重新加载最新的配置文件。如果不配置该值,默认为0,即不扫描配置文件是否被修改。
log4j2底层实现动态加载配置文件的简单解析
虽然log4j2的动态加载配置很简单,但其底层实现比起log4j 1.X却要复杂很多,使用到了很多并发包下的类,具体也不是很了解,这里简单解释下流程。
对于log4j2.xml文件,对应的是org.apache.logging.log4j.core.config.xml.XmlConfiguration
这个类。如果在log4j2.xml里配置了monitorInterval,在构建XmlConfiguration时会根据该值来走一段特定的逻辑:
for (final Map.Entry<String, String> entry : attrs.entrySet()) {
final String key = entry.getKey();
final String value = getStrSubstitutor().replace(entry.getValue());
if ("status".equalsIgnoreCase(key)) {
statusConfig.withStatus(value);
} else if ("dest".equalsIgnoreCase(key)) {
statusConfig.withDestination(value);
} else if ("shutdownHook".equalsIgnoreCase(key)) {
isShutdownHookEnabled = !"disable".equalsIgnoreCase(value);
} else if ("shutdownTimeout".equalsIgnoreCase(key)) {
shutdownTimeoutMillis = Long.parseLong(value);
} else if ("verbose".equalsIgnoreCase(key)) {
statusConfig.withVerbosity(value);
} else if ("packages".equalsIgnoreCase(key)) {
pluginPackages.addAll(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
} else if ("name".equalsIgnoreCase(key)) {
setName(value);
} else if ("strict".equalsIgnoreCase(key)) {
strict = Boolean.parseBoolean(value);
} else if ("schema".equalsIgnoreCase(key)) {
schemaResource = value;
} else if ("monitorInterval".equalsIgnoreCase(key)) {
final int intervalSeconds = Integer.parseInt(value);
if (intervalSeconds > 0) {
getWatchManager().setIntervalSeconds(intervalSeconds);
if (configFile != null) {
final FileWatcher watcher = new ConfiguratonFileWatcher(this, listeners);
getWatchManager().watchFile(configFile, watcher);
}
}
} else if ("advertiser".equalsIgnoreCase(key)) {
createAdvertiser(value, configSource, buffer, "text/xml");
}
}
可以看到,如果monitorInterval的值大于0,则会拿到WatchManager并设置扫描配置文件的时间间隔,如果配置文件存在,则会new一个ConfiguratonFileWatcher对象,并将配置文件和该对象一起传递给WatchManager的watchFile方法。这两个方法的底层实现很绕,比起log4j 1.X要复杂得多,不容易看懂。不过最终实现的效果还是一样的,依然会开启一个守护线程来监控配置文件是否被改动。
区别在于,log4j2使用线程池来启动线程,在WatchManager#start()
里实现的:
@Override
public void start() {
super.start();
if (intervalSeconds > 0) {
future = scheduler.scheduleWithFixedDelay(new WatchRunnable(), intervalSeconds, intervalSeconds,
TimeUnit.SECONDS);
}
}
而该方法则是在启动配置文件时被调用的,AbstractConfiguration#start()
:
/**
* Start the configuration.
*/
@Override
public void start() {
// Preserve the prior behavior of initializing during start if not initialized.
if (getState().equals(State.INITIALIZING)) {
initialize();
}
LOGGER.debug("Starting configuration {}", this);
this.setStarting();
if (watchManager.getIntervalSeconds() > 0) {
watchManager.start();
}
...
}
这里只是简单解析了下主要的流程,具体的实现细节目前还看不太懂,有兴趣的可以自己去看看log4j2的源码。另外我在其他文章里看到有人说monitorInterval
的最小值是5,但是在源码里也没看到这个,只要配置值大于0应该就是可以的。有不对之处,欢迎指出。