1. 程式人生 > >log4j和log4j2怎麼動態載入配置檔案

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應該就是可以的。有不對之處,歡迎指出。

參考連結