使用Log4j2代碼方式配置實(shí)現(xiàn)線程級(jí)動(dòng)態(tài)控制
一 需求
最近平臺(tái)進(jìn)行升級(jí),要求日志工具從Log4j升級(jí)到Log4j2,以求性能上的提升。之前我寫(xiě)過(guò)以代碼方式的配置Log4j,來(lái)實(shí)現(xiàn)線程級(jí)日志對(duì)象的管理,今天把版本升級(jí)到Log4j2,依然采用原有思路來(lái)做,但是實(shí)現(xiàn)上有諸多區(qū)別,這是因?yàn)長(zhǎng)og4j2的實(shí)現(xiàn)較老版本改動(dòng)太多。
至于以配置文件方式配置的方法,我不做介紹,對(duì)于Log4j2的詳細(xì)實(shí)現(xiàn)亦然,這些部分有興趣的朋友可以自己網(wǎng)絡(luò)搜索,也可以自行跟蹤源碼查閱。
主要需求為每個(gè)線程單獨(dú)一個(gè)日志對(duì)象處理,可動(dòng)態(tài)控制日志輸出級(jí)別等參數(shù),可控制的同步及異步輸出模式,標(biāo)準(zhǔn)的日志頭格式等。
大體思路同我之前寫(xiě)的Log4j配置,只不過(guò)日志對(duì)象的創(chuàng)建管理等功能實(shí)現(xiàn)上略有區(qū)別。
二 對(duì)外暴露的接口
設(shè)計(jì)的重中之重,這部分一旦設(shè)計(jì)完成便不可輕易改動(dòng),后續(xù)的維護(hù)只能盡其可能的向前兼容。
首先我們需要提供對(duì)外可用的預(yù)設(shè)參數(shù)值,包括日志輸出等級(jí)、日志的輸出目標(biāo)(文件、控制臺(tái)、網(wǎng)絡(luò)及數(shù)據(jù)庫(kù)等)等,這些值設(shè)定我們均以static final來(lái)修飾。
第二部分是全局參數(shù)的設(shè)置,這些參數(shù)不隨日志對(duì)象的狀態(tài)而發(fā)生變動(dòng),反而是作為日志對(duì)象構(gòu)造的屬性值來(lái)源,比如說(shuō)日志文件的路徑、日志文件的最大容量、日志文件的備份數(shù)量,以及每個(gè)線程的日志對(duì)象初始的日志輸出等級(jí)等等。這部分參數(shù)可以從外部配置讀取,當(dāng)然也需要有默認(rèn)值設(shè)定,屬全局參數(shù)配置,以static修飾。
第三部分為日志輸出的接口定義,這部分可有可無(wú),但是對(duì)于大型項(xiàng)目來(lái)說(shuō)極為重要,以規(guī)范的接口進(jìn)行日志輸出,可以為日志的采集、分析帶來(lái)巨大便利,如通訊日志、異常日志等內(nèi)容的格式化輸出。
所以,按總體思路預(yù)先定義日志工具的外對(duì)暴露接口,如下:
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 } …… }
這里我暫定了一個(gè)公用的日志輸出接口名為log(),參數(shù)列表為日志的輸出等級(jí)、日志的類(lèi)型以及日志內(nèi)容。而通訊日志的輸出接口為logCommunication(),其參數(shù)列表暫時(shí)空著,按各位讀者的需求自行填寫(xiě),異常日志的輸出亦然。后文我會(huì)在測(cè)試部分對(duì)其填寫(xiě)。
為了描述方便,我僅定義了兩個(gè)日志輸出目標(biāo),一為控制臺(tái),二為文件。
三 代碼方式配置Log4j2日志對(duì)象
接下來(lái)是重頭戲,如果不采取配置文件的方式進(jìn)行Log4j2的配置,那么Log4j2會(huì)自行采用默認(rèn)的配置,這不是我們想要的。雖然這個(gè)過(guò)程我們無(wú)法選擇,也規(guī)避不了,但是我們最后都是使用Logger對(duì)象來(lái)進(jìn)行日志輸出的,所以我們只需要按需構(gòu)造Logger對(duì)象,并將其管理起來(lái)即可。
這里提供一個(gè)思路,首先LogUtil維護(hù)一組線程級(jí)日志對(duì)象,然后這一組線程級(jí)日志對(duì)象共同訪問(wèn)同一組Logger對(duì)象。
跟蹤源碼我發(fā)現(xiàn)Log4j2對(duì)Logger對(duì)象的構(gòu)造還是較為復(fù)雜的,使用了大量的Builder,其實(shí)較為早期的版本中也提供了構(gòu)造函數(shù)方式來(lái)初始化對(duì)象,但是后期的版本卻都被標(biāo)記了@depreciation。對(duì)于Builder模式大家自己查閱其他信息了解吧。
好消息是Log4j2的核心組件依然是Appender、Logger,只不過(guò)提供了更多的可配置內(nèi)容,包括日志同時(shí)按日期和文件大小進(jìn)行備份,這才是我想要的,之前寫(xiě)Log4j的時(shí)候我可是自己派生了一個(gè)Appender類(lèi)才實(shí)現(xiàn)的同時(shí)備份。
向控制臺(tái)輸出的Appender具體類(lèi)型為ConsoleAppender,我使用默認(rèn)的構(gòu)造函數(shù)來(lái)處理,這是唯一一個(gè)被公開(kāi)出來(lái),且沒(méi)有被@depreciation修飾的構(gòu)造函數(shù),想來(lái)只要是能通過(guò)默認(rèn)配置實(shí)現(xiàn)的都是被Log4j2認(rèn)可的吧,不然為啥要弄這么多Builder嘞。
向文件輸出的Appender我采用RollingFileAppender,無(wú)他,就是為了能夠?qū)崿F(xiàn)同時(shí)按日期及文件大小進(jìn)行備份,而且該Appender可適用全局異步模式,性能較AsyncAppender高了不少。它的創(chuàng)建方式要麻煩許多,因?yàn)槲倚枰O(shè)置觸發(fā)器來(lái)控制何時(shí)觸發(fā)備份,以及備份策略。
整體設(shè)計(jì)如下:
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; }
注意?。∥以诔跏蓟疞ogger對(duì)象的時(shí)候,是根據(jù)LogUtil是否開(kāi)啟了異步輸出模式來(lái)判定是否需要開(kāi)啟全局異步模式的,這里簡(jiǎn)單說(shuō)一些Log4j2的異步模式。
Log4j2提供了兩種異步模式,第一種是使用AsyncAppender,這種方式跟我以前寫(xiě)的Log4j的異步輸出模式一樣,都是單獨(dú)開(kāi)啟一個(gè)線程來(lái)輸出日志。第二種方式是AsychLogger,這個(gè)就厲害了,官推,而且官方提供了兩種使用模式,一個(gè)是混合異步,一個(gè)是全局異步,全局異步時(shí)不需要對(duì)配置文件進(jìn)行任何改動(dòng),盡在應(yīng)用啟動(dòng)時(shí)添加一個(gè)jvm參數(shù)即可,并且據(jù)壓測(cè)數(shù)據(jù)顯示,全局異步模式下性能飆升。
更多的配置信息建議讀者朋友自行查閱官方文檔。
最后還有一點(diǎn)需要注意的是,Log4j2的全局異步是要依賴(lài)隊(duì)列緩存的,其實(shí)現(xiàn)采用的是disruptor,所以需要依賴(lài)這個(gè)外部jar,不然在初始化Logger對(duì)象的時(shí)候,你會(huì)看到相關(guān)異常。
貼一下依賴(lài)的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è)置,不要同時(shí)出現(xiàn)?。?!
最最后,我特么還是要嘴賤啰嗦一下,不見(jiàn)得“異步”就是好的,你想想看異步模式無(wú)非是額外線程或者緩存來(lái)實(shí)現(xiàn),這些也是要吃資源的,日志量大的場(chǎng)景下其帶來(lái)的收益很高,但小量日志場(chǎng)景下其對(duì)性能資源的消耗很可能大于其帶來(lái)的性能收益,請(qǐng)酌情使用。
四 線程級(jí)日志對(duì)象的設(shè)計(jì)
按上文中的設(shè)計(jì)思路,LogUtil持有一組線程級(jí)日志對(duì)象,而這一組日志對(duì)象又共享一組Logger對(duì)象。延續(xù)Log4j版本的設(shè)計(jì),線程級(jí)日志對(duì)象類(lèi)型依然為T(mén)hreadLogger,其基礎(chǔ)屬性為線程ID、日志類(lèi)型以及日志的輸出級(jí)別等。
為了解決多并發(fā)問(wèn)題,使用ConcurrentHashMap來(lái)存儲(chǔ)Logger對(duì)象,其Key值為線程ID,這里需要注意的是ConcurrentHashMap的put操作盡管能保證可見(jiàn)性,但是不能保證操作的原子性,所以在其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)日志頭
頭部?jī)?nèi)容應(yīng)包含當(dāng)前的日志輸出級(jí)別,日志打印所在的類(lèi)名、行號(hào),當(dāng)前的線程號(hào)、進(jìn)程號(hào)等信息。這里簡(jiǎn)單介紹下進(jìn)程號(hào)及類(lèi)名行號(hào)的獲取。
每個(gè)應(yīng)用進(jìn)程的進(jìn)程號(hào)唯一,所以?xún)H在第一次獲取該信息時(shí)獲取即可,類(lèi)名行號(hào)則通過(guò)方法棧的棧信息獲取,如下:
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(); }
這里需要注意,類(lèi)名行號(hào)信息對(duì)于每一條日志都不盡相同,所以不能聲明為static,并且需要在LogUtil中獲取,并通過(guò)ThreadLogger的setLocation()方法傳入到ThreadLogger對(duì)象,最后由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)常被人問(wèn)起的一個(gè)問(wèn)題,很多從事400或大機(jī)開(kāi)發(fā)的同事轉(zhuǎn)入java開(kāi)發(fā)后,最常問(wèn)的就是異常堆棧的問(wèn)題,看不懂,不知道怎么來(lái)的。
這里我只提一個(gè)事情,Exception的構(gòu)造,其成員有兩個(gè),其一為cause,Throwable類(lèi)型,其二為message,String類(lèi)型,構(gòu)造函數(shù)提供較多,請(qǐng)大家自己做一個(gè)測(cè)試,看看不同構(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(); }
七 測(cè)試
補(bǔ)充一個(gè)邏輯實(shí)現(xiàn),這里我以異常日志的打印作為測(cè)試接口,并在多線程并發(fā)場(chǎng)景下實(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()); }
測(cè)試代碼:
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(); } }
測(cè)試結(jié)果:
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Mysql字段和java實(shí)體類(lèi)屬性類(lèi)型匹配方式
這篇文章主要介紹了Mysql字段和java實(shí)體類(lèi)屬性類(lèi)型匹配方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Java8如何將Array轉(zhuǎn)換為Stream的實(shí)現(xiàn)代碼
這篇文章主要介紹了Java8如何將Array轉(zhuǎn)換為Stream的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09springboot整合通用Mapper簡(jiǎn)化單表操作詳解
這篇文章主要介紹了springboot整合通用Mapper簡(jiǎn)化單表操作,通用Mapper是一個(gè)基于Mybatis,將單表的增刪改查通過(guò)通用方法實(shí)現(xiàn),來(lái)減少SQL編寫(xiě)的開(kāi)源框架,需要的朋友可以參考下2019-06-06利用spring aop實(shí)現(xiàn)動(dòng)態(tài)代理
這篇文章主要為大家詳細(xì)介紹了利用spring aop實(shí)現(xiàn)動(dòng)態(tài)代理的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03Java初學(xué)者問(wèn)題圖解(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)
本文通過(guò)圖文并茂的形式給大家介紹了java初學(xué)者問(wèn)題,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2017-04-04從0開(kāi)始學(xué)習(xí)大數(shù)據(jù)之java spark編程入門(mén)與項(xiàng)目實(shí)踐
這篇文章主要介紹了從0開(kāi)始學(xué)習(xí)大數(shù)據(jù)之java spark編程入門(mén)與項(xiàng)目實(shí)踐,結(jié)合具體入門(mén)項(xiàng)目分析了大數(shù)據(jù)java spark編程項(xiàng)目建立、調(diào)試、輸出等相關(guān)步驟及操作技巧,需要的朋友可以參考下2019-11-11