log4j與logback包衝突原因及解決,不可忽視的Warning
場景
一個簡單的spring-boot程式,需要用kafka做訊息佇列,於是在maven中引入kafka依賴,一切看似沒問題,在啟動時,打印出Warning資訊:
SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/xxx/learning-slf4j-multiple-bindings/WEB-INF/lib/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [jar:file:/xxx/learning-slf4j-multiple-bindings/WEB-INF/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
原因分析
通過警告訊息,可以簡單的看出是slf4j
繫結發生問題,有多個StaticLoggerBinder.class
存在,即slf4j-log4j12
和logback-classic
衝突。
- 疑惑點1是我並沒有手動引入
slf4j-log4j12
依賴,依賴jar
包是被自動引入的,通過maven自帶工具分析依賴路徑,可以看出是kafka
依賴於slf4j-log4j12
,自動匯入的依賴包。 - 日誌繫結的機制分析
從日誌物件開始探究
slf4j
的繫結方式。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; ··· private final Logger logs = LoggerFactory.getLogger(***.class);
LoggerFactory.getLogger()
方法:(下述均只保留關鍵邏輯程式碼 )
public static Logger getLogger(Class<?> clazz) { Logger logger = getLogger(clazz.getName()); ··· return logger; } public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory();//看這裡 return iLoggerFactory.getLogger(name);//根據名字返回一個Logger例項物件 }
ILoggerFactory
是一個介面,歸屬package org.slf4j;
僅存在一個方法為:
public Logger getLogger(String name);
接下來就是看看getILoggerFactory()
的真面目:
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();//看這裡
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
可以看到performInitialization()
是進行初始化的方法:
private final static void performInitialization() {
bind();
···
}
performInitialization()
內部呼叫bind()
方法:
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();//看這裡
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError ncde) {...
} catch (java.lang.NoSuchMethodError nsme) {...
} catch (Exception e) {...}
}
其中關鍵在於findPossibleStaticLoggerBinderPathSet()
方法,終於到了查詢繫結相關的部分內容,可以看到是查詢所有的"org/slf4j/impl/StaticLoggerBinder.class"
類並載入,同時while迴圈
裡,將可能存在的多個StaticLoggerBinder.class
路徑均加入Set<URL>
返回。
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {//看這裡
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {//看這裡
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
返回到bind()
方法中:
private final static void bind() {
···
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);//看這裡
···
}
···
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) {
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}
這裡可以看到reportMultipleBindingAmbiguity()
裡判斷是否發生多重繫結,就是列印文章開頭Warning
資訊的地方。
成功載入StaticLoggerBinder
後,在bind()
方法中呼叫其getSingleton()
方法得到單例,並修改INITIALIZATION_STATE
狀態,至此完成日誌框架的繫結。
private final static void bind() {
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
StaticLoggerBinder.getSingleton();//看這裡
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
···
}
最後附上slf4j-api-1.7.25.jar
和logback-classic-1.2.3.jar
的目錄結構供參考:
解決方案
分析了原因,那麼解決方案自然很簡單,就是剔除不需要的依賴包,此處就是在kafka
的依賴中剔除slf4j-log4j12
。maven
專案中可以通過exclusions
標籤來完成。
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>0.10.0.1</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
本文就簡單分析了日誌載入繫結的過程,如有遺漏請不吝指出。