程式碼方式配置Log4j並實現執行緒級日誌管理 第五部分
文章目錄
一 第三方程式不應僅限於會用
關於日誌檔案備份的問題,我跟很多人都溝通過,但是結果很遺憾,絕大部分人對於Log4j的理解僅限於常規的使用,網路上能搜尋到的資源絕大部分也只是關於配置的介紹。
其實這樣的現象並不少見,略微延展起來就變成了老生常談的一個問題:對於開源框架的使用,會用即可?我記得剛畢業那會就跟同學爭論過,時至今日我依然認為,會用是最起碼的要求,而理解其實現的原理是必要的,因為我們無法保證在使用任何第三方程式時,不會出現問題或者特殊的需求,如果我們對於其三方程式的理解僅限於使用上,那麼這些問題一旦出現——腦殼疼。
當下的需求就是一個例子,如果不清楚Log4j是如何進行檔案備份的,那麼何談實現更為複雜的需求?這裡我不再重複網路上滿篇的底層實現邏輯,有興趣的朋友自行百度,簡而言之一句話:檔案備份邏輯在Appender的rollOver()方法裡。
二 同時按日期和檔案大小備份
這是一個很常規的需求,然而Log4j對於這樣的需求似乎很難支援,要麼選擇RollingFileAppender按檔案大小進行備份,要麼選擇DailyRollingFileAppender按天進行檔案備份。
前幾部分我簡單的介紹了按程式碼方式配置Log4j的思路,最後的一部分,讓我們來實現這個需求,通過複寫來實現日誌檔案同時按日期和檔案大小進行備份。
實現思路是自己實現一個Appender,並重寫rollOver()方法,考慮到按檔案大小進行備份的實現,RollingFileAppender已經做到了,那麼我們對RollingFileAppender進行一個簡單的功能擴充套件即可。
package com.bubbling;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.helpers.CountingQuietWriter;
import org.apache.log4j.spi.LoggingEvent;
/**
* 派生自RollingFileAppender,所有具備按檔案大小備份的條件; 通過重寫其rollOver()方法,補充按日期備份的邏輯,即可實現。
*
* @author 胡楠
*
*/
public class MyRollingFileAppender extends RollingFileAppender
{
/**
* 按日期備份時的檔名包含的日期樣式
*/
public static final String strDatePattern = "yyyyMMdd";
/**
* 當前的日期,一旦日期發生變化,則觸發對應的備份邏輯
*/
private String strDate = new SimpleDateFormat(strDatePattern).format(Calendar.getInstance().getTime());
/*
* 注意:別對這個方法理解有問題,這裡僅僅是發生了檔案備份後的處理邏輯,不是判斷是否進行備份的邏輯
*/
public void rollOver()
{
File target;
File file;
// 如果設定了最大檔案備份數量(每日的喲),那麼執行以下邏輯
if (maxBackupIndex > 0)
{
file = new File(getBackupFileName(maxBackupIndex));
if (file.exists())
{
file.delete();
}
// 這裡的邏輯略繞,從當前已備份到的最大序號開始,倒著遍歷,看不懂的跟蹤除錯下就好
for (int i = maxBackupIndex - 1; i >= 1; i--)
{
file = new File(getBackupFileName(i));
if (file.exists())
{
target = new File(getBackupFileName(i + 1));
file.renameTo(target);
}
}
target = new File(getBackupFileName(1));
this.closeFile();
file = new File(fileName);
file.renameTo(target);
}
// 如果沒有設定過最大檔案備份數量,則給他21個億
else
{
for (int i = 1; i < Integer.MAX_VALUE; i++)
{
target = new File(getBackupFileName(i));
if (!target.exists())
{
this.closeFile();
file = new File(fileName);
file.renameTo(target);
break;
}
}
}
// 以上僅處理了按檔案大小備份,下面處理日期發生變化後的處理
try
{
String strFile = this.getFile();
if (!(strDate.equals(strFile.substring(strFile.indexOf("-") + 1, strFile.lastIndexOf(".log")))))
{
StringBuilder builder = new StringBuilder(strFile.substring(0, strFile.lastIndexOf("-") + 1));
builder.append(strDate);
builder.append(".log");
this.fileName = builder.toString();
}
this.setFile(fileName, false, bufferedIO, bufferSize);
}
catch (IOException e)
{
e.printStackTrace();
}
}
/*
* 注意:這裡才是判斷是否備份的邏輯,依然重寫,使日期變更也觸發rollOver()方法
*/
protected void subAppend(LoggingEvent event)
{
String strCurrentDate = new SimpleDateFormat(strDatePattern).format(Calendar.getInstance().getTime());
// 滿足以下所有條件才進行檔案備份
if (fileName != null && ((CountingQuietWriter) qw).getCount() >= maxFileSize || !strDate.equals(strCurrentDate))
{
// 如果是因為日期變動導致的備份,需要調整當前時間
if (!strDate.equals(strCurrentDate))
{
strDate = strCurrentDate;
}
this.rollOver();
}
super.subAppend(event);
}
/**
* 此方法用於自行改寫備份檔名,我喜歡***yyyyMMdd-00001.log這樣的格式,看著舒服
*
* @param maxBackupIndex
* @return
*/
private String getBackupFileName(int maxBackupIndex)
{
StringBuilder builder = new StringBuilder();
builder.append(fileName.substring(0, fileName.indexOf(".log")));
builder.append("-");
builder.append(String.format("%05d", maxBackupIndex));
builder.append(".log");
return builder.toString();
}
}
三 調整ThreadLogger使用複寫的Appender
實現了自定義的Appender之後,我們把ThreadLogger中建立Logger物件的方法重構下,不再使用原RollingFileAppender物件,而且因為加入了檔案大小和檔案備份數量等引數,LogUtil也需要調整。
public class ThreadLogger
{
……
private String filePath = "";
private LogTarget logTarget = LogTarget.File;
private LogLevel logLevel = LogLevel.Debug;
// 補充新的屬性,檔案大小
private String maxFileSize = "1MB";
// 補充新的屬性,檔案備份數量
private int maxBackupIndex = 10;
……
private Logger getFileLogger()
{
// 初始化一個RollingFileAppender物件,此處調整為自定義的Appender
MyRollingFileAppender appender = new MyRollingFileAppender();
// 設定檔案備份數量
appender.setMaxBackupIndex(maxBackupIndex);
// 設定檔案大小
appender.setMaxFileSize(maxFileSize);
// 設定Appender物件名,若不設定則執行時報錯
appender.setName(filePath);
……
}
……
}
四 簡單測試下
最後簡單驗證下,因為沒有仔細的完善所有執行邏輯,僅僅為了舉個例子,所以執行結果大致符合預期就算是大功告成,各位看官有興趣可以自行對邏輯進行處理。
package com.bubbling;
import com.bubbling.LogUtil.LogLevel;
public class LogUtilTest
{
public static void main(String[] args)
{
long id = Thread.currentThread().getId();
LogUtil.setFilePath("D:\\測試\\" + id + ".log");
LogUtil.setLogLevel(LogLevel.Debug);
for (int i = 0; i < 5000000; i++)
{
LogUtil.debug("測試執行緒:" + id + "debug輸出");
LogUtil.info("測試執行緒:" + id + "info輸出");
LogUtil.warn("測試執行緒:" + id + "warn輸出");
LogUtil.error("測試執行緒:" + id + "error輸出");
}
}
}
五 結語
從一開始的需求,到逐步的實現,這其中並沒有什麼特別高深莫測的東西,我始終主張能能最簡單的實現,來滿足當下的需求即可,直到對效能有了更高的要求,再去仔細分析優化空間。
但這並不是說下手寫程式碼的時候不需要考慮效能問題,嚴謹的邏輯(一般就好)是每一個程式設計師的基礎,查閱原始碼、文件及跟蹤除錯是基本功。
不要總是想著複製貼上,如果你對技能提升有渴望,請多花些心思去思考設計方案,而不是急著拷貝別人的程式碼。
程式設計師應該多花時間在設計上,實現上消耗的時間應該是最少的,程式碼的編寫也不是一步到位的,隨著實現的深入,逐步重構,完善程式碼層級結構,設計模式也就漸漸顯現出來,合理的設計也對後期的維護提供了堅實的基礎。雖說道理如此簡單,但是真的能沉下心來這樣做的,怕是十不足一。