了解java日志体系

log4j, log4j2, slf4j, logback关系

log4j是由Apache开发的一套元老级日志框架,为无数新老系统提供了日志服务;而后来log4j的作者Ceki因为某些原因离开Apache,并自己开发了性能更优化的日志门面slf4j和新的日志框架logback,logback能够与slf4j无缝集成;Apache之后也发力对log4j进行了多方面优化,并推出了新的日志框架log4j2,相对于log4j和logback,很大程度上提高了日志的吞吐量并降低了延时。

目前slf4j因为其优秀的性能和“日志门面”的设计思想,受到了广泛的应用;而log4j2因为其性能已全面超越log4j与logback,也是日志系统中一个很重要的选择。

除了这几个纠葛比较深的日志框架外,还包括其他的日志框架,common-logging, java.util.logging等。

日志门面与日志框架

slf4j就是典型的“日志门面(Logging Facade)”,利用了设计模式中的门面模式思想,对外提供一套通用的日志记录的API,而不提供具体的日志输出服务,要实现日志输出,需要集成其他的日志框架,例如log4j2,logback,log4j,jul等。

这种门面模式的好处在于,记录日志的API和日志输出的服务分离开,代码里面只需要关注记录日志的API,通过slf4j指定的接口记录日志;而日志输出通过引入jar包的方式即可指定其他的日志框架。当我们需要改变系统的日志输出服务时,不用修改代码,只需要改变引入日志输出框架jar包。


slf4j集成原理.png-19.5kB
slf4j集成原理.png-19.5kB

需要指出一点,门面模式提供了一种日志API和输出分离的模式,但是除slf4j和common logging之外的其他完整的日志框架,本身就具备同时提供日志API和输出的服务,当然也是可以直接采用这些框架本身记录日志的。

slf4j与common-logging

common-logging同样是一套日志门面,spring框架本身使用的是common logging,与slf4j的区别主要在于与日志输出服务的绑定机制。
common-logging采用运行时动态绑定的机制,运行时通过一套动态寻找绑定的规则:

  • 在进程启动时尝试获取名为"org.apache.commons.logging.Log"的配置属性),按配置选取对应的日志输出服务
  • 如果没有获取到对应配置属性,会尝试在系统参数中寻找名为"org.apache.commons.logging.Log"的参数项
  • 如果均没有获取到,会在classpath下寻找log4j的相关class,如果找到,则使用log4j作为日志输出服务
  • 如果没有找到log4j,则尝试使用java.util.logging包作为日志输出服务
  • 如果上述都失败,则使用SimpleLog作为日志输出服务,即将所有日志输出至控制台标准输出System.err

common-logging基于classLoader来动态寻找和加载所绑定的日志输出服务,但这种动态的方式效率不高;另外在一个复杂甚至混乱的依赖环境下,动态查找机制容易引发混乱;而且对于像OSGI这类需要使用自定义classLoader的框架,无法与common-logging一起工作。

slf4j日志输出服务绑定则相对简单很多,在编译时就静态绑定日志输出服务,只需要提前引入需要的日志框架,以及引入slf4j到该框架的适配库,常见的适配库有log4j-slf4j-impl,slf4j-log4j12,slf4j-jdk14等,slf4j与logback天然集成,不需要适配库(毕竟是一个作者写的)。

slf4j的另外一些小优点体现在提供的API上,包括日志参数强制要求String类型,避免不规范代码;提供支持填充参数的日志模板,而且只会在确实需要输出日志时才会拼接日志字符串。

logger.error("Failed to format {}", s, e);

slf4j适配到各日志框架

基于slf4j的优势,目前常见的做法是使用slf4j做日志门面,再结合其他日志输出服务。目前除了logback之外,其他的日志框架无法直接与slf4j集成,因此需要我们在使用时引入各种适配库,将基于slf4j API记录的日志指向我们需要的日志框架进行输出。下面列了一下slf4j到logback,log4j,java.util.logging,log4j2的适配库。


slf4j-binding (1).png-30.4kB
slf4j-binding (1).png-30.4kB

除了slf4j本身需要引入的slf4j-api.jar之外,其它还需要:

  • slf4j+logback: logback-classic.jar, logback-core.jar
  • slf4j+log4j: slf44j-log4j12.jar, log4j.jar
  • slf4j+jul: slf4j-jdk14.jar
  • slf4j+log4j2: log4j-slf4j-impl.jar, log4j-api.jar, log4j-core.jar

slf4j通过这些适配库与各个日志框架集成的原理很简单,首先我们在使用slf4j记录日志时,会首先初始化一个Logger:

private static Logger logger=LoggerFactory.getLogger(TestClass.class);

slf4j的LoggerFactory提供的getLogger方法:

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

public static ILoggerFactory getILoggerFactory() {
    …… ……
    return StaticLoggerBinder.getSingleton().getLoggerFactory();
    …… ……
  }

