前言
前幾天,在linux上部署一個war包應用時,tomcat直接起不來,查看tomcat的日志,catalina.out里沒啥特別的,但是查看localhost日志,發現棧溢出了。
[root@localhost logs]# vim localhost.2019-12-26.log
26-Dec-2019 16:27:31.811 INFO [localhost-startStop-1] org.apache.catalina.core.ApplicationContext.log No Spring WebApplicationInitializer types detected on classpath
26-Dec-2019 16:27:31.855 SEVERE [localhost-startStop-1] org.apache.catalina.core.StandardContext.listenerStart Exception sending context initialized event to listener instance of class [org.springframework.web.context.ContextLoaderListener]
java.lang.StackOverflowError
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:58)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
at org.apache.log4j.Category.<init>(Category.java:57)
at org.apache.log4j.Logger.<init>(Logger.java:37)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
at org.apache.log4j.Category.<init>(Category.java:57)
at org.apache.log4j.Logger.<init>(Logger.java:37)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
at org.apache.log4j.Category.<init>(Category.java:57)
at org.apache.log4j.Logger.<init>(Logger.java:37)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
at org.apache.log4j.Category.<init>(Category.java:57)
at org.apache.log4j.Logger.<init>(Logger.java:37)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
at org.apache.log4j.Category.<init>(Category.java:57)
at org.apache.log4j.Logger.<init>(Logger.java:37)
當時腦子昏得很,所幸搜索引擎上找到了解決辦法,
https://www.jb51.net/article/143486.htm
后邊呢,忙着改bug,沒時間細想,但總感覺還是有點糊里糊塗的。今天徹底排查了一下,清晰多了。
slf4j與其他日志的關系
slf4j,通俗來說,只是一個api類型的jar包,沒有定義實現;具體的日志實現框架有哪些呢,主要有log4j、logback,大家可以看看下面這個圖(懶得自己畫了,從前面那個鏈接里拿的):
這種接口和實現的關系,在軟件設計里,就是為了對上層提供統一的編程入口,比如,我們寫日志,只需要使用slf4j里的接口和類,而不用直接使用log4j/logback
等,方便替換;這個類似於java的spi
機制,比如,java官方定義jdbc接口,各廠商實現jdbc接口,提供出自己的驅動包,比如mysql-driver、oracle-driver等。
slf4j如何尋找自身的實現
在spi里,java.util.ServiceLoader
通過尋找當前線程類加載器路徑下的META-INF/services/接口的全名
來尋找實現類;
在slf4j里,則是通過如下機制,大家可以查看下面的源碼:
這個工程里,我們只添加了如下的maven依賴:
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
測試代碼也很簡單:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Test {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Test.class);
logger.info("hahha");
}
}
output如下:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
只要關注第一行即可,load class org.slf4j.impl.StaticLoggerBinder
失敗了。這個類,就是slf4j-api和slf4j-api的實現之間的粘合劑。
畫個圖:
我圖里說了,兩個包內,都是有org.slf4j.impl.StaticLoggerBinder
這個類的,我不騙大家,大家可以看下面這個module的源碼:
pom依賴主要有:
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.24</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
這里面有slf4j的多個實現,logback和slf4j-log4j。同樣的測試類,此時會輸出:
SLF4J: Class path contains multiple SLF4J bindings.
//這里在logback-classic-1.2.3.jar包里找到了包名和類名全匹配的類
SLF4J: Found binding in [jar:file:/D:/soft/Repo_backup/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
//在slf4j-log4j12也找到了
SLF4J: Found binding in [jar:file:/D:/soft/Repo_backup/org/slf4j/slf4j-log4j12/1.7.25/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: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
所以,slf4j-api的做法其實很原始,你曉得噻,classpath下通常有很多jar包,這里,slf4j-api就是去classpath下找全類名匹配org.slf4j.impl.StaticLoggerBinder
的class文件,找到幾個算幾個,一個沒有就報錯,多了就警告,然后隨便選一個(其實是看哪個class在前面)。
核心代碼:
slf4j-api包內:org.slf4j.LoggerFactory#bind
private final static void bind() {
try {
//通過classLoader.getResources(org/slf4j/impl/StaticLoggerBinder.class)查找class文件
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
//如果有多個,警告一下
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
...
// StaticLoggerBinder就是前面說了半天的那個類,在slf4j-api里是不存在的,如果整個classpath下都沒有,但這里又去調用其靜態方法,會直接拋異常NoClassDefFoundError,被catch住;
// 如果存在呢,就會使用classloader加載StaticLoggerBinder,但這個順序沒法保證,假設有logback-classic和slf4j-log4j兩個實現,就看類加載器先加載哪個了
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
} else {
failedBinding(ncde);
throw ncde;
}
}
}
大家可以看我的注釋,
StaticLoggerBinder就是前面說了半天的那個類,在slf4j-api里是不存在的,如果整個classpath下都沒有,但這里又去調用其靜態方法,會直接拋異常NoClassDefFoundError,被catch住;
如果存在呢,就會使用classloader加載StaticLoggerBinder,但這個順序沒法保證,假設有logback-classic和slf4j-log4j兩個實現,就看類加載器先加載哪個了
我這里說了一點,存在多個實現的時候,先加載哪個,后加載哪個,全看classloader。
大家可以實際測一下,一般來說,在windows下和linux下,會有不同的表現的,坑吧,誰讓你進了這充滿bug的行業呢,很多很奇怪的問題,都是classloader在不同OS下,獲取到的jar包順序不同導致的:
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Logger logger = LoggerFactory.getLogger(Test.class);
logger.info("hahha");
//打印jar包的加載順序
ClassLoader loader = Test.class.getClassLoader();
Method getURLs = loader.getClass().getMethod("getURLs");
getURLs.setAccessible(true);
URL[] o = (URL[]) getURLs.invoke(loader);
for (URL url : o) {
System.out.println(url);
}
}
}
問題真相揭秘
回到問題,我仔細研究了一晚上,在本地復現了問題,可參考module測試代碼:
這個module里,pom依賴如下:
堆棧,發現大概是這樣的,圖小可以單獨tab頁查看:
我這里也有對堆棧的文字解釋:
at org.apache.log4j.Category.
|
log4j-over-slf4j |
---|---|
at org.apache.log4j.Logger.
|
log4j-over-slf4j包,已經死循環了 |
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43) | log4j-over-slf4j包 |
at org.apache.log4j.LogManager.getLogger(LogManager.java:45) | log4j-over-slf4j,這個LogManager是log4j包里的,因為log4j-over-slf4j是一個log4j的冒充者,所以它也有這個類 |
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66) | slf4j-log4j12,實現了slf4j,進行了static bind的 |
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358) | slf4j-api |
at org.apache.log4j.Category.
|
log4j-over-slf4j |
at org.apache.log4j.Logger.
|
log4j-over-slf4j |
關於log4j-over-slf4j,可進一步閱讀:
https://blog.csdn.net/john1337/article/details/76152906
揭秘過程揭秘
真相可能足夠簡單,但是在找真相的過程反而更難一些,因為這個包,其實在windows下跑是沒問題的,在linux有問題,魔幻?
並不魔幻。因為這個war包里,本來是log4j依賴的:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
相當於:log4j-over-slf4j 和 log4j 共存,我們說了,log4j-over-slf4j里,提供了log4j的類,包名和類名都一樣,誰知道先加載哪一個呢?誰知道,windows下先加載哪個,linux下先加載哪個呢?這個就是要靠運氣的時候了,所以是偶現。
不信你把我們上面測試的module里,照下面這樣操作,windows下,立馬就好了
之前不記得采用這種方式來驗證,為此,還專門定義了一個自定義classloader,先加載slf4j-log4j jar包,再代理給parent,可參考:
總結
這個問題還是比較有意思的,我也還沒有完全弄懂,但大概了解了。
關於classloader在windows和linux下的不同表現的問題,可參考:
https://stackoverflow.com/questions/17324975/library-load-order-different-on-two-machines
博主之前也寫了一篇, 關於這個的:
關於日志,博主之前還寫了一篇:
面試題:應用中很多jar包,比如spring、mybatis、redis等等,各自用的日志系統各異,怎么用slf4j統一輸出?(上)
本次博客的相關代碼:
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/slf4j-log4j-stackoverflow-demo
大家有問題請留言,大家覺得有幫助,請點贊哦,這個也是對我的鼓勵。
謝謝大家!