應用場景與問題
當項目在運行時,我們如果需要修改log4j 1.X或者log4j2的配置文件,一般來說我們是不能直接將項目停止運行再來修改文件重新部署的。於是就有這樣一個問題:如何在不停止當前項目的運行的情況下,讓系統能夠自動地監控配置文件的修改狀況,從而實現動態加載配置文件的功能?而log4j 1.X和log4j2的差別略大,各自應該怎么實現這個功能?
log4j 1.X動態加載配置文件
log4j 1.X提供了動態加載配置文件的方法:
DOMConfigurator.configureAndWatch()
PropertyConfigurator.onfigureAndWatch()
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)。
log4j2動態加載配置文件
和log4j 1.X比起來,log4j2的動態加載配置很簡單就能實現,不需要另外在代碼中調用api,方法如下:
<configuration monitorInterval="30">
...
</configuration>
在log4j2.xml配置文件中的configuration節點添加monitorInterval的值,單位是秒,如果配置的值大於0,則會按照時間間隔來自動掃描配置文件是否被修改,並在修改后重新加載最新的配置文件。如果不配置該值,默認為0,即不掃描配置文件是否被修改。
Log4j 1.X動態加載配置文件的底層實現原理
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底層實現動態加載配置文件的簡單解析
雖然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應該就是可以的。有不對之處,歡迎指出。
這是官方原文:
Automatic Reconfiguration
When configured from a File, Log4j has the ability to automatically detect changes to the configuration file and reconfigure itself. If the monitorInterval attribute is specified on the configuration element and is set to a non-zero value then the file will be checked the next time a log event is evaluated and/or logged and the monitorInterval has elapsed since the last check. The example below shows how to configure the attribute so that the configuration file will be checked for changes only after at least 30 seconds have elapsed. The minimum interval is 5 seconds.
