Log4j對Java開發者來講是常常使用到的日誌框架,我每次使用都對它的配置文件頭大,網上搜一個別人的例子本身改巴改巴,草草了事。再次使用時,又忘了怎麼回事了。此次忽然來了興趣,想看看它具體是怎麼作的,作個筆記,加深一下印象。併發
目前的版本是 log4j:log4j:1.2.17app
Log4j的輸出類都須要實現的接口,爲了用戶自定義log輸出策略,抽象出了如下幾點功能框架
這個接口只定義了一個方法 void activateOptions();
,用於按需初始化一些配置。ide
既然是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() {} ````
繼承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,下次再更新。 還有上面幾個關鍵方法中的同步關鍵字,我還沒搞懂應該怎麼解釋。