在系统运行的时候,不可避免的会出现问题,这个时候就需要进行排查,而排查的最简单的方式就是查看日志。一些关键位置的日志输出会给开发者排查问题带来巨大的方便。而我们java的日志体系可以说非常复杂。这里我们就研究下java的日志体系。这里先把日志的关系拉出来,你就有很大的感触了。
这都什么玩意儿!!
日志的基本用法
我自己项目内使用的是lombook + slf4j + log4j的组合:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
有了依赖之后在类顶部标记@slf4j即可产生日志对象:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LogTest {
public static void main(String[] args) {
log.info("打印info日志");
log.debug("打印debug日志");
log.warn("打印warn日志");
log.error("打印error日志");
}
}
其中四种打印日志的方法需要注意其使用场景:
- ERROR:当程序发生不可预期的错误(按照设计不应该发生)时打印。或者不可恢复的错误(启动时初始化失败,直接退出)时打印。
- WARN:当程序发生可预期或者不影响主要功能的时候时使用。
- INFO:用于追踪程序的正常执行。典型的在 Web 模块中,任何一处 INFO 日志的输出在一次 Web Request 中被调用的次数应该是 “常量级” 别的,且大部分情况下这个常量应该是 1。
- DEBUG:调试日志,DEBUG 日志打印的量没有限制,但需要注意不要影响执行性能。
发展历史
为了更好理解我们的日志体系结构,先看看历史上日志框架是怎么演变过来的
System.out和System.err
这应该是最早的日志记录方式吧,但是不灵活也不可配置,要么就是全部打印,要么就是全部不打印,没有一个统一的日志级别
Log4j
在1996年初,E.U.SEMPER
(欧洲安全电子市场)项目决定编写自己的跟踪API
,最后该API
演变为Log4j
,Log4j
日志软件包一经推出就备受欢迎,当然这里必须要提到一个人,就是Log4j
的主要贡献者,这个大佬:Ceki Gülcü。后来Log4j
成为了Apache
基金会项目中的一员,同时Log4j
的火爆,让Log4j
一度成为业内日志标杆。(据说Apache
基金会还曾经建议Sun
引入Log4j
到java
的标准库中,但是sun
拒绝了)
JUL
果然Sun
有自己的考虑,2002年2月Java1.4
发布,Sun
竟然推出了自己的日志库Java Util Logging
(缩写是JUL)。很多日志的实现思想也都是仿照Log4j
,毕竟Log4j
先出来很多年了,已经很成熟了此时,这两个日志工具打架,显然Log4j
是更胜一筹
它们打架感觉就是互相竞争,Sun
心里可能在想,不就是做个日志工具嘛,谁不会!
JCL(Jakarta Commons Logging)
于是JUL
刚出来不久,2002年8月Apache
又推出了日志接口Jakarta Commons Logging
,也就是日志抽象层,当然也提供了一个默认实现Simple Log
,这野心很大,想一统日志抽象(就像以前的JDBC
一统数据库访问层),让日志产品去实现它的抽象,这样只要你的日志代码依赖的是JCL
的接口,你就可以很方便的在Log4j
和JUL
之间做切换,当时日志领域大概是这样的结构,当然也还是方便理解的,也很优雅
但是好景不长,在使用过程中,虽然现在日志系统在JCL
的统一下很优雅,很美好,但大家发现了JCL
还不够好,有些人甚至认为JCL
造成的问题比解决的问题还多。
Slf4j(Simple Logging Facade for Java)
大佬Ceki Gülcü(也就是Log4j
的作者)由于一些原因离开了Apache
,之后觉得JCL
不好,于是于2005年自己撸出一个新工程,也就是一套新日志接口(有得也叫日志门面):Slf4j(Simple Logging Facade for Java)
,明显这个Slf4j
是直指JCL
,后面确实也证明了Slf4j
是要比JCL
在很多地方更优秀
但是由于Slf4j
出来的较晚,而且还只是一个日志接口,所以之前已经出现的日志产品,如JUL
和Log4j
都是没有实现这个接口的,所以尴尬的是光有一个接口,没有实现的产品也是很憋屈啊,就算开发者想用Slf4j
也是用不了。这时候,大佬Ceki Gülcü发话了:别急,我早帮你们想好了,要让Sun
或者Apache
这两个庞然大物来实现我的接口太难了,我直接帮你们实现!于是大佬Ceki Gülcü撸出了之前提到的桥接包,也就是这种类似适配器模式。大佬提供了桥接包,于是日志系统现在有了这样的结构
但是其实之前很多Java
应用应该依赖的JCL
,所以光有日志产品桥接包,好像还不够Ceki Gülcü:没问题,不就是不够桥接包么,我写,我来证明Slf4j是最完美的 于是有了JCL
的桥接包
相当于此时的桥接包就是分了两种场景
- 之前
Java
应用用的日志接口(如JCL
) - 之前
Java
应用用的日志产品(如Log4j
)
那好,那我们如果再考虑一下这种场景呢?假设你的Java
应用使用了Spring
的第三方的框架,但是假设Spring
默认用JCL
,并且最终用的JUL
打印的日志,但是你的系统使用了Slf4j
作为日志接口,日志产品使用了Log4j
,那你将有两种日志输出,两种日志的打印方式不统一,到时候解决bug的时候就很恼火,而且配置日志的配置文件还需要两份。
所以为了方便统一应用中的所有日志,大佬发话了Ceki Gülcü:没事,大家都选择用Slf4j统一吧,我来帮大家统一,没有事是桥接包解决不了的,有的话,那就再来个
当然此时这种场景也是符合之前说的两种情况的,因此现在日志系统大体应该是这样的
但好景又不长,大佬毕竟是大佬,Log4j
不就是自己写的么,所以最清楚Log4j
缺点的人也正是他
Logback
由于使用Slf4j
,需要一次桥接包,也就是之前的日志产品都不是正统的Slf4j
的实现,因此,2006年,出自Ceki Gülcü之手的日志产品Logback
应运而生,Logback
是完美实现了Slf4j
,于是现在日志系统变成了
ok了,现在咱们有了2个日志接口,3个日志产品,大家也都看起来相安无事。但Slf4j+Logback
的模式,显然很冲击JCL+Log4j
,并且本身Logback
确实比Log4j
性能更优,设计更为合理,所以Apache
看不下去了
Log4j2
在2012年,Apache
直接推出新项目,不是Log4j1.x
升级,而是新项目Log4j2
,因为Log4j2
是完全不兼容Log4j1.x
的。Log4j2
几乎涵盖Logback
所有的特性,更甚者的Log4j2
也搞了分离的设计,分化成log4j-api
和log4j-core
,这个log4j-api
也是日志接口,log4j-core
是日志产品。
现在我们可有了3个日志接口,以及4个日志产品了。为了让大家可以接入自己的Log4j2
,Apache
也推出了它的桥接包。
已经没眼看了。
最佳实践
了解了日志的发展历史,那现在我们再回过头来看看如果,你的系统在选择日志方案的时候,如何抉择呢?毕竟我们3个日志接口,以及4个日志产品
- 显然第一点是使用日志接口的
API
而不是直接使用日志产品的API
这一条也是必须的,也是符合依赖倒置原则的,我们应该依赖日志的抽象,而不是日志的实现 - 日志产品的依赖只添加一个 当然也这个也是必须的,依赖多个日志产品,只会让自己的应用处理日志显得更复杂,不可统一控制
- 把日志产品的依赖设置为
Optional
和runtime scope
其中Optional
是为了依赖不会被传递,比如别的人引用你这个jar
,就会被迫使用不想用的日志依赖而scope
设置为runtime
,是可以保证日志的产品的依赖只有在运行时需要,编译时不需要,这样,开发人员就不会在编写代码的过程中使用到日志产品的API
了