每一個(gè)Java程序員都知道日志對(duì)于任何一個(gè)Java應(yīng)用程序,尤其是服務(wù)端程序是至關(guān)重要的,而很多程序員也已經(jīng)熟悉各種不同的日志庫(kù)如java.util.logging、Apache log4j、logback。
- 我強(qiáng)烈建議,在java中任何新的代碼開(kāi)發(fā),都應(yīng)使用SLF4J而不是任何的日志API,包括log4J。
本文將解釋為什么使用SLF4J比log4j或者java.util.logging要優(yōu)秀?
一、抽象
SLF4J(Simple logging Facade for Java)不是一個(gè)真正的日志實(shí)現(xiàn),而是一個(gè)抽象層( abstraction layer),它允許你在后臺(tái)使用任意一個(gè)日志類庫(kù)。如果是在編寫(xiě)供內(nèi)外部都可以使用的API或者通用類庫(kù),那么你真不會(huì)希望使用你類庫(kù)的客戶端必須使用你選擇的日志類庫(kù)。
如果一個(gè)項(xiàng)目已經(jīng)使用了log4j,而你加載了一個(gè)類庫(kù),比方說(shuō) Apache Active MQ——它依賴于于另外一個(gè)日志類庫(kù)logback,那么你就需要把它也加載進(jìn)去。但如果Apache Active MQ使用了SLF4J,你可以繼續(xù)使用你的日志類庫(kù)而無(wú)語(yǔ)忍受加載和維護(hù)一個(gè)新的日志框架的痛苦。
總的來(lái)說(shuō),SLF4J使你的代碼獨(dú)立于任意一個(gè)特定的日志API,這是一個(gè)對(duì)于開(kāi)發(fā)API的開(kāi)發(fā)者很好的思想。雖然抽象日志類庫(kù)的思想已經(jīng)不是新鮮的事物而且Apache commons logging也已經(jīng)在使用這種思想了,但現(xiàn)在SLF4J正迅速成為Java世界的日志標(biāo)準(zhǔn)。
二、占位符
在代碼中表示為{}的特性。占位符是一個(gè)非常類似于在String的format()方法中的%s,它會(huì)在運(yùn)行時(shí)被某個(gè)提供的實(shí)際字符串所替換。
這不僅降低了你代碼中字符串連接次數(shù),而且還節(jié)省了新建的String對(duì)象。
因?yàn)镾tring對(duì)象是不可修改的并且它們建立在一個(gè)String池中,它們消耗堆內(nèi)存( heap memory)而且大多數(shù)時(shí)間他們是不被需要的,例如當(dāng)你的應(yīng)用程序在生產(chǎn)環(huán)境以ERROR級(jí)別運(yùn)行時(shí)候,一個(gè)String使用在DEBUG語(yǔ)句就是不被需要的。
通過(guò)使用SLF4J,你可以在運(yùn)行時(shí)延遲字符串的建立,這意味著只有需要的String對(duì)象才被建立。而如果你已經(jīng)使用log4j,那么你已經(jīng)對(duì)于在if條件中使用debug語(yǔ)句這種變通方案十分熟悉了,但SLF4J的占位符就比這個(gè)好用得多。
Log4j:
if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}
SLF4J:
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
在SLF4J,我們不需要字符串連接而且不會(huì)導(dǎo)致暫時(shí)不需要的字符串消耗。而是以一個(gè)以占位符和以參數(shù)傳遞實(shí)際值的模板格式下寫(xiě)日志信息。你可能會(huì)在想萬(wàn)一我有很多個(gè)參數(shù)怎么辦?那么你可以選擇使用變量參數(shù)版本的日志方法或者用以O(shè)bject數(shù)組傳遞。這是一個(gè)相當(dāng)?shù)姆奖愫透咝Х椒ǖ拇蛉罩痉椒?。記住,在生產(chǎn)最終日志信息的字符串之前,這個(gè)方法會(huì)檢查一個(gè)特定的日志級(jí)別是不是打開(kāi)了,這不僅降低了內(nèi)存消耗而且預(yù)先降低了CPU去處理字符串連接命令的時(shí)間。這里是使用SLF4J日志方法的代碼,來(lái)自于slf4j-log4j12-1.6.1.jar中的Log4j的適配器類Log4jLoggerAdapter。
public void debug(String format, Object arg1, Object arg2) {
if (logger.isDebugEnabled()) {
FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
}
}
三、原因總結(jié)
- 在你的開(kāi)源或內(nèi)部類庫(kù)中使用SLF4J會(huì)使得它獨(dú)立于任何一個(gè)特定的日志實(shí)現(xiàn),這意味著不需要管理多個(gè)日志配置或者多個(gè)日志類庫(kù),你的客戶端會(huì)很感激這點(diǎn)。
- SLF4J提供了基于占位符的日志方法,這通過(guò)去除檢查isDebugEnabled(), isInfoEnabled()等等,提高了代碼可讀性。
- 通過(guò)使用SLF4J的日志方法,你可以延遲構(gòu)建日志信息(Srting)的開(kāi)銷,直到你真正需要,這對(duì)于內(nèi)存和CPU都是高效的。
- 作為附注,更少的暫時(shí)的字符串意味著垃圾回收器(Garbage Collector)需要做更好的工作,這意味著你的應(yīng)用程序有為更好的吞吐量和性能。
再次強(qiáng)調(diào),在java中任何新的代碼開(kāi)發(fā),都應(yīng)使用SLF4J而不是任何的日志API,包括log4J。
四、slf4j與log4j常見(jiàn)沖突
4.1. 問(wèn)題一
描述
java.lang.IllegalAccessError: tried to access field org.slf4j.impl.Static..
java.lang.IllegalAccessError: tried to access field org.slf4j.impl.StaticLoggerBinder.SINGLETON from class org.slf4j.LoggerFactory
問(wèn)題原因:jar文件版本沖突
類 org.slf4j.impl.StaticLoggerBinder在slf4j-api 中是類的公有靜態(tài)變量:
public static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
而在slf4j-log4j12(slf4j-nop.jar, slf4j-simple.jar, slf4j-log4j12.jar, slf4j-jdk14.jar or logback-classic.jar其中之一)中確是私有變量:
private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
解決方案:
修改slf的源代碼,將這個(gè)變量有私有改為公有,再打包,問(wèn)題可解決。
slf4j-api.jar 刪除,再導(dǎo)入同版本的slf4j-api-1.5.6.jar 和slf4j-log4j12-1.5.6.jar ,問(wèn)題可解決。
4.2 問(wèn)題二
問(wèn)題描述:
log4j:WARN No appenders could be found for logger (xxx.yyy.zzz).
log4j:WARN Please initialize the log4j system properly.
問(wèn)題解決:
在src下面新建file名為log4j.properties內(nèi)容如下:
# Configure logging for testing: optionally with log file
log4j.rootLogger=WARN, stdout
# log4j.rootLogger=WARN, stdout, logfile
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
其他情形下的問(wèn)題解決:
在Eclipse中開(kāi)發(fā)相關(guān)項(xiàng)目時(shí),在控制臺(tái)經(jīng)??吹饺缦滦畔?
log4j:WARN No appenders could be found for logger
log4j:WARN Please initialize the log4j system properly.
此處輸出信息并不是錯(cuò)誤信息而僅只是警告信息,因?yàn)閘og4j無(wú)法輸出日志,log4j是一個(gè)日志輸入軟件包??梢詫truts或Hibernate等壓縮包解壓,內(nèi)有l(wèi)og4j.properties文件,將它復(fù)制到項(xiàng)目src文件夾或?qū)og4j.properties放到 \WEB-INF\classes文件夾中即可。
4.3 問(wèn)題三
問(wèn)題描述
log4j:WARN No appenders could be found for logger (org.springframework.web.context.ContextLoader).
log4j:WARN Please initialize the log4j system properly.
問(wèn)題解決
在網(wǎng)上查了一下,多是說(shuō)把ContextLoaderListener改為SpringContextServlet,但我這樣改了沒(méi)用。后來(lái)在一個(gè)英文網(wǎng)站上看到一個(gè)遇到同樣問(wèn)題的帖子,他是這樣改的:
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/config/log4j.properties</param-value>
</context-param>
<!-- 定義LOG4J監(jiān)聽(tīng)器 -->
<listener>
<listener-class>
org.springframework.web.util.Log4jConfigListener
</listener-class>
</listener>
這樣改了問(wèn)題就解決了,不用再修改ContextLoaderListener。