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, common-logging
- 完整的日志框架包括:log4j2, logback, log4j, java.util.logging
(完整日志框架是指框架本身包括记录日志的API和日志输出的服务)
需要指出一点,门面模式提供了一种日志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本身需要引入的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 日志体系