前言

前几天,在linux上部署一个war包应用时,tomcat直接起不来,查看tomcat的日志,catalina.out里没啥特别的,但是查看localhost日志,发现栈溢出了。

  1. [root@localhost logs]# vim localhost.2019-12-26.log
  2. 26-Dec-2019 16:27:31.811 INFO [localhost-startStop-1] org.apache.catalina.core.ApplicationContext.log No Spring WebApplicationInitializer types detected on classpath
  3. 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]
  4. java.lang.StackOverflowError
  5. at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:58)
  6. at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
  7. at org.apache.log4j.Category.<init>(Category.java:57)
  8. at org.apache.log4j.Logger.<init>(Logger.java:37)
  9. at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
  10. at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
  11. at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
  12. at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
  13. at org.apache.log4j.Category.<init>(Category.java:57)
  14. at org.apache.log4j.Logger.<init>(Logger.java:37)
  15. at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
  16. at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
  17. at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
  18. at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
  19. at org.apache.log4j.Category.<init>(Category.java:57)
  20. at org.apache.log4j.Logger.<init>(Logger.java:37)
  21. at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
  22. at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
  23. at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
  24. at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
  25. at org.apache.log4j.Category.<init>(Category.java:57)
  26. at org.apache.log4j.Logger.<init>(Logger.java:37)
  27. at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
  28. at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
  29. at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:66)
  30. at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
  31. at org.apache.log4j.Category.<init>(Category.java:57)
  32. 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依赖:

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.slf4j</groupId>
  4. <artifactId>slf4j-api</artifactId>
  5. <version>1.7.25</version>
  6. </dependency>
  7. </dependencies>

测试代码也很简单:

  1. import org.slf4j.Logger;
  2. import org.slf4j.LoggerFactory;
  3. public class Test {
  4. public static void main(String[] args) {
  5. Logger logger = LoggerFactory.getLogger(Test.class);
  6. logger.info("hahha");
  7. }
  8. }

output如下:

  1. SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
  2. SLF4J: Defaulting to no-operation (NOP) logger implementation
  3. 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依赖主要有:

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.slf4j</groupId>
  4. <artifactId>slf4j-api</artifactId>
  5. <version>1.7.24</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>ch.qos.logback</groupId>
  9. <artifactId>logback-classic</artifactId>
  10. <version>1.2.3</version>
  11. <scope>compile</scope>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.slf4j</groupId>
  15. <artifactId>slf4j-log4j12</artifactId>
  16. <version>1.7.25</version>
  17. </dependency>
  18. </dependencies>

这里面有slf4j的多个实现,logback和slf4j-log4j。同样的测试类,此时会输出:

  1. SLF4J: Class path contains multiple SLF4J bindings.
  2. //这里在logback-classic-1.2.3.jar包里找到了包名和类名全匹配的类
  3. 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]
  4. //在slf4j-log4j12也找到了
  5. 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]
  6. SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
  7. 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在前面)。

核心代码:

  1. slf4j-api包内:org.slf4j.LoggerFactory#bind
  2. private final static void bind() {
  3. try {
  4. //通过classLoader.getResources(org/slf4j/impl/StaticLoggerBinder.class)查找class文件
  5. Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
  6. //如果有多个,警告一下
  7. reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
  8. ...
  9. // StaticLoggerBinder就是前面说了半天的那个类,在slf4j-api里是不存在的,如果整个classpath下都没有,但这里又去调用其静态方法,会直接抛异常NoClassDefFoundError,被catch住;
  10. // 如果存在呢,就会使用classloader加载StaticLoggerBinder,但这个顺序没法保证,假设有logback-classic和slf4j-log4j两个实现,就看类加载器先加载哪个了
  11. StaticLoggerBinder.getSingleton();
  12. INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
  13. reportActualBinding(staticLoggerBinderPathSet);
  14. fixSubstituteLoggers();
  15. replayEvents();
  16. // release all resources in SUBST_FACTORY
  17. SUBST_FACTORY.clear();
  18. } catch (NoClassDefFoundError ncde) {
  19. String msg = ncde.getMessage();
  20. if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
  21. INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
  22. Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
  23. Util.report("Defaulting to no-operation (NOP) logger implementation");
  24. Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
  25. } else {
  26. failedBinding(ncde);
  27. throw ncde;
  28. }
  29. }
  30. }

