前言

前几天,在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里,则是通过如下机制,大家可以查看下面的源码:

https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/slf4j-log4j-stackoverflow-demo/slf4j-no-binder

这个工程里,我们只添加了如下的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的源码:

https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/slf4j-log4j-stackoverflow-demo/slf4j-multi-binder

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测试代码:

https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/slf4j-log4j-stackoverflow-demo/slf4j-log4j-cycle-reference-exclude-log4j-in-pom

这个module里,pom依赖如下:

堆栈,发现大概是这样的,图小可以单独tab页查看:

我这里也有对堆栈的文字解释:

at org.apache.log4j.Category.(Category.java:57) log4j-over-slf4j
at org.apache.log4j.Logger.(Logger.java:37) 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.(Category.java:57) log4j-over-slf4j
at org.apache.log4j.Logger.(Logger.java:37) 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,可参考:

https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/slf4j-log4j-stackoverflow-demo/slf4j-log4j-cycle-reference-custom-classloader

总结

这个问题还是比较有意思的,我也还没有完全弄懂,但大概了解了。

关于classloader在windows和linux下的不同表现的问题,可参考:

https://stackoverflow.com/questions/17324975/library-load-order-different-on-two-machines

https://stackoverflow.com/questions/2179858/how-to-find-which-jars-and-in-what-order-are-loaded-by-a-classloader

博主之前也写了一篇, 关于这个的:

了不得,我可能发现了Jar 包冲突的秘密

关于日志,博主之前还写了一篇:

面试题:应用中很多jar包,比如spring、mybatis、redis等等,各自用的日志系统各异,怎么用slf4j统一输出?(上)

本次博客的相关代码:

https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/slf4j-log4j-stackoverflow-demo

大家有问题请留言,大家觉得有帮助,请点赞哦,这个也是对我的鼓励。

谢谢大家!

曹工改bug--这次,我遇到了一个难缠的栈溢出bug,还是日志相关的,真的难的更多相关文章

  1. 曹工改bug:cpu狂飙,old gc频繁,线程神秘死亡连环案件调查报告

    曹工改bug:cpu狂飙,old gc频繁,线程神秘死亡连环案件调查报告 前言 前两天,访问开发环境上一个java服务,发现一直转圈圈,因为我开着fiddler,可以看到的现象是,接口一直没返回:本来 ...

  2. 曹工改bug--本来以为很简单的数据库字段长度不足的问题,最后竟然靠抓包才解决

    问题描述 这两天本来忙着新功能开发,结果之前的一个项目最近要上了,然后又在测试,然后就喜提bug一枚(not mine),看bug描述,很简单,而且本地环境也重现了,只要刷入2000个英文字符就可以复 ...

  3. 曹工改bug:centos下,mongodb开机不能自启动,systemctl、rc.local都试了,还是不行,要不要放弃?

    问题背景 最近装个centos 7.6的环境,其中,基础环境包括,redis.nginx.mongodb.fastdfs.mysql等,其中,自启动使用的是systemctl,其他几个组件,都没啥问题 ...

  4. 曹工杂谈--使用mybatis的同学,进来看看怎么在日志打印完整sql吧,在数据库可执行那种

    前言 今天新年第一天,给大家拜个年,祝大家新的一年里,技术突突突,头发长长长! 咱们搞技术的,比较直接,那就开始吧.我给大家看看我demo工程的效果(代码下边会给大家的): 技术栈是mybatis/m ...

  5. 曹工说Spring Boot源码(11)-- context:component-scan,你真的会用吗(这次来说说它的奇技淫巧)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  6. 曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎

    曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎 写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean De ...

  7. 曹工说Spring Boot源码(28)-- Spring的component-scan机制,让你自己来进行简单实现,怎么办

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 曹工说Spring Boot源码(3)-- 手动注册Bean Definition不比游戏好玩吗,我们来试一下

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享 工程代码地址 思维导图地址 工程结构图: 大 ...

  9. 曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. POLARDB 2.0 重磅升级,分别支持Oracle与PostgreSQL

    点击订阅新品发布会! 新产品.新版本.新技术.新功能.价格调整,评论在下方,下期更新!关注更多内容,了解更多 最新发布 POLARDB 2.0 重磅升级 2019年6月19日15时,阿里云 POLAR ...

  2. 1、Ubuntu 16.04 安装.net core

    Register the Microsoft key register the product repository Install required dependencies 参考网址:https: ...

  3. 命名分组(?<name>....)

    捕获组分为: 普通捕获组(Expression) 命名捕获组(?Expression) 普通捕获组 从正则表达式左侧开始,每出现一个左括号"("记做一个分组,分组编号从 1 开始. ...

  4. vscode settings.json配置

    // 将设置放入此文件中以覆盖默认设置 { "editor.fontSize": 18, "editor.tabSize": 2, "editor.m ...

  5. 全文检索 java Lucene

    索引文件:[D:\luceneDemo\data\TXT小说\陛下是妻迷.txt] 大小:[1185.0 KB] 索引文件:[D:\luceneDemo\data\TXT小说\随身空间重生在七十年代. ...

  6. H3C 链路层协议

  7. jquery tab点击切换的问题

    问题: 页面结构见下 <div id="wrap"> <li> <a href="#" class="active&qu ...

  8. H3C ISDN DCC备份配置示例

  9. H3C ISDN BRI和PRI

  10. Activiti6-快速开始

    下载 https://www.activiti.org/download-links 快速开始 https://www.activiti.org/quick-start 用户指南 https://ww ...