Apache Common-Logging是廣泛使用的Java日誌門面庫。我以前一直都使用它和log4j編寫日誌。
Apache Common-Logging通過動態查找的機制,在程序運行時自動找出真正使用的日誌庫。
Apache Common-Logging一直都運作得很好。直到最近,我寫OSGI插件時,它不能工作了。
原因是Apache Common-Logging使用了ClassLoader尋找和載入底層的日誌庫。而OSGI中,不同的插件使用自己的ClassLoader。
一個線程的ClassLoader在執行不同的插件時,其執行能力是不同的。
OSGI的這種機制保證了插件互相獨立,然而確使Apache Common-Logging無法工作!
解決之道是使用新的日誌門面庫Slf4j。
Slf4j庫類似於Apache Common-Logging。但是,他在編譯時靜態綁定真正的Log庫。使用Slf4j時,如果你需要使用某一種日誌實現,那麼你必須選擇正確的Slf4j的jar包的集合。
這確實麻煩了一點,但總算可以在OSGI中開發日誌了。
其實,這一點點工作也不算麻煩。
使用CommonLog接口而實際由Slf4j和Log4j實現的過程
1,項目中照常使用
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
編寫日誌。
2,仍然在src下使用log4j.properties文件進行配置。
3,使用的所有jar文件:
1)log4j-1.2.15.jar 這是log4j的庫。 Slf4j並不改變這個底層實現庫。
2)slf4j-api-1.5.2.jar 這是Slf4j庫。
3)slf4j-log4j12-1.5.2.jar 這包含Log4j的適配器和靜態綁定log4j底層實現。
4)jcl-over-slf4j-1.5.2.jar 這提供了Commons-Logging接口,以及使用common-loggin的接口,底層還是由SLF4J來決定哪種實現機制 。
這裏,我們需要使用Log4j的原生庫,但是不需要Commons-Logging的原生庫。
OK,把上面這4個jar包複製到lib下,導入項目中,就可以像以往一樣繼續使用Apache Common-Logging編寫日誌了。
各種jar包總結
-
log4j1:
- log4j:log4j1的全部內容
-
log4j2:
- log4j-api:log4j2定義的API
- log4j-core:log4j2上述API的實現
-
logback:
- logback-core:logback的核心包
- logback-classic:logback實現了slf4j的API
-
commons-logging:
- commons-logging:commons-logging的原生全部內容
- log4j-jcl:commons-logging到log4j2的橋樑
- jcl-over-slf4j:commons-logging到slf4j的橋樑
-
slf4j轉向某個實際的日誌框架:
場景介紹:如 使用slf4j的API進行編程,底層想使用log4j1來進行實際的日誌輸出,這就是slf4j-log4j12乾的事。
- slf4j-jdk14:slf4j到jdk-logging的橋樑
- slf4j-log4j12:slf4j到log4j1的橋樑
- log4j-slf4j-impl:slf4j到log4j2的橋樑
- logback-classic:slf4j到logback的橋樑
- slf4j-jcl:slf4j到commons-logging的橋樑
-
某個實際的日誌框架轉向slf4j:
場景介紹:如 使用log4j1的API進行編程,但是想最終通過logback來進行輸出,所以就需要先將log4j1的日誌輸出轉交給slf4j來輸出,slf4j再交給logback來輸出。將log4j1的輸出轉給slf4j,這就是log4j-over-slf4j做的事
這一部分主要用來進行實際的日誌框架之間的切換(下文會詳細講解)
- jul-to-slf4j:jdk-logging到slf4j的橋樑
- log4j-over-slf4j:log4j1到slf4j的橋樑
- jcl-over-slf4j:commons-logging到slf4j的橋樑
3集成總結
3.1 commons-logging與其他日誌框架集成
-
1 commons-logging與jdk-logging集成:
需要的jar包:
- commons-logging
-
2 commons-logging與log4j1集成:
需要的jar包:
- commons-logging
- log4j
-
3 commons-logging與log4j2集成:
需要的jar包:
- commons-logging
- log4j-api
- log4j-core
- log4j-jcl(集成包)
-
4 commons-logging與logback集成:
需要的jar包:
- logback-core
- logback-classic
- slf4j-api、jcl-over-slf4j(2個集成包,可以不再需要commons-logging)
-
5 commons-logging與slf4j集成:
需要的jar包:
- jcl-over-slf4j(集成包,不再需要commons-logging)
- slf4j-api
3.2 slf4j與其他日誌框架集成
-
slf4j與jdk-logging集成:
需要的jar包:
- slf4j-api
- slf4j-jdk14(集成包)
-
slf4j與log4j1集成:
需要的jar包:
- slf4j-api
- log4j
- slf4j-log4j12(集成包)
-
slf4j與log4j2集成:
需要的jar包:
- slf4j-api
- log4j-api
- log4j-core
- log4j-slf4j-impl(集成包)
-
slf4j與logback集成:
需要的jar包:
- slf4j-api
- logback-core
- logback-classic(集成包)
-
slf4j與commons-logging集成:
需要的jar包:
- slf4j-api
- commons-logging
- slf4j-jcl(集成包)
4 日誌系統之間的切換
4.1 log4j無縫切換到logback
4.1.1 案例
我們已經在代碼中使用了log4j1的API來進行日誌的輸出,現在想不更改已有代碼的前提下,使之通過logback來進行實際的日誌輸出。
已使用的jar包:
- log4j
使用案例:
-
private static final Logger logger=Logger.getLogger(Log4jTest.class);
-
public static void main(String[] args){
-
if(logger.isInfoEnabled()){
-
logger.info("log4j info message");
-
}
-
}
上述的Logger是log4j1自己的org.apache.log4j.Logger,在上述代碼中,我們在使用log4j1的API進行編程
現在如何能讓上述的日誌輸出通過logback來進行輸出呢?
只需要更換一下jar包就可以:
- 第一步:去掉log4j jar包
-
第二步:加入以下jar包
- log4j-over-slf4j(實現log4j1切換到slf4j)
- slf4j-api
- logback-core
- logback-classic
- 第三步:在類路徑下加入logback的配置文件
原理是什麼呢?
4.1.2 切換原理
看下log4j-over-slf4j就一目瞭然了:
我們可以看到,這裏面其實是簡化更改版的log4j。去掉log4j1的原生jar包,換成該簡化更改版的jar包(可以實現無縫遷移)。
但是簡化更改版中的Logger和原生版中的實現就不同了,簡化版中的Logger實現如下(繼承了Category):
-
public class Category {
-
private String name;
-
protected org.slf4j.Logger slf4jLogger;
-
private org.slf4j.spi.LocationAwareLogger locationAwareLogger;
-
Category(String name) {
-
this.name = name;
-
slf4jLogger = LoggerFactory.getLogger(name);
-
if (slf4jLogger instanceof LocationAwareLogger) {
-
locationAwareLogger = (LocationAwareLogger) slf4jLogger;
-
}
-
}
-
}
從上面可以看到簡化版中的Logger內部是使用slf4j的API來生成的,所以我們使用的簡化版的Logger會委託給slf4j來進行輸出,由於當前類路徑下有logback-classic,所以slf4j會選擇logback進行輸出。從而實現了log4j到logback的日誌切換。
下面的內容就只講解日誌系統到slf4j的切換,不再講解slf4j選擇何種日誌來輸出
4.2 jdk-logging無縫切換到logback
4.2.1 案例
-
private static final Logger logger=Logger.getLogger(JulSlf4jLog4jTest.class.getName());
-
public static void main(String[] args){
-
logger.log(Level.INFO,"jul info a msg");
-
logger.log(Level.WARNING,"jul waring a msg");
-
}
可以看到上述是使用jdk-logging自帶的API來進行編程的,現在我們想這些日誌交給logback來輸出
解決辦法如下:
-
第一步:加入以下jar包:
- jul-to-slf4j (實現jdk-logging切換到slf4j)
- slf4j-api
- logback-core
- logback-classic
-
第二步:在類路徑下加入logback的配置文件
-
第三步:在代碼中加入如下代碼:
static{
SLF4JBridgeHandler.install();
}
4.2.2 切換原理
先來看下jul-to-slf4j jar包中的內容:
我們看到只有一個類:SLF4JBridgeHandler
它繼承了jdk-logging中定義的java.util.logging.Handler,Handler是jdk-logging處理日誌過程中的一個處理器(具體我也沒仔細研究過),在使用之前,必須要提前註冊這個處理器,即上述的SLF4JBridgeHandler.install()操作,install後我們就可以通過這個handler實現日誌的切換工作,如下:
-
protected Logger getSLF4JLogger(LogRecord record) {
-
String name = record.getLoggerName();
-
if (name == null) {
-
name = UNKNOWN_LOGGER_NAME;
-
}
-
return LoggerFactory.getLogger(name);
-
}
在處理日誌的過程中,使用了slf4j的原生方式LoggerFactory來獲取一個slf4j定義的Logger來進行日誌的輸出
而slf4j則又會選擇logback來進行實際的日誌輸出
4.3 commons-logging切換到logback
4.3.1 使用案例
使用的jar包
- commons-logging
案例如下:
-
private static Log logger=LogFactory.getLog(JulJclTest.class);
-
public static void main(String[] args){
-
if(logger.isTraceEnabled()){
-
logger.trace("commons-logging-jcl trace message");
-
}
-
}
可以看到我們使用commons-logging的API來進行日誌的編程操作,現在想切換成logback來進行日誌的輸出(這其實就是commons-logging與logback的集成)
解決辦法如下:
- 第一步:去掉commons-logging jar包(其實去不去都無所謂)
-
第二步:加入以下jar包:
- jcl-over-slf4j(實現commons-logging切換到slf4j)
- slf4j-api
- logback-core
- logback-classic
-
第三步:在類路徑下加入logback的配置文件
4.3.2 切換原理
這個原理之前都已經說過了,可以看下commons-logging與logback的集成
就是commons-logging通過jcl-over-slf4j 來選擇slf4j作爲底層的日誌輸出對象,而slf4j又選擇logback來作爲底層的日誌輸出對象。
4.4 常用的日誌場景切換解釋
上面把日誌的切換原理說清楚了,下面就針對具體的例子來進行應用
先來看下slf4j官方的一張圖:
下面分別詳細說明這三個案例
4.4.1 左上圖
-
現狀:
目前的應用程序中已經使用瞭如下混雜方式的API來進行日誌的編程:
- commons-logging
- log4j1
- jdk-logging
現在想統一將日誌的輸出交給logback
-
解決辦法:
-
第一步:將上述日誌系統全部無縫先切換到slf4j
- 去掉commons-logging(其實去不去都可以),使用jcl-over-slf4j將commons-logging的底層日誌輸出切換到slf4j
- 去掉log4j1(必須去掉),使用log4j-over-slf4j,將log4j1的日誌輸出切換到slf4j
- 使用jul-to-slf4j,將jul的日誌輸出切換到slf4j
-
第二步:使slf4j選擇logback來作爲底層日誌輸出
加入以下jar包:
- slf4j-api
- logback-core
- logback-classic
-
下面的2張圖和上面就很類似
4.4.2 右上圖
-
現狀:
目前的應用程序中已經使用瞭如下混雜方式的API來進行日誌的編程:
- commons-logging
- jdk-logging
現在想統一將日誌的輸出交給log4j1
-
解決辦法:
-
第一步:將上述日誌系統全部無縫先切換到slf4j
- 去掉commons-logging(其實去不去都可以),使用jcl-over-slf4j將commons-logging的底層日誌輸出切換到slf4j
- 使用jul-to-slf4j,將jul的日誌輸出切換到slf4j
-
第二步:使slf4j選擇log4j1來作爲底層日誌輸出
加入以下jar包:
- slf4j-api
- log4j
- slf4j-log4j12(集成包)
-
4.4.3 左下圖
-
現狀:
目前的應用程序中已經使用瞭如下混雜方式的API來進行日誌的編程:
- commons-logging
- log4j
現在想統一將日誌的輸出交給jdk-logging
-
解決辦法:
-
第一步:將上述日誌系統全部無縫先切換到slf4j
- 去掉commons-logging(其實去不去都可以),使用jcl-over-slf4j將commons-logging的底層日誌輸出切換到slf4j
- 去掉log4j1(必須去掉),使用log4j-over-slf4j,將log4j1的日誌輸出切換到slf4j
-
第二步:使slf4j選擇jdk-logging來作爲底層日誌輸出
加入以下jar包:
- slf4j-api
- slf4j-jdk14(集成包)
-
5 衝突說明
仍然是這裏的內容slf4j官網的衝突說明
其實明白上面介紹的各jar包的作用,就很容易理解
5.1 jcl-over-slf4j 與 slf4j-jcl 衝突
-
jcl-over-slf4j: commons-logging切換到slf4j
-
slf4j-jcl : slf4j切換到commons-logging
如果這兩者共存的話,必然造成相互委託,造成內存溢出
5.2 log4j-over-slf4j 與 slf4j-log4j12 衝突
- log4j-over-slf4j : log4j1切換到slf4j
- slf4j-log4j12 : slf4j切換到log4j1
如果這兩者共存的話,必然造成相互委託,造成內存溢出。但是log4j-over-slf4內部做了一個判斷,可以防止造成內存溢出:
即判斷slf4j-log4j12 jar包中的org.slf4j.impl.Log4jLoggerFactory是否存在,如果存在則表示衝突了,拋出異常提示用戶要去掉對應的jar包,代碼如下,在slf4j-log4j12 jar包的org.apache.log4j.Log4jLoggerFactory中:
5.3 jul-to-slf4j 與 slf4j-jdk14 衝突
- jul-to-slf4j : jdk-logging切換到slf4j
- slf4j-jdk14 : slf4j切換到jdk-logging
如果這兩者共存的話,必然造成相互委託,造成內存溢出
6 結束語
至此,這個日誌系列就算終於完成了。它注重於日誌系統之間的交互與集成,所以想深入研究單個日誌系統的架構的話,就需要各位自行去深入研究了。