使用Log4j2代碼方式配置實(shí)現(xiàn)線程級動態(tài)控制
一 需求
最近平臺進(jìn)行升級,要求日志工具從Log4j升級到Log4j2,以求性能上的提升。之前我寫過以代碼方式的配置Log4j,來實(shí)現(xiàn)線程級日志對象的管理,今天把版本升級到Log4j2,依然采用原有思路來做,但是實(shí)現(xiàn)上有諸多區(qū)別,這是因?yàn)長og4j2的實(shí)現(xiàn)較老版本改動太多。
至于以配置文件方式配置的方法,我不做介紹,對于Log4j2的詳細(xì)實(shí)現(xiàn)亦然,這些部分有興趣的朋友可以自己網(wǎng)絡(luò)搜索,也可以自行跟蹤源碼查閱。
主要需求為每個線程單獨(dú)一個日志對象處理,可動態(tài)控制日志輸出級別等參數(shù),可控制的同步及異步輸出模式,標(biāo)準(zhǔn)的日志頭格式等。
大體思路同我之前寫的Log4j配置,只不過日志對象的創(chuàng)建管理等功能實(shí)現(xiàn)上略有區(qū)別。
二 對外暴露的接口
設(shè)計的重中之重,這部分一旦設(shè)計完成便不可輕易改動,后續(xù)的維護(hù)只能盡其可能的向前兼容。
首先我們需要提供對外可用的預(yù)設(shè)參數(shù)值,包括日志輸出等級、日志的輸出目標(biāo)(文件、控制臺、網(wǎng)絡(luò)及數(shù)據(jù)庫等)等,這些值設(shè)定我們均以static final來修飾。
第二部分是全局參數(shù)的設(shè)置,這些參數(shù)不隨日志對象的狀態(tài)而發(fā)生變動,反而是作為日志對象構(gòu)造的屬性值來源,比如說日志文件的路徑、日志文件的最大容量、日志文件的備份數(shù)量,以及每個線程的日志對象初始的日志輸出等級等等。這部分參數(shù)可以從外部配置讀取,當(dāng)然也需要有默認(rèn)值設(shè)定,屬全局參數(shù)配置,以static修飾。
第三部分為日志輸出的接口定義,這部分可有可無,但是對于大型項(xiàng)目來說極為重要,以規(guī)范的接口進(jìn)行日志輸出,可以為日志的采集、分析帶來巨大便利,如通訊日志、異常日志等內(nèi)容的格式化輸出。
所以,按總體思路預(yù)先定義日志工具的外對暴露接口,如下:
public class LogUtil
{
public static final int LevelTrace = 0;
public static final int LevelDebug = 1;
public static final int LevelInfo = 2;
public static final int LevelWarn = 3;
public static final int LevelError = 4;
public static final String[] LevelName = new String[]
{ "TRACE", "DEBUG", "INFO", "WARN", "ERROR" };
public static final String TypeCommunication = "comm";
public static final String TypeProcess = "proc";
public static final String TypeException = "exce";
public static final int AppenderConsole = 1;
public static final int AppenderFile = 2;
private static int DefaultLogLevel = LevelDebug;
private static String FilePath = null;
private static String FileSize = "100MB";
private static int BackupIndex = -1;
private static int BufferSize = 0;
private static String LinkChar = "-";
private static int LogAppender = AppenderFile;
public static void log(int logLevel, String logType, String logText)
{
getThreadLogger().log(logLevel, logType, logText);
}
public static void logCommunication()
{
// TODO
}
public static void logException()
{
// TODO
}
……
}
這里我暫定了一個公用的日志輸出接口名為log(),參數(shù)列表為日志的輸出等級、日志的類型以及日志內(nèi)容。而通訊日志的輸出接口為logCommunication(),其參數(shù)列表暫時空著,按各位讀者的需求自行填寫,異常日志的輸出亦然。后文我會在測試部分對其填寫。
為了描述方便,我僅定義了兩個日志輸出目標(biāo),一為控制臺,二為文件。
三 代碼方式配置Log4j2日志對象
接下來是重頭戲,如果不采取配置文件的方式進(jìn)行Log4j2的配置,那么Log4j2會自行采用默認(rèn)的配置,這不是我們想要的。雖然這個過程我們無法選擇,也規(guī)避不了,但是我們最后都是使用Logger對象來進(jìn)行日志輸出的,所以我們只需要按需構(gòu)造Logger對象,并將其管理起來即可。
這里提供一個思路,首先LogUtil維護(hù)一組線程級日志對象,然后這一組線程級日志對象共同訪問同一組Logger對象。
跟蹤源碼我發(fā)現(xiàn)Log4j2對Logger對象的構(gòu)造還是較為復(fù)雜的,使用了大量的Builder,其實(shí)較為早期的版本中也提供了構(gòu)造函數(shù)方式來初始化對象,但是后期的版本卻都被標(biāo)記了@depreciation。對于Builder模式大家自己查閱其他信息了解吧。
好消息是Log4j2的核心組件依然是Appender、Logger,只不過提供了更多的可配置內(nèi)容,包括日志同時按日期和文件大小進(jìn)行備份,這才是我想要的,之前寫Log4j的時候我可是自己派生了一個Appender類才實(shí)現(xiàn)的同時備份。
向控制臺輸出的Appender具體類型為ConsoleAppender,我使用默認(rèn)的構(gòu)造函數(shù)來處理,這是唯一一個被公開出來,且沒有被@depreciation修飾的構(gòu)造函數(shù),想來只要是能通過默認(rèn)配置實(shí)現(xiàn)的都是被Log4j2認(rèn)可的吧,不然為啥要弄這么多Builder嘞。
向文件輸出的Appender我采用RollingFileAppender,無他,就是為了能夠?qū)崿F(xiàn)同時按日期及文件大小進(jìn)行備份,而且該Appender可適用全局異步模式,性能較AsyncAppender高了不少。它的創(chuàng)建方式要麻煩許多,因?yàn)槲倚枰O(shè)置觸發(fā)器來控制何時觸發(fā)備份,以及備份策略。
整體設(shè)計如下:
private Logger createLogger(String loggerName)
{
Appender appender = null;
PatternLayout.Builder layoutBuilder = PatternLayout.newBuilder();
layoutBuilder.withCharset(Charset.forName(DefaultCharset));
layoutBuilder.withConfiguration(loggerConfiguration);
Layout<String> layout = layoutBuilder.build();
if (LogUtil.AppenderConsole == LogUtil.getAppender())
{
appender = ConsoleAppender.createDefaultAppenderForLayout(layout);
}
if (LogUtil.AppenderFile == LogUtil.getAppender())
{
RollingFileAppender.Builder<?> loggerBuilder = RollingFileAppender.newBuilder();
if (LogUtil.getBufferSize() > 0)
{
loggerBuilder.withImmediateFlush(false);
loggerBuilder.withBufferedIo(true);
loggerBuilder.withBufferSize(LogUtil.getBufferSize());
System.setProperty(AsyncPropKey, AsyncPropVal);
}
else
{
loggerBuilder.withImmediateFlush(true);
loggerBuilder.withBufferedIo(false);
loggerBuilder.withBufferSize(0);
System.getProperties().remove(AsyncPropKey);
}
loggerBuilder.withAppend(true);
loggerBuilder.withFileName(getFilePath(loggerName));
loggerBuilder.withFilePattern(spellBackupFileName(loggerName));
loggerBuilder.withLayout(layout);
loggerBuilder.withName(loggerName);
loggerBuilder.withPolicy(CompositeTriggeringPolicy.createPolicy(
SizeBasedTriggeringPolicy.createPolicy(LogUtil.getFileSize()),
TimeBasedTriggeringPolicy.createPolicy("1", "true")));
loggerBuilder.withStrategy(DefaultRolloverStrategy.createStrategy(
LogUtil.getBackupIndex() > 0 ? String.valueOf(LogUtil.getBackupIndex()) : "-1", "1",
LogUtil.getBackupIndex() > 0 ? null : "nomax", null, null, true, loggerConfiguration));
appender = loggerBuilder.build();
}
appender.start();
loggerConfiguration.addAppender(appender);
AppenderRef appenderRef = AppenderRef.createAppenderRef(loggerName, Level.ALL, null);
AppenderRef[] appenderRefs = new AppenderRef[]
{ appenderRef };
LoggerConfig loggerConfig = LoggerConfig.createLogger(false, Level.ALL, loggerName, "false", appenderRefs, null,
loggerConfiguration, null);
loggerConfig.addAppender(appender, null, null);
loggerConfiguration.addLogger(loggerName, loggerConfig);
loggerContext.updateLoggers();
loggerConfiguration.start();
Logger logger = LogManager.getLogger(loggerName);
return logger;
}
注意!!我在初始化Logger對象的時候,是根據(jù)LogUtil是否開啟了異步輸出模式來判定是否需要開啟全局異步模式的,這里簡單說一些Log4j2的異步模式。
Log4j2提供了兩種異步模式,第一種是使用AsyncAppender,這種方式跟我以前寫的Log4j的異步輸出模式一樣,都是單獨(dú)開啟一個線程來輸出日志。第二種方式是AsychLogger,這個就厲害了,官推,而且官方提供了兩種使用模式,一個是混合異步,一個是全局異步,全局異步時不需要對配置文件進(jìn)行任何改動,盡在應(yīng)用啟動時添加一個jvm參數(shù)即可,并且據(jù)壓測數(shù)據(jù)顯示,全局異步模式下性能飆升。
更多的配置信息建議讀者朋友自行查閱官方文檔。
最后還有一點(diǎn)需要注意的是,Log4j2的全局異步是要依賴隊(duì)列緩存的,其實(shí)現(xiàn)采用的是disruptor,所以需要依賴這個外部jar,不然在初始化Logger對象的時候,你會看到相關(guān)異常。
貼一下依賴的Maven:
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.3.6</version> </dependency>
嚴(yán)重強(qiáng)調(diào):AsyncAppender、AsyncLogger以及全局異步的System.property設(shè)置,不要同時出現(xiàn)?。?!
最最后,我特么還是要嘴賤啰嗦一下,不見得“異步”就是好的,你想想看異步模式無非是額外線程或者緩存來實(shí)現(xiàn),這些也是要吃資源的,日志量大的場景下其帶來的收益很高,但小量日志場景下其對性能資源的消耗很可能大于其帶來的性能收益,請酌情使用。
四 線程級日志對象的設(shè)計
按上文中的設(shè)計思路,LogUtil持有一組線程級日志對象,而這一組日志對象又共享一組Logger對象。延續(xù)Log4j版本的設(shè)計,線程級日志對象類型依然為ThreadLogger,其基礎(chǔ)屬性為線程ID、日志類型以及日志的輸出級別等。
為了解決多并發(fā)問題,使用ConcurrentHashMap來存儲Logger對象,其Key值為線程ID,這里需要注意的是ConcurrentHashMap的put操作盡管能保證可見性,但是不能保證操作的原子性,所以在其put操作上需要額外加鎖。
class ThreadLogger
{
private static final String AsyncPropKey = "log4j2.contextSelector";
private static final String AsyncPropVal = "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector";
private static final String DefaultCharset = "UTF-8";
private static final String DefaultDateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS";
private static final Object LoggerMapLock = new Object();
private static final ConcurrentHashMap<String, Logger> LoggerMap = new ConcurrentHashMap<>();
private static LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
private static Configuration loggerConfiguration = loggerContext.getConfiguration();
public static void cleanLoggerMap()
{
synchronized (LoggerMapLock)
{
LoggerMap.clear();
}
}
private Logger getLogger(String loggerName)
{
if (StringUtil.isEmpty(loggerName))
return null;
Logger logger = LoggerMap.get(loggerName);
if (logger == null)
{
synchronized (LoggerMapLock)
{
logger = createLogger(loggerName);
LoggerMap.put(loggerName, logger);
}
}
return logger;
}
}
五 標(biāo)準(zhǔn)日志頭
頭部內(nèi)容應(yīng)包含當(dāng)前的日志輸出級別,日志打印所在的類名、行號,當(dāng)前的線程號、進(jìn)程號等信息。這里簡單介紹下進(jìn)程號及類名行號的獲取。
每個應(yīng)用進(jìn)程的進(jìn)程號唯一,所以僅在第一次獲取該信息時獲取即可,類名行號則通過方法棧的棧信息獲取,如下:
private static String ProcessID = null;
private String logLocation = null;
public static String getProcessID()
{
if (ProcessID == null)
{
ProcessID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
}
return ProcessID;
}
private static String getLocation()
{
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StringBuilder builder = new StringBuilder(stackTraceElements[3].getClassName());
builder.append(":");
builder.append(stackTraceElements[3].getLineNumber());
return builder.toString();
}
這里需要注意,類名行號信息對于每一條日志都不盡相同,所以不能聲明為static,并且需要在LogUtil中獲取,并通過ThreadLogger的setLocation()方法傳入到ThreadLogger對象,最后由ThreadLogger的日志頭拼裝函數(shù)拼接到日志頭部信息中:
private String spellLogText(int logLevel, String logText)
{
StringBuilder builder = new StringBuilder();
SimpleDateFormat sdf = new SimpleDateFormat(DefaultDateFormat);
builder.append(sdf.format(Calendar.getInstance().getTime()));
builder.append("|");
builder.append(LogUtil.LevelName[logLevel]);
builder.append("|");
builder.append(getProcessID());
builder.append("|");
builder.append(this.threadID);
builder.append("|");
builder.append(this.threadUUID);
builder.append("|");
builder.append(this.logLocation);
builder.append("|");
builder.append(logText);
return builder.toString();
}
六 異常日志的堆棧信息打印
需要注意咯,異常的日志輸出略微復(fù)雜些,這也是我經(jīng)常被人問起的一個問題,很多從事400或大機(jī)開發(fā)的同事轉(zhuǎn)入java開發(fā)后,最常問的就是異常堆棧的問題,看不懂,不知道怎么來的。
這里我只提一個事情,Exception的構(gòu)造,其成員有兩個,其一為cause,Throwable類型,其二為message,String類型,構(gòu)造函數(shù)提供較多,請大家自己做一個測試,看看不同構(gòu)造其輸出的內(nèi)容有何不同,cause和message成員又有何關(guān)系。
如果你弄明白了Exception的構(gòu)造,那么下面的邏輯不難理解:
private static String getExceptionStackTrace(Exception e)
{
StringBuilder builder = new StringBuilder();
builder.append(e.toString());
StackTraceElement[] stackTraceElements = e.getStackTrace();
for (int i = 0; i < stackTraceElements.length; i++)
{
builder.append("\r\nat ");
builder.append(stackTraceElements[i].toString());
}
Throwable throwable = e.getCause();
while (throwable != null)
{
builder.append("\r\nCaused by:");
builder.append(throwable.toString());
stackTraceElements = throwable.getStackTrace();
for (int i = 0; i < stackTraceElements.length; i++)
{
builder.append("\r\nat ");
builder.append(stackTraceElements[i].toString());
}
throwable = throwable.getCause();
}
return builder.toString();
}
七 測試
補(bǔ)充一個邏輯實(shí)現(xiàn),這里我以異常日志的打印作為測試接口,并在多線程并發(fā)場景下實(shí)現(xiàn)異步輸出。
異常日志輸出接口補(bǔ)充完整:
public static void logException(String desc, Exception exception)
{
getThreadLogger().setLogLocation(getLocation());
StringBuilder builder = new StringBuilder("Description=");
builder.append(StringUtil.isEmpty(desc) ? "" : desc);
builder.append(",Exception=");
builder.append(getExceptionStackTrace(exception));
log(LevelError, TypeException, builder.toString());
}
測試代碼:
public static void main(String[] args)
{
LogUtil.setAppender(LogUtil.AppenderFile);
LogUtil.setBufferSize(1024); // 1KB緩存
LogUtil.setFileSize("1MB");
LogUtil.setBackupIndex(10);
LogUtil.setFilePath("C:\\log");
for (int i = 0; i < 4; i++)
{
Thread thread = new Thread(new Runnable()
{
@Override
public void run()
{
LogUtil.setModule(Thread.currentThread().getId() + "");
for (int j = 0; j < 100000; j++)
{
LogUtil.logException("test", new Exception("my test"));
}
}
});
thread.start();
}
}
測試結(jié)果:


以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java8如何將Array轉(zhuǎn)換為Stream的實(shí)現(xiàn)代碼
這篇文章主要介紹了Java8如何將Array轉(zhuǎn)換為Stream的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
利用spring aop實(shí)現(xiàn)動態(tài)代理
這篇文章主要為大家詳細(xì)介紹了利用spring aop實(shí)現(xiàn)動態(tài)代理的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03
Java初學(xué)者問題圖解(動力節(jié)點(diǎn)Java學(xué)院整理)
本文通過圖文并茂的形式給大家介紹了java初學(xué)者問題,非常不錯,具有參考借鑒價值,需要的朋友參考下2017-04-04
從0開始學(xué)習(xí)大數(shù)據(jù)之java spark編程入門與項(xiàng)目實(shí)踐
這篇文章主要介紹了從0開始學(xué)習(xí)大數(shù)據(jù)之java spark編程入門與項(xiàng)目實(shí)踐,結(jié)合具體入門項(xiàng)目分析了大數(shù)據(jù)java spark編程項(xiàng)目建立、調(diào)試、輸出等相關(guān)步驟及操作技巧,需要的朋友可以參考下2019-11-11

