Log4j DailyRollingFileAppender源碼初探

瞎扯

Log4j對Java開發者來講是常常使用到的日誌框架,我每次使用都對它的配置文件頭大,網上搜一個別人的例子本身改巴改巴,草草了事。再次使用時,又忘了怎麼回事了。此次忽然來了興趣,想看看它具體是怎麼作的,作個筆記,加深一下印象。併發

目前的版本是 log4j:log4j:1.2.17app

依賴結構

clipboard.png

Appender接口

Log4j的輸出類都須要實現的接口,爲了用戶自定義log輸出策略,抽象出了如下幾點功能框架

  • 過濾鏈
  • log輸出
  • 錯誤處理
  • log格式

clipboard.png

OptionHandler接口

這個接口只定義了一個方法 void activateOptions();,用於按需初始化一些配置。ide

AppenderSkeleton抽象類

既然是Skeleton,那它必須是最核心的骨架。這個類主要作了如下幾個事ui

  • 過濾鏈(鏈表)增刪操做this

    protected Filter headFilter;
    protected Filter tailFilter;
    
    public void addFilter(Filter newFilter) {
    if(headFilter == null) {
      headFilter = tailFilter = newFilter;
    } else {
      tailFilter.setNext(newFilter);
      tailFilter = newFilter;    
    }
    }
    
    public void clearFilters() {
    headFilter = tailFilter = null;
* 定義了日誌優先級 `threshold` 「門檻」,實現日誌的分級輸出
protected Priority threshold;//默認爲空

public boolean isAsSevereAsThreshold(Priority priority) {
    return ((threshold == null) || priority.isGreaterOrEqual(threshold));

}編碼

* log的輸出核心邏輯
public synchronized void doAppend(LoggingEvent event) {
if(closed) {
  LogLog.error("Attempted to append to closed appender named ["+name+"].");
  return;
}
//日誌級別攔截
if(!isAsSevereAsThreshold(event.getLevel())) {
  return;
}

Filter f = this.headFilter;

//結合Filter實現類自身的優先級[中止輸出、當即輸出、依次過濾後輸出]進行過濾,
FILTER_LOOP:
while(f != null) {
  switch(f.decide(event)) {
  case Filter.DENY: return;
  case Filter.ACCEPT: break FILTER_LOOP;
  case Filter.NEUTRAL: f = f.getNext();
  }
}
//具體的輸出開放給子類實現
this.append(event);

}spa

* 下放的權限
//子類只須要關心日誌具體的輸出方式
abstract protected void append(LoggingEvent event);
//配置方法,子類能夠按本身的需求覆寫
public void activateOptions() {}
````

WriteAppender

繼承AppenderSkeleton,用戶可選擇將log按字符流或字節流輸出。增長了如下特性線程

  • 提供了寫入刷新控制
  • 可配置編碼方式
  • 提供了靜態字符流QuitWriter,異常不會拋出,會交給ErrorHandler去處理debug

    //默認實時刷新,效率低但可保證每次輸出都可寫入,設爲false時,若程序崩潰,尾部log可能丟失
    protected boolean immediateFlush = true;
    protected String encoding;
* 提供了字節流->字符流的轉換
* log輸出 官方註釋說明了在log輸出以前作的檢查或過濾操做[檢查日誌級別->過濾->檢查當前輸出情況(Appender狀態、輸出流、格式是否均具有)->輸出]
public void append(LoggingEvent event) {

// Reminder: the nesting of calls is:
//
//    doAppend()
//      - check threshold
//      - filter
//      - append();
//        - checkEntryConditions();
//        - subAppend();

if(!checkEntryConditions()) {
  return;
}
subAppend(event);

}

protected void subAppend(LoggingEvent event) {

this.qw.write(this.layout.format(event));//將日誌格式化後輸出
//依次輸出異常棧
if(layout.ignoresThrowable()) {
  String[] s = event.getThrowableStrRep();
  if (s != null) {
    int len = s.length;
    for(int i = 0; i < len; i++) {
      this.qw.write(s[i]);
      this.qw.write(Layout.LINE_SEP);
    }
  }
}
//寫入刷新控制
if(shouldFlush(event)) {
  this.qw.flush();
}

}

* 還有一些Header、Footer的寫入和輸出流的關閉操做

### FileAppender ###
繼承了WriteAppender,將log輸出到文件。這個比較簡單,主要就是將父類中的輸出流封裝指向到文件。

protected boolean fileAppend = true;//是否覆蓋
protected String fileName = null;//目標文件名
protected boolean bufferedIO = false;//是否緩衝
protected int bufferSize = 8*1024;//默認緩衝區大小

public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)

