一、聊聊java中混乱的日志体系
♀️波妞:先提个问题,你知道哪些日志框架?
♀️波妞:呃。。。我说说搜我听过的吧,比如log4j、jul、jcl、slf4j、log4j2、simple log、logback,已经有7个了,但是肯定不止7个。
♂️宗介:卧槽,那在项目中我到底该用哪个,看得我脑壳痛,它们有啥子关系没得嘛?
♀️波妞:别着急,下面让我慢慢说。
二、先打印几个日志看看
1. 使用log4j输出一下日志
- 先导入
log4j
的依赖log4j log4j 1.2.17 - 然后在
classpath
下加上log4j
的配置文件log4j.properties
,日志级别设置的是info
log4j.rootLogger=info,stdout # 输出到控制台 log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %p [%t] %C.%M(%L) | %m%n
- 主程序
import org.apache.log4j.Logger; public class Log4j { public static void main(String[] args) { Logger logger = Logger.getLogger("lof4j"); logger.info("log4j"); } }
-
运行程序,看到控制台输出了日志
log4j控制台输出效果,嗯,是白色
2. 使用jul(java.util.logging)输出一下日志
- 因为
jul
是jdk
自带的日志包,所以不需要添加依赖,直接干,主程序如下,注意导包导的是java.util.logging.Logger
import java.util.logging.Logger; public class Jul { public static void main(String[] args) { Logger logger = Logger.getLogger("jul"); logger.info("jul"); } }
-
运行程序,查看控制台
jul控制台输出效果,发现它时红色滴
3.使用jcl输出一下日志
- 先导入
jcl
的依赖commons-logging commons-logging 1.2 - 上主程序,这次不是直接通过
new
得到一个logger
了,而是使用工厂去拿一个logger
,这也是有原因的哈import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class Jcl { public static void main(String[] args) { Log log = LogFactory.getLog("jcl"); log.info("jcl"); } }
-
运行看控制台
咦,红色?怎么和jul的效果那么像
deug发现的确是jul logger
-
把
log4j
的依赖加上试试,加上后的依赖变成了下面的样子commons-logging commons-logging 1.2 log4j log4j 1.2.17 -
再次运行,看看控制台是什么样子
???白色?怎么又变的和log4j的效果一样了,小朋友有很多问号啊
打个断点进一步验证,发现的确是log4j logger -
聪明的你可能已经猜到了,
jcl
根本不是一个日志实现(或者说日志产品),而是一个日志抽象层,主程序里面那个LogFactory.getLog("jcl")
拿到是才一个具体的日志产品的对象。所以我们猜测jcl
的结构是下面这样的
-
为了进一步证实我们的猜想,我们看看源码,在
LogFactoryImpl
这个类中发现一段关键代码- 定义了一个成员变量
classesToDiscover
private static final String[] classesToDiscover = { LOGGING_IMPL_LOG4J_LOGGER, // org.apache.commons.logging.impl.Log4JLogger "org.apache.commons.logging.impl.Jdk14Logger", "org.apache.commons.logging.impl.Jdk13LumberjackLogger", "org.apache.commons.logging.impl.SimpleLog" };
- 代码片段
for(int i=0; i
- 所以玄机就在这里,
LogFactoryImpl
中先定义了一个成员变量classesToDiscover
,它是一个String
数组,里面包含了log4j
和jul
的logger
的全限定名。然后会在代码里面遍历这个数组,每次拿到一个全限定名并调用createLogFromClass
尝试去实例化对象,如果没实例化成功则会返回null
,继续使用下一个全类名去实例化对象。可以看到数组的第二个元素是Jdk14Logger
,它封装的是jul
里面的logger
,而jul
是jdk
自带的库,所以99.99%
的情况下,如果你没导入log4j
的jar
包,最终返回的logger
是jul
的logger
。如果导入了log4j
的jar
包,那么在遍历数组第一个元素后就会得到logger
对象了,此时得到的就是log4j
的logger
对象。这就是前面的例子中为什么没加log4j
依赖时,使用的jul
的logger
,加了log4j
依赖后使用的是log4j
的logger
原因。最后需要注意jul
是java.util.logging
包的简称,是jdk4
正式发布的日志库,可以说是老妖怪。而Jdk13LumberjackLogger
应该是对jdk1.3
及以前版本的东西的封装(没有深究过,),可以说是个超级老妖怪。你肯定不会去用jdk3
去做开发,所以不用在意Jdk13LumberjackLogger
。至于SimpleLog
,这个是jcl
的默认实现,简单但是功能不够强大,很少用,所以也不用在意啦。
- 定义了一个成员变量
4. 小总结
通过上面的demo
,我们发现jc
l是一个日志抽象层。我们面向抽象编程,可以让我们的程序可扩展,可以无缝切换日志产品,比如你今天用的log4j
,明天想用jul
了,那么直接把项目中的log4j
依赖排除掉即可。但是有个问题,看了源码发现jcl
只支持4
个日志产品,还有两个是没用的,也就说jcl
只支持log4j
和jul
。现在市面上这么多日志产品,还有什么logback
呀、nop
呀什么的。我如果要用loback
,想面向抽象编程jcl
也不支持呀。的确是的,因为jcl
已经很久没有更新了,没有对新的日志框架做适配。但是好消息是有个后起之秀来代替它,它就是slf4j
。
三、下面我们开始来说说java中日志系统的前世今生吧
起源:System.out和System.err
这应该是最早的记录日志的方式,不灵活、不能根据实际需要进行一些选项配置
Ⅰ、1996年,Log4j横空出世
在1996
年初,E.U.SEMPER(欧洲安全电子市场)
项目决定编写自己的跟踪API
,最后该API
演变为Log4j
,Log4j
日志软件包一经推出就备受欢迎,当然这里必须要提到一个人,他是Log4j
的主要贡献者,这个大佬叫Ceki Gülcü
,可能应该叫巨佬。。。后面你就明白了。后来Log4j
成为了Apache
基金会项目中的一员,同时Log4j
的火爆,让Log4j
一度成为业内日志标杆。(据说♂️Apache基金会
还曾经建议️♀️Sun
引入Log4j
到java
的标准库中,但是️♀️sun
拒绝了,️♀️sun
可能是有自己的私心)。
Ⅱ、2002年2月,JUL诞生
果然️♀️sun
是有私心的(我自己的亲儿子怎么能用你撸出来的东西),于是在2002
年2
月Java1.4
发布,️♀️sun
正式推出了自己的日志库Java Util Logging
,其实很多日志的实现思想也都是仿照Log4j
,毕竟Log4j
先出来很多年了,已经很成熟了此时,这两个日志工具打架,显然Log4j
是更胜一筹。它们打架其实就是互相竞争,️♀️sun
心里可能在想,不就是做个日志工具嘛,谁不会!当然好景不长。
Ⅲ、2002年8月,JCL(Jakarta Commons Logging)出生,想一统江湖
终于,♂️Apache
发言了, 玩编程,谁玩的过我!你不让我成为jdk
标准,我就把自己变成日志的标准,哼!!!!于是JUL
刚出来不久,2002
年8
月♂️Apache
又推出了日志接口Jakarta Commons Logging
,也就是日志抽象层,当然也提供了一个默认实现Simple Log
,这野心很大,想一统日志抽象(就像以前的JDBC
一统数据库访问层),让日志产品去实现它的抽象,这样只要你的日志代码依赖的是JCL
的接口,你就可以很方便的在实现了jcl
接口的日志产品之间做切换,当时日志领域大概是这样的结构,当然也还是很容易理解的,也很优雅。
但是好景不长,在使用过程中,虽然现在日志系统在JCL
的统一下很优雅,很美好,但大家发现了JCL
还不够好,有些人甚至认为JCL
造成的问题比解决的问题还多...【抱怨地址】
Ⅳ、2005年,slf4j(Simple Logging Facade for Java)出世,最后证明,后来者居上
大佬Ceki Gülcü
(也就是Log4j
的作者)由于一些原因离开了♂️Apache
,之后觉得jcl
不好,于是于2005
年自己撸出一个新工程,也就是一套新日志接口(也叫java简单日志门面):slf4j
(Simple Logging Facade for Java
),感觉粗来了么。。。这战争的硝烟 ,明显这个Slf4j
是剑指jcl
啊,但是后面确实也证明了slf4j
是要比jcl
的战斗力更强。
Ceki Gülcü
心想,和我玩接口,我一个人就是一支军队!玩死你们。
slf4j绑定器:绑定器是什么?
由于slf4j
出生的较晚,而且还只是一个日志接口,所以之前已经出现的日志产品,如JCL
和Log4j
都是没有实现这个接口的,所以尴尬的是光有一个接口,没有实现的产品也是很憋屈啊,就算开发者想用Slf4j
也是用不了,这时候,巨佬Ceki Gülcü发话了。
Ceki Gülcü
:咳咳....,别急,我早帮你们想好了,要让️♀️ sun
或者♂️Apache
这两个家伙来实现我的接口,太南啦,老铁,但。。。我帮你们实现,不就完了么。。。
于是巨佬Ceki Gülcü
撸了个绑定器(巨佬Ceki Gülcü的slf4j官网说的是slf4j binding
,就叫它绑定器吧),他是这样说的,只需要在你的类路径下替换不同的绑定器,就可以实现日志框架的切换。比如要将jul
替换成log4j
,只需要用slf4j-log4j-xxx.jar
替换slf4j-jdk14-xxx.jar
(xxx表示版本号)。下面三张图,仔细品味下哦,特别是第二张。
slf4j桥接器:桥接器是什么?
巨佬Ceki Gülcü
的意思就是,你的组件可能依赖slf4j
以外的日志组件,并且这些日志组件不能改变成slf4j
,这样就会产生日志冲突,在你的应用中就会出现两种及以上的日志,造成日志混乱。对于这种场景,巨佬Ceki Gülcü
提供了slf4j桥
这个东西,让其他框架的日志可以重定向到slf4j,这样就完美解决了日志冲突问题。
下面举一个栗子,先喝杯水:
假设你的应用中使用的日志框架是jul
,而spring
使用的jcl
,jcl
又使用的log4j
,如下图。
现在有两种解决方式:
-
一是使用
log4j
,因为你可以改自己的代码,所以可以直接更换slf4j
绑定器,将应用使用的日志框架切换到log4j
将绑定器从 slf4j-jdk14 换成 slf4j-log4j 就ok了 -
二是如果不想妥协,我就想使用
juc
,这个时候,你又改不了人家spring
的代码,让spring
不使用log4j
,怎么办,桥接器登场
jcl-over-slf4j桥接器中重写了log4j,也就是说jcl-over-slf4j有一个山寨版的log4j,这个log4j会重定向到slf4j,最终使用juc。
看了上面的介绍后是不是觉得,有木有jio得巨佬Ceki Gülcü
真的是个巨佬了,他一个人干赢了️♀️ sun
和♂️Apache
,让他们做自己的小弟。
Ceki Gülcü
心想:挖鼻屎,和我斗,我一个人就能爆发出你们两个加起来乘上99999的力量。
Logback(2006年)
巨佬Ceki Gülcü
觉得,使用slf4j
,要么需要加上绑定器,要么需要家还是那个桥接包,因为那些日志产品都不是slf4j
的亲生儿子,所以他觉得应该让slf4j
有个亲儿子,于是在2006年logback
诞生了,logback
完美实现了slf4j
。由于log4j
也是自己写的,他专门写了篇文章建议在你的应用中首先考虑使用logback
而不是log4j
,并指出logback
相比log4j
更好的原因--->文章地址,具体优点体现在性能更好、更安全可靠、更节约内存等等,具体可以去看文章,这里不多费口舌了。
先对上面提到的东西做个小总结,这里面只画出了常用的日志产品,也就是log4j、jul和logback
Ⅴ、2012年,战火还未结束,天降log4j2
在2012年,♂️Apache
直接推出新项目,不是对log4j1.x
升级,而是新项目log4j2
,因为log4j2
是完全不兼容log4j1.x
的
并且很微妙的,log4j2
几乎涵盖logback
所有的特性(这不是对着干是啥~而且还有抄袭的嫌疑。。。哈哈哈),更甚者的Log4j2
也搞了分离的设计,分化成log4j-api
和log4j-core
,这个log4j-api
也是日志接口,log4j-core
才是日志产品。。。
emmm,我看到这,我都有点崩溃了
现在我们可有了3个日志接口(jcl
、slf4j
、log4j2
),以及4个日志产品(log4j
、jul
、simple-log
、logback
)。。。当然♂️Apache
也知道该做啥,为了让大家可以接入自己的Log4j2
,那不就是绑定器嘛,或者要不就是桥接器(在官网只看到了桥接器,没有看到绑定器,我猜应该是♂️Apache
太懒了),♂️Apache
也麻溜的推出了它的桥接包。
。。。最后的图大概是这样。嗯,画完这张图我掉了99999根头发。
Ⅵ、现在是2020年,各个公司使用不同的日志框架,没具体统计过,我觉得是slf4j
占主导地位
四、前上面说了那么多,现在说一点注意事项和总结
先说注意事项:
- 如果系统系统采用
slf4j
,那么需要导入slf4j-api
和对应的绑定器。需要说的是这个绑定器,比如导入了slf4j-log4j12
的依赖,slf4j-log4j12
会自动导入log4j
的依赖。所以需要排除掉应用自己导入的log4j依赖
。 - 如果使用需要将
jul
或者jcl
桥接到log4j2
,除了需要导入log4j-api
和lo4j-core
,还需要导入log4j-jul
或log-jcl
桥接包;而如果想将log4j
桥接到log4j2
,则只需要导入lo4j-core
和log4j-1.2-api
即可。 - 如果要将
jul
桥接到log4j2
,需要设置系统属性,属性名为java.util.logging.manager
,属性值为org.apache.logging.log4j.jul.LogManager
。可以通过两种方式设置,一是添加vm参数-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
,二是在调用Logger
或LoggerManager
之前执行System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
。
import java.util.logging.Logger; public class Jul { public static void main(String[] args) { System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")。 Logger logger = Logger.getLogger("jul"); logger.info("jul"); } }
- 如果要将
jul
桥接到slf4j
,也需要做些特殊处理将jul的日志路由到slf4j
介绍了两种方法:
第一种是编程的方式,如下代码所示import org.slf4j.bridge.SLF4JBridgeHandler; import java.util.logging.Logger; public class Jul { public static void main(String[] args) { // 移除已经存在的handler SLF4JBridgeHandler.removeHandlersForRootLogger(); // 安装SLF4JBridgeHandler SLF4JBridgeHandler.install(); Logger logger = Logger.getLogger("jul"); logger.info("jul"); } }
*第二种方式是修改JAVA_HOME\jre\lib\logging.properties
循环依赖
比如应用中加上了jul-to-slf4j和slf4j-jdk14的依赖,jul会重定向到slf4j,slf4j又绑定的jul,所以这样就产生了循环依赖。
下面列出一些不能会禅心循环依赖的jar依赖
slf4j中
jul-to-slf4j 和 jul-to-slf4j
log4j-over-slf4j 和 slf4j-log4j12
jcl-over-slf4j 和 slf4j-jcl
log4j2中
log4j-to-slf4j 和 log4j-slf4j-impl还有许多细节,比如性能呀什么的,文中没有提到,乡亲们感兴趣的话可以去看官网哦:slf4j官网、log4j2官网
总结:
- 对于库的创造者(上帝),不写接口造成的后果就是------>看到上面画的日志的各种桥接的图你心碎吗,我反正碎了。如果一开始就定义了接口规范,我想我们的日志框架不会那么混乱。
- 对于库的使用者,一定要面向抽象编程,这样的应用才会健壮,易于扩展。
- 没有什么问题是加一个适配器层(比如各种桥接器、绑定器,他们都是适配器)解决不了的,解决不了,那就两个。