log4j和log4j2怎麼動態載入配置檔案
應用場景與問題
當專案在執行時,我們如果需要修改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應該就是可以的。有不對之處,歡迎指出。