程式碼方式配置Log4j並實現執行緒級日誌管理 第三部分
文章目錄
一 對第二部分的一些補充
第二部分用很簡單的樣例來描述了Logger物件的初始化,其實邏輯是不嚴謹的,而且執行時會有一些異常,這裡我簡單修正下,還是以RollingFileAppender為例,能夠讓其進行日誌輸出,如果有更加豐富的需求,那麼請參考官方文件,或者自行百度下,我的部落格僅做設計思路分享,不做教程:
private Logger getFileLogger ()
{
// 初始化一個RollingFileAppender物件
RollingFileAppender appender = new RollingFileAppender();
// 設定Appender物件名,若不設定則執行時報錯
appender.setName(filePath);
// 設定日誌內容追加到檔案內容末尾
appender.setAppend(true);
// 設定日誌檔案的儲存位置
appender.setFile(filePath);
// 不開啟非同步模式
appender.setBufferedIO(false);
// 僅開啟非同步模式,快取大小才有意義
appender.setBufferSize(0);
// 設定日誌輸出格式
appender.setLayout(new PatternLayout("%m%n"));
// 下面的方法是對上面四個屬性設定的一個封裝
// appender.setFile("", true, false, 0);
// 需要啟用Appender物件的配置,這樣屬性設定才會生效
appender.activateOptions();
// 注意這裡需要是指Logger物件名,後續設計會對此處進行重構,目前以呼叫類的SimpleName作為Logger物件的name屬性值
Logger logger = Logger.getLogger(filePath);
// 為Logger物件新增Appender成員
logger.addAppender(appender);
// 設定Logger物件不繼承上層節點屬性配置,僅向檔案中輸出內容
logger.setAdditivity(false);
// 為什麼設定日誌輸出級別為Trace,因為後續我們需要通過LogUtil公開的方法對日誌級別進行動態控制,所以此處暫時設定為最低級別
logger.setLevel(Level.TRACE);
return logger;
}
二 如何實現執行緒級日誌物件管理
第二部分已經通過程式碼配置的方式完成了Log4j的核心物件的初始化工作,按需求繼續設計,則要考慮如何實現執行緒級的日誌物件管理了。
這裡有兩個方向可以考慮(其他人有想法可以分享下):
- 每個處理執行緒分配一個Logger物件,並使用ThreadLocal進行管理,保證不會出現併發問題。但這裡有一個比較難以解決的問題,如果執行緒數量過多,或者說應用的執行緒池管理並不嚴格的情況下,Logger物件可能會非常的多,而且沒有行之有效的物件回收機制,那麼是存在記憶體消耗劇烈的情況的,甚至記憶體溢位。
- 維護一組Logger物件,並且對所有執行緒提供Logger物件資源,以實現物件管理及資源的複用。但是需要注意,因為Logger物件組對於所有執行緒開放,那麼解決併發問題顯得尤為重要。
這裡選用第二種實現方式,首先對LogUtil進行重構改造,並且將Logger物件的例項化過程對應用隱藏,一應配置首先通過LogUtil的預設屬性配置,並通過LogUtil提供的靜態方法針對每一個執行緒進行重置,那麼我們首先需要一個ThreadLogger類,對Logger物件進行配置隱藏。
三 實現ThreadLogger
建立ThreadLogger類,由LogUtil對其進行維護管理,ThreadLogger內部則維護一組Logger物件,供所有執行緒使用。
這裡大家一定要對結構層次有所瞭解,不然設計到最後會懵逼的,並且慎之又慎的注意所有屬性的訪問級別,哪些屬性應該是類級訪問的,哪些是物件級可訪問的。
package com.bubbling;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.RollingFileAppender;
import com.bubbling.LogUtil.LogLevel;
import com.bubbling.LogUtil.LogTarget;
/**
* 執行緒級日誌物件,共享一組Logger物件對外提供日誌輸出功能
*
* @author 胡楠
*
*/
public class ThreadLogger
{
// ThreadLogger維護一組Logger物件,供所有執行緒使用,使用ConcurrentHashMap解決併發訪問問題,其中map的key值為Logger物件名
private static ConcurrentHashMap<String, Logger> loggerMap = new ConcurrentHashMap<String, Logger>();
// 清空所有Logger物件,
public static void cleanThreadLogger()
{
loggerMap.clear();
}
private String filePath = "";
private LogTarget logTarget = LogTarget.File;
private LogLevel logLevel = LogLevel.Debug;
ThreadLogger()
{
}
ThreadLogger(String configFilePath)
{
// TODO 使用配置檔案初始化ThreadLogger物件,檔案解析過程自己寫,這裡僅作參考
}
public String getFilePath()
{
return filePath;
}
public void setFilePath(String path)
{
this.filePath = path;
}
public LogTarget getLogTarget()
{
return logTarget;
}
public void setLogTarget(LogTarget target)
{
if (logTarget != target)
{
this.logTarget = target;
Logger logger = loggerMap.get(filePath);
if (logger != null)
{
synchronized (logger)
{
getLogger();
}
}
}
}
public LogLevel getLogLevel()
{
return logLevel;
}
public void setLogLevel(LogLevel level)
{
this.logLevel = level;
}
public Logger getLogger()
{
Logger logger = loggerMap.get(filePath);
if (logger == null)
{
logger = initLogger();
loggerMap.put(filePath, logger);
}
return logger;
}
public void logTrace(String message)
{
if (logLevel.ordinal() <= LogLevel.Trace.ordinal())
{
getLogger().trace(message);
}
}
public void logDebug(String message)
{
if (logLevel.ordinal() <= LogLevel.Debug.ordinal())
{
getLogger().debug(message);
}
}
public void logInfo(String message)
{
if (logLevel.ordinal() <= LogLevel.Info.ordinal())
{
getLogger().info(message);
}
}
public void logWarn(String message)
{
if (logLevel.ordinal() <= LogLevel.Warn.ordinal())
{
getLogger().warn(message);
}
}
public void logError(String message)
{
if (logLevel.ordinal() <= LogLevel.Error.ordinal())
{
getLogger().error(message);
}
}
private Logger initLogger()
{
Logger logger = null;
if (LogTarget.Console == logTarget)
{
logger = getConsoleLogger();
}
else if (LogTarget.File == logTarget)
{
logger = getFileLogger();
}
else if (LogTarget.Socket == logTarget)
{
logger = getSocketLogger();
}
return logger;
}
private Logger getSocketLogger()
{
// TODO Auto-generated method stub
return null;
}
private Logger getConsoleLogger()
{
// TODO Auto-generated method stub
return null;
}
private Logger getFileLogger()
{
// 初始化一個RollingFileAppender物件
RollingFileAppender appender = new RollingFileAppender();
// 設定Appender物件名,若不設定則執行時報錯
appender.setName(filePath);
// 設定日誌內容追加到檔案內容末尾
appender.setAppend(true);
// 設定日誌檔案的儲存位置
appender.setFile(filePath);
// 不開啟非同步模式
appender.setBufferedIO(false);
// 僅開啟非同步模式,快取大小才有意義
appender.setBufferSize(0);
// 設定日誌輸出格式
appender.setLayout(new PatternLayout("%m%n"));
// 下面的方法是對上面四個屬性設定的一個封裝
// appender.setFile("", true, false, 0);
// 需要啟用Appender物件的配置,這樣屬性設定才會生效
appender.activateOptions();
// 注意這裡需要是指Logger物件名,後續設計會對此處進行重構,目前以呼叫類的SimpleName作為Logger物件的name屬性值
Logger logger = Logger.getLogger(filePath);
// 為Logger物件新增Appender成員
logger.addAppender(appender);
// 設定Logger物件不繼承上層節點屬性配置,僅向檔案中輸出內容
logger.setAdditivity(false);
// 為什麼設定日誌輸出級別為Trace,因為後續我們需要通過LogUtil公開的方法對日誌級別進行動態控制,所以此處暫時設定為最低級別
logger.setLevel(Level.TRACE);
return logger;
}
}
這是一個非常簡單的封裝,ThreadLogger維護了一組Logger物件,並且每個程序都對應一個ThreadLogger物件,僅提供了簡單的日誌檔案路徑及日誌輸出級別的控制,如果有其他的需要,那麼讀者可自行設計。
四 重構LogUtil
執行緒級的日誌物件已經實現完畢,那麼接下來就需要對LogUtil進行調整,LogUtil應該維護一組ThreadLogger物件,以應對每一個執行緒的日誌輸出需求,並且對外提供日誌輸出的方法:
package com.bubbling;
import java.util.concurrent.ConcurrentHashMap;
/**
* 1.實現程式碼方式配置Log4j <br>
* 2.實現執行緒級日誌物件管理 <br>
* 3.實現日誌的非同步輸出模式 <br>
* 4.實現按日誌檔案大小及日期進行檔案備份
*
* @author 胡楠
*
*/
public final class LogUtil
{
/**
* 阻止外部例項化LogUtil,LogUtil應該是一個僅提供日誌配置及輸出相關方法的工具類
*/
private LogUtil()
{
}
/**
* 使用列舉定義日誌輸出的目的地
*/
public enum LogTarget
{
Console, File, Socket;
}
/**
* 對應Log4j的日誌輸出級別,依然用列舉進行定義
*/
public enum LogLevel
{
Trace, Debug, Info, Warn, Error;
private int value;
public void setValue(int value)
{
this.value = value;
}
public int getValue()
{
return value;
}
}
/**
* 配置檔案路徑
*/
private static String LOGGER_CONFIG_FILE_PATH = null;
/**
* LogUtil維護一組ThreadLogger,這些ThreadLogger共享一組Logger物件
*/
private static ConcurrentHashMap<Long, ThreadLogger> LOGGER_MAP = new ConcurrentHashMap<Long, ThreadLogger>();
/**
* 提供設定配置檔案的方法,以重置預設的配置,注意呼叫該方法後應該清除掉當前已例項化的所有Logger物件
*
* @param path
* 配置檔案路徑
*/
public static void setConfigFilePath(String path)
{
LOGGER_CONFIG_FILE_PATH = path;
reloadConfig();
}
/**
* 設定當前處理執行緒對應的日誌輸出物件的日誌檔案路徑
*
* @param path
*/
public static void setFilePath(String path)
{
getThreadLogger().setFilePath(path);
}
/**
* 設定當前處理執行緒對應的日誌輸出物件的日誌輸出級別
*
* @param level
*/
public static void setLogLevel(LogLevel level)
{
getThreadLogger().setLogLevel(level);
}
public static void trace(String message)
{
log(LogLevel.Trace, message);
}
public static void debug(String message)
{
log(LogLevel.Debug, message);
}
public static void info(String message)
{
log(LogLevel.Info, message);
}
public static void warn(String message)
{
log(LogLevel.Warn, message);
}
public static void error(String message)
{
log(LogLevel.Error, message);
}
/**
* 重新裝在配置,若配置檔案路徑不為空,則清空當前所有Logger物件,後續物件建立通過新的LoggerConfig來
*/
private static void reloadConfig()
{
if (LOGGER_CONFIG_FILE_PATH != null)
{
ThreadLogger.cleanThreadLogger();
}
}
private static ThreadLogger getThreadLogger()
{
long threadId = Thread.currentThread().getId();
ThreadLogger logger = LOGGER_MAP.get(threadId);
if (logger == null)
{
if (LOGGER_CONFIG_FILE_PATH == null)
{
logger = new ThreadLogger(LOGGER_CONFIG_FILE_PATH);
}
else
{
logger = new ThreadLogger();
}
}
LOGGER_MAP.put(threadId, logger);
return logger;
}
private static void log(LogLevel level, String message)
{
if (level == LogLevel.Trace)
{
getThreadLogger().logTrace(message);
}
else if (level == LogLevel.Debug)
{
getThreadLogger().logDebug(message);
}
else if (level == LogLevel.Info)
{
getThreadLogger().logInfo(message);
}
else if (level == LogLevel.Warn)
{
getThreadLogger().logWarn(message);
}
else if (level == LogLevel.Error)
{
getThreadLogger().logError(message);
}
}
}
可以看到,LogUtil提供了一個公開的,且針對執行緒設定的日誌檔案路徑、日誌輸出級別的方法,並且提供了針對不同日誌輸出級別的靜態方法,如此我們已經基本上實現了大部分需求,通過程式碼的方式對Log4j實現了配置,並且針對每一個應用執行緒實現了簡單的屬性控制,接下來我再提供一個簡單的測試方法,供大家參考:
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(LogUtil.LogLevel.Error);
LogUtil.debug("測試執行緒:" + id + "debug輸出");
LogUtil.info("測試執行緒:" + id + "info輸出");
LogUtil.warn("測試執行緒:" + id + "warn輸出");
LogUtil.error("測試執行緒:" + id + "error輸出");
Thread thread = new Thread(new Runnable()
{
public void run()
{
long id = Thread.currentThread().getId();
LogUtil.setFilePath("D:\\測試\\" + id + ".log");
LogUtil.setLogLevel(LogLevel.Debug);
LogUtil.debug("測試執行緒:" + id + "debug輸出");
LogUtil.info("測試執行緒:" + id + "info輸出");
LogUtil.warn("測試執行緒:" + id + "warn輸出");
LogUtil.error("測試執行緒:" + id + "error輸出");
}
});
thread.start();
}
}
最後的輸出結果是沒有問題的,兩個執行緒兩個日誌檔案,其對應的檔案路徑也不相同,並且兩個執行緒的日誌輸出級別不同,其輸出的內容亦不相同。
到此為止,我們還剩下最後兩個個需求,如何實現非同步模式的日誌輸出,因為同步模式下,Logger頻繁的通過IO來寫入檔案,這對磁碟的訪問壓力是非常大的,而且據我自己的測試,Log4j本身對非同步模式的支援並不是特別好,僅提供了一定大小的快取,快取滿了之後再行寫入檔案,雖然減少了IO訪問次數,但是實際上的執行效率並沒有很大的提升,這一點在Log4j2上改進的非常明顯。
並且實現日誌同時按日期和日誌檔案大小進行備份也是極為關鍵的,這涉及到對原始碼的延展,如何重寫Appender,以實現定製化的需求。
我會在第四部分來單獨說一下如何實現非同步模式的日誌輸出,重點不在實現需求上,而在於給所有讀者提供一個Java多執行緒併發操作相關的設計思路,很多新手對於執行緒、併發這些概念的理解比較模糊,更多的場景是上手之後寫出的程式碼其質量較差,執行效率非常低,藉此機會跟大家一起做一個相關方面的交流。