throws IOException {
LogLog.debug("setFile called: "+fileName+", "+append);

// It does not make sense to have immediate flush and bufferedIO.
if(bufferedIO) {
  setImmediateFlush(false);//既然緩衝了,那意味着父類中的刷新控制爲false-不進行同步刷新
}

reset();
FileOutputStream ostream = null;
try {
      ostream = new FileOutputStream(fileName, append);
} catch(FileNotFoundException ex) {
      String parentName = new File(fileName).getParent();
      if (parentName != null) {
         File parentDir = new File(parentName);
         if(!parentDir.exists() && parentDir.mkdirs()) {
            ostream = new FileOutputStream(fileName, append);
         } else {
            throw ex;
         }
      } else {
         throw ex;
      }
}
Writer fw = createWriter(ostream);//利用父類中的字節流->字符流轉換方法
if(bufferedIO) {
  fw = new BufferedWriter(fw, bufferSize);
}
this.setQWForFiles(fw);//實例化父類中的QuitWriter(實際在上面指向了文件輸出流)
this.fileName = fileName;
this.fileAppend = append;
this.bufferedIO = bufferedIO;
this.bufferSize = bufferSize;
writeHeader();
LogLog.debug("setFile ended");

}

protected void setQWForFiles(Writer writer) {

this.qw = new QuietWriter(writer, errorHandler);

}

### DailyRollingFileAppender ###
繼承FileAppender,將log文件進行平常轉存。咱們經常使用的日誌處理類,官方註釋裏說已證明有`併發和數據丟失`的問題,惋惜我看不出來...
能夠自定義轉存日期表達式datePattern(格式需遵循SimpleDateFormat的約定),如

'.'yyyy-MM
'.'yyyy-ww
'.'yyyy-MM-dd
'.'yyyy-MM-dd-a
'.'yyyy-MM-dd-HH
'.'yyyy-MM-dd-HH-mm

注意不要包含任何冒號

它根據用戶提供的日期表達式datePattern,經過內部類RollingCalendar計算獲得對應的`日期檢查週期rc.type`,每次log輸出以前,計算`下次檢查時間nextCheck`,對比當前時間,判斷是否進行文件轉存。

主要方法有

//各級檢查週期對應的常量
// The code assumes that the following constants are in a increasing sequence.
static final int TOP_OF_TROUBLE=-1;
static final int TOP_OF_MINUTE = 0;
static final int TOP_OF_HOUR = 1;
static final int HALF_DAY = 2;
static final int TOP_OF_DAY = 3;
static final int TOP_OF_WEEK = 4;
static final int TOP_OF_MONTH = 5;

//初始化配置項
public void activateOptions() {

super.activateOptions();
if(datePattern != null && fileName != null) {
  now.setTime(System.currentTimeMillis());
  sdf = new SimpleDateFormat(datePattern);
  int type = computeCheckPeriod();//計算datePattern對應的檢查週期
  printPeriodicity(type);//打印當前檢查週期
  rc.setType(type);//內部RollingCalendar會在log輸出以前根據type計算出下次檢查時間
  File file = new File(fileName);//log輸出文件名
  scheduledFilename = fileName+sdf.format(new Date(file.lastModified()));//log轉存文件名

} else {
  LogLog.error("Either File or DatePattern options are not set for appender ["
       +name+"].");
}

}

//初始化配置時,計算檢查週期
int computeCheckPeriod() {

RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.getDefault());
// set sate to 1970-01-01 00:00:00 GMT
Date epoch = new Date(0);
if(datePattern != null) {
  for(int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
    simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT
    String r0 = simpleDateFormat.format(epoch);
    rollingCalendar.setType(i);
    Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
    String r1 =  simpleDateFormat.format(next);
    //r0、r1均以datePattern格式來轉換日期,若type小於datePattern表示的最小範圍,對應日期next的變化不會影響格式化後的r1的值
    //每循環一次,type(也就是i) 增1,最終獲得的type就是datePattern表示的最小範圍
    if(r0 != null && r1 != null && !r0.equals(r1)) {
      return i;
    }
  }
}
return TOP_OF_TROUBLE; // Deliberately head for trouble...

}

//log輸出
protected void subAppend(LoggingEvent event) {

//在每次調用父類subAppend方法輸出文件以前,進行週期計算
//若當前時間晚於'檢查點時間',調用rollOver()方法進行日誌轉存,將當前log文件轉存爲指定日期結尾的文件,而後將父類的QuietWriter指向新的log文件
//固然在轉存以前,須要再次計算並刷新'檢查點時間',rc內部type會影響計算結果(在初始化配置時已根據datePattern計算獲得)
long n = System.currentTimeMillis();
if (n >= nextCheck) {
  now.setTime(n);
  nextCheck = rc.getNextCheckMillis(now);
  try {
rollOver();
  }
  catch(IOException ioe) {
      if (ioe instanceof InterruptedIOException) {
          Thread.currentThread().interrupt();
      }
      LogLog.error("rollOver() failed.", ioe);
  }
}
super.subAppend(event);

}

### RollingFileAppender ###
一樣繼承於FileAppender,由文件大小來轉存log文件

### ExternallyRolledFileAppender ###
繼承於RollingFileAppender,經過Socket監聽轉存消息來進行轉存操做,後臺運行着一個Socket監聽線程,每次收到轉存消息,會新起一個線程進行日誌轉存,並將轉存結果信息返回。



## 不足 ##
只是介紹了關鍵的一些類,但他們的生命週期,相關的屬性類和輔助類還沒提到,主要是Filter和Layout,下次再更新。
還有上面幾個關鍵方法中的同步關鍵字,我還沒搞懂應該怎麼解釋。
相關文章
相關標籤/搜索