javascript
log4j 禁止类输出日志_SpringBoot统一日志处理原理
閱讀推薦
程序員跳槽時機(jī)已到,閑聊中面試官無意泄題
SpringBoot作為日常開發(fā)利器,開箱即用,大量的star等已經(jīng)成為節(jié)省開發(fā)的重要框架之一,但是各個框架的star中引入的日志框架卻不盡相同,有的是log4j,有的是slf4j,這導(dǎo)致我們在引入多個框架的star的時候,往往會引入多個日志框架,每一個日志框架彼此效率不盡相同,那么我們能不能做到在項目中僅引入一個統(tǒng)一的日志框架呢?本篇我們就來探索SpringBoot如何實現(xiàn)統(tǒng)一日志操作
為什么需要日志
首先我們需要明白,日志的作用是什么--即用來在程序運(yùn)行過程中,將我們需要的信息打印出來,便于我們在調(diào)試中查找和觀察。在JAVA中存在很多常見的日志框架,如JUL、JCL、Jboss-logging、log4j、logback、slf4j等,這么多日志框架,我們該如何選擇?
日志門面與日志實現(xiàn)
在日志框架選型之前,我們先了解一個概念,什么是日志門面?日志門面,不是具體的日志解決方案,它只服務(wù)于各種各樣的日志系統(tǒng),允許最終用戶在部署其應(yīng)用時使用其所希望的日志實現(xiàn)來使用日志功能。而日志實現(xiàn)則是基于對應(yīng)的日志門面的規(guī)范來實現(xiàn)的具體日志功能的框架,常見的日志門面與日志實現(xiàn)關(guān)系如下:
每一種日志框架輸出信息的效率也不盡相同,而我們?nèi)粘i_發(fā)使用的框架中往往都會引入一個日志框架來輔助輸出框架信息,然而框架之間由于歷史迭代原因及框架性能等問題,選擇的日志框架也不一樣,常見的框架與默認(rèn)選擇的日志系統(tǒng)關(guān)系如下:
由于歷史迭代原因,JCL和jboss-logging日志框架,基本已經(jīng)很久沒有更新了,不太適合作為現(xiàn)在框架的主流選擇,那么剩下的選擇中log4j、slf4j是使用最多的,然而由于log4j的輸出性能問題,log4j的作者選擇重新編寫了一個日志門面--Slf4j,并且編寫了基于Slf4j的日志實現(xiàn)--logback,其輸出信息的效率遠(yuǎn)超log4j,解決了log4j遺留下的性能問題,所以在SpringBoot框架中,默認(rèn)也選擇了Slf4j來作為默認(rèn)日志框架
slf4j的使用
現(xiàn)在,我們來看看slf4j的使用,引入maven依賴:
org.slf4j slf4j-api 1.7.28按照slf4j官方的說法,,日志記錄方法的調(diào)用,不應(yīng)該來直接調(diào)用日志的實現(xiàn)類,而是調(diào)用日志抽象層里面的實現(xiàn)方法,獲取通過日志工廠創(chuàng)建的日志實例,即可輸出對應(yīng)的日志:
import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class HelloWorld { public static void main(String[] args) { Logger logger = LoggerFactory.getLogger(HelloWorld.class);[圖片上傳中...(slf4j日志輸出過程.png-6f5073-1583207284091-0)] logger.info("Hello World"); }}這里我們注意到了一點(diǎn),使用slf4j的輸出日志的時候,我們也引入了logback這個基于slf4j日志門面實現(xiàn)的具體日志輸出框架,如果不指定具體的日志輸出實現(xiàn),將會找不到具體的日志輸出實例,slf4j的日志輸出過程如圖所示:
slf4j日志輸出過程
從圖中可以看到,應(yīng)用程序調(diào)用了slf4j的api接口以后,具體的實現(xiàn)則是由slf4j日志門面找到對應(yīng)的日志的系統(tǒng)來實現(xiàn)日志輸出
解決多框架日志不統(tǒng)一問題
現(xiàn)在我們再回到日志統(tǒng)一的問題上,前面已經(jīng)了解了,開發(fā)常用的框架,如Spring、mybatis等使用的框架都是框架開發(fā)者自己選擇的,如果我們每個框架就引入一個日志系統(tǒng),并且最終需要打印日志的時候,會出現(xiàn)使用n種日志系統(tǒng)平臺,并且每一種的日志打印的格式、內(nèi)容和性能都需要手動控制,不僅讓項目變大,而且增大了項目復(fù)雜度,對性能也有很大的影響,那么我們該如何讓所有的開源框架統(tǒng)一使用Slf4j來輸出呢?我們來看下slf4j官方給我們的方案,如圖所示:
sfl4j適配日志
從圖中我們可以看出來,官方的方案是針對不同的日志框架,開發(fā)了一套適配兼容的框架與之對應(yīng),使用這些兼容jar來替代原來的日志框架即可,例如log4j日志框架,與之對應(yīng)的就是log4j-over-slf4j.jar,并且常見的日志框架,slf4j團(tuán)隊都實現(xiàn)了一套與之對應(yīng)的基于slf4j的兼容框架,關(guān)系如下:
日志框架slf4j兼容框架log4jlog4j-over-slf4jcommons loggingjcl-over-slf4jjava.util.loggingjui-to-slf4j
SpringBoot如何處理日志關(guān)系
在使用SpringBoot的時候,我們會發(fā)現(xiàn)官方默認(rèn)使用的是spring‐boot‐starter‐logging這個starter來引入日志系統(tǒng)的,我們展開該依賴的依賴圖,如下:
SpringBoot處理日志關(guān)系
可以看到spring‐boot‐starter‐logging這個starter中,引入了四個日志實例的依賴,分別是logback和我們前面提到的日志兼容jar的依賴,并且最終引入了slf4j的日志門面的依賴,實現(xiàn)了統(tǒng)一日志處理。但是為什么兼容jar引入后就能解決日志輸出的問題呢?難道兼容包有什么神奇的黑科技嗎?其實不然,我們隨便展開其中的幾個兼容日志jar的包名,如圖:
日志兼容包的包名關(guān)系
原來這些日志兼容包的包名與原來的日志框架的包名完全一樣,并且完全按照slf4j的方式實現(xiàn)了一套和以前一樣的API,這樣依賴這些日志框架的開源框架在運(yùn)行的時候查找對應(yīng)包名下的class也不會報錯,但熟悉java類加載機(jī)制的都知道,兩個jar的包名以及使用的class都一樣的話,加載會出現(xiàn)異常,我們進(jìn)入spring‐boot‐starter‐logging的pom依賴中一探究竟,最后在maven依賴中發(fā)現(xiàn)了端倪,如Spring框架使用的是commons-logging,而在spring-boot-starter-logging中,將spring的日志依賴排除,如下:
org.springframework spring‐core commons‐logging commons‐logging這樣spring框架在運(yùn)行時使用的時候,使用的就是兼容jar中的日志實例了,SpringBoot成功的完成了一次日志系統(tǒng)統(tǒng)一的偷天換日操作。
slf4j的橋接原理
通過查看SpringBoot的日志處理,我們可以大致總結(jié)如下幾步操作:
1、將系統(tǒng)中其他日志框架先排除出去;2、用中間包來替換原有的日志框架;3、我們導(dǎo)入slf4j其他的實現(xiàn)
通過以上的操作,即可完成日志系統(tǒng)的統(tǒng)一,但是我們開始有了新的疑惑,slf4j是怎么做到的自動查找對應(yīng)的實現(xiàn)日志,并且完成了日志的正常打印操作的呢?這個就要涉及到slf4j的橋接原理,我們先來看看slf4j源碼中關(guān)于日志調(diào)用相關(guān)的代碼:
//slf4j日志調(diào)用過程相關(guān)的代碼//根據(jù)名稱獲取日志實例public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name);}//獲取日志實例工廠并且完成日志實例的查找與初始化操作 public static ILoggerFactory getILoggerFactory() { if (INITIALIZATION_STATE == UNINITIALIZED) { INITIALIZATION_STATE = ONGOING_INITIALIZATION; //查找實現(xiàn)類 performInitialization(); } ... return StaticLoggerBinder.getSingleton().getLoggerFactory(); ... }可以看到整個過程中是通過StaticLoggerBinder.getSingleton() 來進(jìn)行初始化日志工廠操作,而StaticLoggerBinder這個類是從哪來的呢?我們發(fā)現(xiàn)StaticLoggerBinder類并不存在于slf4j的jar中,而是通過查找org/slf4j/impl/StaticLoggerBinder.class類的路徑來發(fā)現(xiàn)具體的實現(xiàn)類,代碼如下:
//設(shè)置默認(rèn)的查找日志實例的StaticLoggerBinder路徑private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";private static Set findPossibleStaticLoggerBinderPathSet() { ....... paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); ......}這個時候我們就該思考一個問題,如果我們同時存在了多個StaticLoggerBinder 時會加載哪一個呢?熟悉java類加載機(jī)制可知,類加載器會按照一定的順序逐個掃描jar包目錄并且加載出來,所以先被類加載器掃描的StaticLoggerBinder會優(yōu)先被加載,具體的加載順序如下:
1.$java_home/lib 目錄下的java核心api
2.$java_home/lib/ext 目錄下的java擴(kuò)展jar包
3.java -classpath/-Djava.class.path所指的目錄下的類與jar包
4.$CATALINA_HOME/common目錄下按照文件夾的順序從上往下依次加載
5.$CATALINA_HOME/server目錄下按照文件夾的順序從上往下依次加載
6.$CATALINA_BASE/shared目錄下按照文件夾的順序從上往下依次加載
7.項目/WEB-INF/classes下的class文件
8.項目/WEB-INF/lib下的jar文件
根據(jù)slf4j橋接原理改造logger
我們都知道平時使用slf4j輸出日志的時候往往獲取Logger實例來進(jìn)行日志打印,但是Logger僅僅支持本地日志,不支持分布式環(huán)境的日志,而在slfj中有LogBean實例,可以支持分布式日志,包含了鏈路相關(guān)信息,那么我們是否可以改造slf4j的橋接過程,使得我們可以靈活的使用本地日志或者分布式日志呢?首先我們先看看我們需要實現(xiàn)的需求:
想要實現(xiàn)這個功能,有以下兩個思路實現(xiàn):
1.我們通過自定義appender,基于logback的appender進(jìn)行擴(kuò)展,可以實現(xiàn)分別輸出本地日志以及分布式日志,但是缺陷在于appender擴(kuò)展性不高,很多參數(shù)信息獲取不到,例如上下文信息等
2.我們通過實現(xiàn)Logger接口,用來將Logger和LogBean聚合在一起,從而實現(xiàn)LogBean集成到Logger中,同樣此種方式的缺陷在于對于第三方框架日志,我們無能為力,無法直接替換使用,并且在使用的時候需要使用自定義的LogFactory
第一種思路我們可以看出來,局限性太高,靈活度不夠,接下來我們嘗試使用第二種方案,實現(xiàn)聚合Logger和LogBean,對外公開統(tǒng)一的api進(jìn)行日志輸出使用:
public class CustomLogger implements LocationAwareLogger { private Logger logger; //提供getLogger方法獲取logger public static LoggerFacade getLogger(Class clazz) { LoggerFacade loggerFacade = new LoggerFacade(); loggerFacade.logger = LoggerFactory.getLogger(clazz); return loggerFacade; } ... //打印本地日志的同時 輸出到logbean中 @Override public void warn(String msg) { logger.warn(msg); appendExtra(msg, Level.WARN); } ...... public void appendExtra(String str, Level level) { String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"); //獲取上下文,通過上下文判斷,如果存在則獲取分布式環(huán)境的LogBean實例 ThreadContext threadContext = ContextContainer.retrieveServiceContext(); if (threadContext != null) { LogBean logBean = threadContext.getLogBean(); if (logBean != null) { logBean.getInner().getExtra().add(date + " " + level.toString() + " " + simpleName(getName()) + " -" + " " + str); } } }}接下來我們可以替換slf4j的實現(xiàn),修改為我們自定義的CustomerLogger,內(nèi)部調(diào)用logback的日志本地輸出,而通過前面橋接原理可以知道,slf4j具體橋接獲取實例的過程是通過LoggerFactory來獲取,那么我們來嘗試修改LoggerFactory的代碼實現(xiàn)替換為CustomerLogger實例:
public class CustomLoggerFactory implements ILoggerFactory { private static CustomLoggerFactory customLoggerFactory; public static CustomLoggerFactory getInstance(LoggerContext loggerContext) { if (customLoggerFactory == null) { customLoggerFactory = new CustomLoggerFactory(loggerContext); } return customLoggerFactory; } //logback的LoggerFactory實現(xiàn) private LoggerContext loggerContext; public CustomLoggerFactory(LoggerContext loggerContext) { this.loggerContext = loggerContext; } //返回CustomLogger @Override public Logger getLogger(String name) { ch.qos.logback.classic.Logger logger = loggerContext.getLogger(name); return CustomLogger.getLogger(logger); } public LoggerContext getLoggerContext() { return loggerContext; } @Override public ILoggerFactory getLoggerFactory() { if (!initialized) { return defaultLoggerContext; } if (contextSelectorBinder.getContextSelector() == null) { throw new IllegalStateException( "contextSelector cannot be null. See also " + NULL_CS_URL); } LoggerContext loggerContext = contextSelectorBinder.getContextSelector().getLoggerContext(); return CustomLoggerFactory.getInstance(loggerContext); }}由以上替換后,項目中通過LoggerFactory獲取的到logger對象 就替換成了CustomLogger對象了,從而實現(xiàn)了降低侵入,將Logger與LogBean整合的效果
結(jié)語
Hi~ o( ̄▽ ̄)ブ ,整理了約100G的面試、學(xué)習(xí)資料,但是呢篇幅有限。若你有此需求,那便可免費(fèi)分享下載,在簡信發(fā)送“面試”或 點(diǎn)擊此鏈接獲取資源下載方式下載吧。
網(wǎng)盤上百G資源
java面試題詳解
java視頻及資料
總結(jié)
以上是生活随笔為你收集整理的log4j 禁止类输出日志_SpringBoot统一日志处理原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java ee ide 添加spring
- 下一篇: jpa原生query_Spring Da