详解log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析

注:下文中的“桥接”、“转调”、“绑定”等词基本都是同一个概念。

log4j-over-slf4j和slf4j-log4j12是跟java日志系统相关的两个jar包,当它们同时出现在classpath下时,就可能会引起堆栈溢出异常。异常信息大致如下(摘自slf4j官网文档Detected both log4j-over-slf4j.jar AND slf4j-log4j12.jar on the class path, preempting StackOverflowError):

Exception in thread "main" java.lang.StackOverflowError
at java.util.Hashtable.containsKey(Hashtable.java:306)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:36)
at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
at org.apache.log4j.Category.<init>(Category.java:53)
at org.apache.log4j.Logger..<init>(Logger.java:35)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
at org.apache.log4j.Category..<init>(Category.java:53)
at org.apache.log4j.Logger..<init>(Logger.java:35)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
subsequent lines omitted...

现有日志体系

分析这个异常出现的具体原因之前,有必要先快速了解一下现有的Java日志体系。下图是现有Java日志体系的一个示意:

上图不是非常精准,但是能够比较清晰地展示现有Java日志体系的主体架构。Java日志体系大体可以分为三个部分:日志门面接口、桥接器、日志框架具体实现。

Java日志框架有很多种,最简单的是Java自带的java.util.logging,而最经典的是log4j,后来又出现了一个比log4j性能更好的logback,其他的日志框架就不怎么常用了。应用程序直接使用这些具体日志框架的API来满足日志输出需求当然是可以的,但是由于各个日志框架之间的API通常是不兼容的,这样做就使得应用程序丧失了更换日志框架的灵活性。

比直接使用具体日志框架API更合理的选择是使用日志门面接口。日志门面接口提供了一套独立于具体日志框架实现的API,应用程序通过使用这些独立的API就能够实现与具体日志框架的解耦,这跟JDBC是类似的。最早的日志门面接口是commons-logging,但目前最受欢迎的是slf4j。

日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的桥接器,就好像JDBC与各种不同的数据库之间的结合需要对应的JDBC驱动一样。

需要注意的是,前面说过,上图并不精准,这只是主要部分,实际情况并不总是简单的“日志门面接口-->桥接器-->日志框架”这一条单向线。实际上,独立的桥接器有时候是不需要的,而且也并不是只有将日志门面API转调到具体日志框架API的桥接器,也存在将日志框架API转调到日志门面API的桥接器。

说白了,所谓“桥接器”,不过就是对某套API的伪实现。这种实现并不是直接去完成API所声明的功能,而是去调用有类似功能的别的API。这样就完成了从“某套API”到“别的API”的转调。如果同时存在A-to-B.jar和B-to-A.jar这两个桥接器,那么可以想象当应用程序开始调用A或者B的API时,会发生什么事。这就是最开始引出的那个stack overflow异常的基本原理。

slf4j的转接绑定

上面只是从整体上大概说了下Java现有日志体系,还看无法详细说明问题所在,需要进一步了解一下slf4j与具体日志框架的桥接情况。

slf4j桥接到具体日志框架

下图来自slf4j官网文档Binding with a logging framework at deployment time

可以看到slf4j与具体日志框架结合的方案有很多种。当然,每种方案的最上层(绿色的应用层)都是统一的,它们向下都是直接调用slf4j提供的API(浅蓝色的抽象API层),依赖slf4j-api.jar。然后slf4j API向下再怎么做就非常自由了,几乎可以使用所有的具体日志框架。注意图中的第二层是浅蓝色的,看左下角的图例可知这代表抽象日志API,也就是说它们不是具体实现。如果像左边第一种方案那样下层没有跟任何具体日志框架实现相结合,那么日志是无法输出来的(这里不确定是否可能会默认输出到标准输出)。

图中第三层明显就不如第一、二层那么整齐划一了,因为这里已经开始涉及到了具体的日志框架。