大家可以看我的注释,

StaticLoggerBinder就是前面说了半天的那个类,在slf4j-api里是不存在的,如果整个classpath下都没有,但这里又去调用其静态方法,会直接抛异常NoClassDefFoundError,被catch住;

如果存在呢,就会使用classloader加载StaticLoggerBinder,但这个顺序没法保证,假设有logback-classic和slf4j-log4j两个实现,就看类加载器先加载哪个了

我这里说了一点,存在多个实现的时候,先加载哪个,后加载哪个,全看classloader。

大家可以实际测一下,一般来说,在windows下和linux下,会有不同的表现的,坑吧,谁让你进了这充满bug的行业呢,很多很奇怪的问题,都是classloader在不同OS下,获取到的jar包顺序不同导致的:

  1. public class Test {
  2. public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
  3. Logger logger = LoggerFactory.getLogger(Test.class);
  4. logger.info("hahha");
  5. //打印jar包的加载顺序
  6. ClassLoader loader = Test.class.getClassLoader();
  7. Method getURLs = loader.getClass().getMethod("getURLs");
  8. getURLs.setAccessible(true);
  9. URL[] o = (URL[]) getURLs.invoke(loader);
  10. for (URL url : o) {
  11. System.out.println(url);
  12. }
  13. }
  14. }

问题真相揭秘

回到问题,我仔细研究了一晚上,在本地复现了问题,可参考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依赖的:

  1. <dependency>
  2. <groupId>log4j</groupId>
  3. <artifactId>log4j</artifactId>
  4. </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. Streamy障碍一:大批量条目

  2. oracle函数 decode(条件,值1,翻译值1,值2,翻译值2,...值n,翻译值n,缺省值)

    [功能]根据条件返回相应值 [参数]c1, c2, ...,cn,字符型/数值型/日期型,必须类型相同或null 注:值1……n 不能为条件表达式,这种情况只能用case when then end解 ...

  3. oracle函数 NLS_INITCAP(x[,y])

    [功能]返回字符串并将字符串的第一个字母变为大写,其它字母小写; [参数]x字符型表达式 [参数]Nls_param可选, 查询数据级的NLS设置:select * from nls_database ...

  4. 阿里云POLARDB荣膺2019中国数据库年度最佳创新产品

    在日前的DTCC 2019(第十届中国数据库技术大会)上,阿里云自研云原生数据库POLARDB获选2019中国数据库——“年度最佳创新产品”. POLARDB是阿里云在2018年正式商业化的云原生数据 ...

  5. 【HAOI2015】树上染色

    [HAOI2015]树上染色 这题思路好神仙啊,首先显然是树形dp,f[i][j]表示在以i为根的子树中选j个黑点对答案的贡献(并不是当前子树最大值),dp时只考虑i与儿子连边的贡献.此时(i,son ...

  6. UISearchDisplayController “No Results“ cancel修改

    Recently I needed to fully customize a UISearchBar, so here are some basic "recipes" on ho ...

  7. LightOJ 1123 Trail Maintenance

    题意:n个城市m天.每一天修一条道路,输出当前天数的最小生成树,但是这里有一个条件,就是说最小生成树必须包括全部n个城市,否则输出-1 思路:边数有6000如果每一天跑一次最小生成树的话就接近O(m^ ...

  8. [ Laravel 5.1 文档 ] 服务 —— 帮助函数

    http://laravelacademy.org/post/205.html 1.简介 Laravel自带了一系列PHP帮助函数,很多被框架自身使用,然而,如果你觉得方便的话也可以在应用中随心所欲的 ...

  9. SQL 循环语句

    一.if语句 二.while语句 练习: 三.case when 四.练习 1. 2. 3. 4.

  10. Java一行代码可声明多个同类变量

    Java支持一句语句声明多个同类变量. Example: String a = "Hello", c = "hello"; int x = 5, y = 5;