可以看到,当我们调用slf4j的LoggerFactory.getLogger()方法时,适配库的作用就是:

  • 提供org/slf4j/impl/StaticLoggerBinder.class类,这个类的作用就是返回一个实现ILoggerFactory接口的类(例如log4j-slf4j-impl中返回Log4jLoggerFactory类);
  • 提供实现ILoggerFactory接口的类,该类实现getLogger()方法,返回一个具体的logger实例。

需要注意的是,当我们使用slf4j日志门面之后,只能指定一个slf4j的适配库,否则会在编译期间报错。

各日志体系桥接到slf4j

如果目前应用程序中已经使用了如下混杂方式的API来进行日志的编程:

  • commons-logging
  • jdk-logging
  • log4j

而程序希望统一通过logback进行日志输出,可以通过将这些日志框架桥接到slf4j,然后由slf4j指定logback做日志输出的方式,这就需要指定各个日志框架到slf4j的桥接ba桥接包。

  • 去掉commons-logging(去不去都可以),使用jcl-over-slf4j将commons-logging的底层日志输出切换到slf4j;
  • 去掉log4j1(必须去掉),使用log4j-over-slf4j,将log4j1的日志输出切换到slf4j
  • 使用jul-to-slf4j,将jul的日志输出切换到slf4j

下图是slf4j官网提供的桥接示例图。


此处输入图片的描述
此处输入图片的描述

这种桥接的方式的原理也好理解,桥接包中会直接提供与其他日志框架API相同路径的类,替换掉它们本身的类。例如jcl-over-slf4j会替换掉common-logging中的org.apache.commons.logging.LogFactory类,这个类会使用slf4j创建logger实例。

常见的log包冲突问题解决

1.项目中引入了slf4j的多个日志输出框架,导致报错

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [slf4j-log4j12-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [logback-classic-1.1.3.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 [org.slf4j.impl.Log4jLoggerFactory]

原因:slf4j的LoggerFactory中需要调用StaticLoggerBinder类,当多个适配库存在时,会有多个StaticLoggerBinder存在。
解决:排掉多余的slf4j适配包,只保留需要的日志输出服务;

2.桥接包相互桥接,例如同时引入了log4j-over-slf4j 与 slf4j-log4j12,导致栈溢出

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...

原因:前者将log4j桥接到slf4j,后者将slf4j桥接到log4j,循环桥接,当第一个通过slf4j或log4j获取的logger被调用时,就会出现StackOverflowError。
解决:明确到底使用哪个日志框架,如果使用slf4j做日志API和输出,则去掉slf4j-log4j12;如果使用log4j做日志记录和输出,则去掉log4j-over-slf4j。

3.项目中已有log4j做日志记录API,同时又引入log4j-over-slf4j希望将log4j桥接到slf4j,但使用log4j记录的日志没有正常输出?
原因:log4j-over-slf4j桥接包的原理是替代log4j包本身的org.apache.log4j.Logger类,如果引入了该桥接包,又没有排除log4j本身的包,导致使用Log4j做日志记录的地方还是使用log4j做日志输出,然后项目里面没有任何关于log4j的日志输出配置,导致日志输出失败。
解决:解决方法很简单,排除掉log4j的包即可。

另外关于slf4j相关的问题可以参考slf4j官网提供的一些常见问题和原因分析以及解决方法。

4.项目中依赖各种日志框架,有多个门面slf4j,common logging,还有各种其他的日志框架log4j2, log4j, jul等;有用日志门面记录日志的,也有用非门面日志框架记录日志的,总之,一片混乱 ~
思路:想给服务提供统一的日志输出,可以将各种日志API首先桥接到slf4j,然后指定slf4j的日志输出服务,这样就算不同的日志记录API,也可以通过统一的日志输出服务输出日志。同时也要记得排除各种不需要的日志jar包,解决各种循环桥接的问题。

参考阅读:
slf4j、jcl、jul、log4j1、log4j2、logback大总结
slf4j与jul、log4j1、log4j2、logback的集成原理
该让log4j退休了 - 论Java日志组件的选择
混乱的 Java 日志体系

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 历史 log4j可以当之无愧地说是Java日志框架的元老,1999年发布首个版本,2012年发布最后一个版本,20...
    kelgon阅读 10,144评论 3 53
  • 对于Java的日志框架,你也许会经常看到这些名词: Log4j、Log4j2 Logback Slf4j JCL ...
    NoahU阅读 3,942评论 0 15
  • 前言 最近学习开java web服务器开发,开始学习java,处理业务逻辑,但对其中的日志比较好奇,之前没怎么接触...
    九风萍舟阅读 3,288评论 1 6
  • 在项目开发过程中,我们可以通过 debug 查找问题。而在线上环境我们查找问题只能通过打印日志的方式查找问题。因此...
    Java架构阅读 3,467评论 2 41
  • 概述 在项目开发中,为了跟踪代码的运行情况,常常要使用日志来记录信息。在Java世界,有很多的日志工具库来实现日志...
    静默虚空阅读 1,842评论 1 9