首先看第三层中间的两个湖蓝色块,这是适配层,也就是桥接器。左边的slf4j-log4j12.jar桥接器看名字就知道是slf4j到log4j的桥接器,同样,右边的slf4j-jdk14.jar就是slf4j到Java原生日志实现的桥接器了。它们的下一层分别是对应的日志框架实现,log4j的实现代码是log4j.jar,而jul实现代码已经包含在了JVM runtime中,不需要单独的jar包。

再看第三层其余的三个深蓝色块。它们三个也是具体的日志框架实现,但是却不需要桥接器,因为它们本身就已经直接实现了slf4j API。slf4j-simple.jar和slf4j-nop.jar这两个不用多说,看名字就知道一个是slf4j的简单实现,一个是slf4j的空实现,平时用处也不大。而logback之所以也实现了slf4j API,据说是因为logback和slf4j出自同一人之手,这人同时也是log4j的作者。

第三层所有的灰色jar包都带有红框,这表示它们都直接实现了slf4j API,只是湖蓝色的桥接器对slf4j API的实现并不是直接输出日志,而是转去调用别的日志框架的API。

其它日志框架API转调回slf4j

如果只存在上面这些从sfl4j到其他日志框架的桥接器,可能还不会出什么问题。但是实际上还有另外一类桥接器,它们的作用跟上面的恰好相反,它们将其它日志框架的API转调到slf4j的API上。下图来自slf4j官网文档Bridging legacy APIs:

上图展示了目前为止能安全地从别的日志框架API转调回slf4j的所有三种情形。

以左上角第一种情形为例,当slf4j底层桥接到logback框架的时候,上层允许桥接回slf4j的日志框架API有log4j和jul。jcl虽然不是什么日志框架的具体实现,但是它的API仍然是能够被转调回slf4j的。要想实现转调,方法就是图上列出的用特定的桥接器jar替换掉原有的日志框架jar。需要注意的是这里不包含logback API到slf4j API的转调,因为logback本来就是slf4j API的实现。

看完三种情形以后,会发现几乎所有其他日志框架的API,包括jcl的API,都能够随意的转调回slf4j。但是有一个唯一的限制就是转调回slf4j的日志框架不能跟slf4j当前桥接到的日志框架相同。这个限制就是为了防止A-to-B.jar跟B-to-A.jar同时出现在类路径中,从而导致A和B一直不停地互相递归调用,最后堆栈溢出。目前这个限制并不是通过技术保证的,仅仅靠开发者自己保证,这也是为什么slf4j官网上要强调所有合理的方式只有上图的三种情形。

到这里,在开始所展示的那个异常的原理基本已经清楚了。此外,通过上图还可以看出可能会出现类似异常的组合不仅仅是log4j-over-slf4j和slf4j-log4j12,slf4j官网还指出了另外一对:jcl-over-slf4j.jar和slf4j-jcl.jar

代码示例

前面的分析都是理论上的,实际代码中即便同时使用了log4j-over-slf4j和slf4j-log4j12,也未必一定会出现异常。下面的代码调用slf4j的API输出日志,slf4j底层桥接到log4j:

package test;

public class HelloWorld {
public static void main(String[] args) {
org.apache.log4j.BasicConfigurator.configure();
org.slf4j.Logger logger = org.slf4j.LoggerFactory
.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}

配置clas配置classpath上的jar包为(注意log4j在log4j-over-slf4j之前):

在这种情况下运行测试程序是能够正常输出日志的,不会出现stack overflow异常。但是如果调整classpath上的jar顺序为:

再运行测试程序就出现类似于本文最开始的stack overflow异常了,可以看到明显的周期性重复:

序列图分析

上图是堆栈溢出的详细调用过程序列图。从调用1开始,依次调用1.1、1.1.1……最后到了1.1.1.1.1.1(图中最后一个调用)的时候,发现它跟1是完全一样的,那么后续的过程就是完全一样的重复了。

需要特别说明的是最开始的导火索并不只有图中所示的LoggerFactory.getLogger()一种,应用程序中能够触发堆栈溢出异常的直接调用还有好几种其它的,比如前面示例代码中触发异常的实际上是第一条语句org.apache.log4j.BasicConfigurator.configure(),但后续的互相无限递归调用过程基本都是跟上图相同的过程。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • log4j2日志异步打印(实例讲解)

    log4j2支持日志的异步打印,日志异步输出的好处在于,使用单独的进程来执行日志打印的功能,可以提高日志执行效率,减少日志功能对正常业务的影响. 异步日志在程序的classpath需要加载disruptor-3.0.0.jar或者更高的版本. Asynchronous Loggers是一个新增特性在Log4j 2 ,可以实现完全异步也可以和同步混合使用,还可以只异步化Appender,以提升系统性能,官方数据显示混合没有完全异步化效果好. 1,完全异步模式: 这种异步日志方式,不需要修改原来的配

  • 详解log4j.properties的简单配置和使用

    本文介绍了详解log4j.properties的简单配置和使用,分享给大家,具体如下: 简单log4j.properties配置示例 ### set log levels ### log4j.rootLogger = INFO , console , debug , error ### console ### log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = Syst

  • Log4j 配置日志打印时区的实现方法

    og4j版本:2.8.1 开发桌面程序时遇到一个问题,idea中日志时间和CMD中jar包运行日志时间都正常,使用exe4j打成的exe运行后,日志的时间都要少8个小时,时区变得不对了 网上找了一圈发现没有类似问题的解决办法,后来终于在stack overflow 中找到相应的解决办法 在log4j2.xml配置文件中,进行如下配置即可 <!-- 输出日志格式 --> <PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS}{

  • log4j2 项目日志组件的实例代码

    在项目运行过程中,常常需要进行功能调试以及用户行为的跟踪和记录,部分人习惯使用System.out,但这并不建议,它仅仅是使用方便但不便于维护也无扩展性.相比log4j的话,log4j可以控制日志信息的输送目的地.输出格式以及级别等等,使我们能够更加细致地控制日志的生成过程. Log4j2是对Log4j1的升级,在性能和功能上有显著的改进,包括多线程中吞吐量的增强.占位符的支持.配置文件自动重新加载等 一.入门介绍 1.下载jar包 pox.xml <dependencies> <dep

  • 浅谈log4j 不打印异常堆栈

    本文研究的主要是log4j 不打印异常堆栈的相关内容,具体如下. 最近在线上系统的错误日志中发现了一个现象: 代码里用log4j打印系统运行时异常堆栈信息,在错误日志中无法看到堆栈信息,只有异常信息.这对于程序员来说是一个打击,没有堆栈信息何从查bug啊. [01-15 11:29:26] [ERROR] [org.apache.thrift.server.AbstractNonblockingServer$FrameBuffer:524] Unexpected throwable while

  • log4j2异步Logger(详解)

    1 异步Logger的意义 之前的日志框架基本都实现了AsyncAppender,被证明对性能的提升作用非常明显. 在log4j2日志框架中,增加了对Logger的异步实现.那么这一步的解耦,意义何在呢? 如图,按我目前的理解:异步Logger是让业务逻辑把日志信息放入Disruptor队列后可以直接返回(无需等待"挂载的各个Appender"都取走数据) 优点:更高吞吐.调用log方法更低的延迟. 缺点:异常处理麻烦. 可变日志消息问题.更大的CPU开销.需要等待"最慢的A

  • 详解log4j-over-slf4j与slf4j-log4j12共存stack overflow异常分析

    注:下文中的"桥接"."转调"."绑定"等词基本都是同一个概念. log4j-over-slf4j和slf4j-log4j12是跟java日志系统相关的两个jar包,当它们同时出现在classpath下时,就可能会引起堆栈溢出异常.异常信息大致如下(摘自slf4j官网文档Detected both log4j-over-slf4j.jar AND slf4j-log4j12.jar on the class path, preempting St

  • 详解go基于viper实现配置文件热更新及其源码分析

    go第三方库 github.com/spf13/viper实现了对配置文件的读取并注入到结构中,好用方便. 其中以 viperInstance := viper.New() // viper实例 viperInstance.WatchConfig() viperInstance.OnConfigChange(func(e fsnotify.Event) { log.Print("Config file updated.") viperLoadConf(viperInstance) //

  • 详解Log4j 日志文件存放位置设置

    以DailyRollingFileAppender 为例:假设每天一个日志文件 有以下设置: log4j.appender.A1=org.apache.log4j.DailyRollingFileAppender log4j.appender.A1.File=app.log log4j.appender.A1.DatePattern='.'yyyy-MM-dd log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.append

  • 详解MySQL的limit用法和分页查询语句的性能分析

    limit用法 在我们使用查询语句的时候,经常要返回前几条或者中间某几行数据,这个时候怎么办呢?不用担心,mysql已经为我们提供了这样一个功能. SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数.LIMIT 接受一个或两个数字参数.参数必须是一个整数常量.如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目.初始记

  • 详解PostgreSQL 14.4安装使用及一些安装的异常问题

    PostgreSQL 14的安装以及使用 因为公司的一些要求,可能要换数据库,虽然之前装过,但是版本感觉还是新一点比较好,所以重新装一下 首先下载文件,直接去官网下载就行 https://www.enterprisedb.com/downloads/postgres-postgresql-downloads 下载以后双击打开安装包 大部分的内容都是一路next下去 安装路径设置 额外的功能软件设置 安装路径设置 设置密码 端口号 语言 然后一路点下去就能完成安装了 要使用cmd进行操作,对数据库

  • log4j 详解异步日志的配置和测试

     log4j 详解异步日志的配置和测试 日志可以帮助我们分析故障原因,做些数据挖掘的工作.最简单的日志方法,就是自己写个写文件的方法,在需要打日志的时候调用下,但是这显然不可能在实际工程上用.还有个问题,就是频繁地打日志,会增加磁盘I/O,使得系统性能下降.这里用log4j这个库来部署一个含有日志管理的轻量级的系统,主要支持日志的异步写和等级分类的功能,完成最低限度的日志需求. 首先,我们建立一个Maven工程,并且在pom.xml文件里面引入log4j的依赖: <dependency> &l

  • jdk源码阅读Collection详解

    见过一句夸张的话,叫做"没有阅读过jdk源码的人不算学过java".从今天起开始精读源码.而适合精读的源码无非就是java.io,.util和.lang包下的类. 面试题中对于集合的考察还是比较多的,所以我就先从集合的源码开始看起. (一)首先是Collection接口. Collection是所有collection类的根接口;Collection继承了Iterable,即所有的Collection中的类都能使用foreach方法. /** * Collection是所有collec

  • 分析Springboot中嵌套事务失效原因详解

    首先两个事务方法,其中一个调用另一个. @Transactional(rollbackFor = Exception.class) public void trance() { try { trance1();//调用下一个事务方法. } catch (Exception e) { e.printStackTrace(); } User user = new User(); ShardingIDConfig shardingIDConfig = new ShardingIDConfig(); u

  • Redis慢查询日志及慢查询分析详解

    目录 前提介绍 单线程命令的处理机制 本章内容 什么是慢查询 慢查询日志 Redis慢查询日志 Redis慢查询的危害 Redis客户端执行一条命令的步骤 慢查询引发的问题 阈值和慢查询的日志的设置 阈值参数设置 慢查询执行时间阈值 慢查询数据存储阈值 慢查询的配置类型和方式 慢查询日志的操作命令 slowlog get [n] slowlog len slowlog reset 前提介绍 本篇文章主要介绍了Redis的执行的慢查询的功能的查询和配置功能,从而可以方便我们在实际工作中,进行分析R

随机推荐