一、Java日志体系的发展历程
Log4j:在JDK 1.3及以前,Java打日志依赖System.out.println(), System.err.println()或者e.printStackTrace(),Debug日志被写到STDOUT流,错误日志被写到STDERR流。这样打日志有一个非常大的缺陷,即无法定制化,且日志粒度不够细。
于是,Ceki Gülcü 于2001年发布了Log4j,后来成为Apache 基金会的顶级项目。Log4j 在设计上非常优秀,对后续的 Java Log 框架有长久而深远的影响,它定义的Logger、Appender、Level等概念如今已经被广泛使用。Log4j 的短板在于性能,在Logback 和 Log4j2 出来之后,Log4j的使用也减少了。JUL:Apache要求把log4j并入到JDK,SUN拒绝,并在jdk1.4版本后增加了JUL(java.util.logging),但是JUL功能远不如log4j完善,开发者需要自己编写Appenders(Sun称之为Handlers),且只有两个Handlers可用(Console和File),JUL在Java1.5以后性能和可用性才有所提升。
JCL(commons-logging):由于项目的日志打印必然选择两个框架中至少一个,如果有人想换成其他日志组件,如log4j换成JUL,因为api完全不同,就需要改动代码。Apache见此,开发了JCL(Jakarta Commons Logging)。JCL 是一个Log Facade,只提供 Log API,不提供实现,然后有 Adapter 来使用 Log4j 或者 JUL 作为Log Implementation。
在程序中日志创建和记录都是用JCL中的接口,在真正运行时,会看当前ClassPath中有什么实现,如果有Log4j 就是用 Log4j, 如果啥都没有就是用 JDK 的 JUL。
这样,在你的项目中,还有第三方的项目中,大家记录日志都使用 JCL 的接口,然后最终运行程序时,可以按照自己的需求(或者喜好)来选择使用合适的Log Implementation。如果用Log4j, 就添加 Log4j 的jar包进去,然后写一个 Log4j 的配置文件;如果喜欢用JUL,就只需要写个 JUL 的配置文件。如果有其他的新的日志库出现,也只需要它提供一个Adapter,运行的时候把这个日志库的 jar 包加进去。
不过,commons-logging对Log4j和JUL的配置问题兼容的并不好,使用commons-loggings还可能会遇到类加载问题,导致NoClassDefFoundError的错误出现。到这个时候一切看起来都很简单,很美好。接口和实现做了良好的分离,在统一的JCL之下,不改变任何代码,就可以通过配置就换用功能更强大,或者性能更好的日志库实现。SLF4J & Logback:这样看上去也挺美好的,但是log4j的作者觉得JCL不好用,自己开发出SLF4J(Simple Logging Facade for Java),它跟JCL类似,本身不替供日志具体实现,只对外提供接口或门面。目的就是为了替代JCL。同时,还开发出logback,一个比log4j拥有更高性能的组件,目的是为了替代log4j。
SLF4J 是用来在log和代码层之间起到门面作用,类似于 JCL 的 Log Facade。对于用户来说只要使用SLF4J提供的接口,即可隐藏日志的具体实现,SLF4J提供的核心API是一些接口和一个LoggerFactory的工厂类,用户只需按照它提供的统一纪录日志接口,最终日志的格式、纪录级别、输出方式等可通过具体日志系统的配置来实现,因此可以灵活的切换日志系统。log4j2:Apache参考了logback,并做了一系列优化,推出了log4j2
二、Java日志框架的关系和依赖
1、依赖关系
JCL支持日志组件不多(spring默认使用JCL输入日志),所以基本上没太多人用,现在主要以slf4j体系为主
以上两张图的意思是如果你想用slf4j作为日志门面的话,你如何去配合使用其他日志实现组件,这里说明一下(注意jar包名缺少了版本号,在找版本时也要注意版本之间是否兼容)
除了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
2、JCL桥接到slf4j
对于新项目来说,一般会使用logback+slf4j日志框架。那对于依赖的第三方(如spring框架)使用了log4j或者jul的怎么办,如何统一输出?答案是桥接器。
比如:本项目使用的日志框架依赖是logback+slf4j日志框架
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.7</version>
</dependency>
可以添加如下的桥接器(将jul和log4j重定向到slf4j)
<!-- https://mvnrepository.com/artifact/org.slf4j/jcl-over-slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/log4j-over-slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
具体的思路:
1、确定日志Facade,一般就是slf4j
2、确定日志实现框架,不同的日志框架针对slf4j都提供绑定器,也就是说slf4j可以配合大部分日志实现使用
- slf4j-jdk14:slf4j到jdk-logging的桥梁
- slf4j-log4j12:slf4j到log4j1的桥梁
- log4j-slf4j-impl:slf4j到log4j2的桥梁这是apache实现,依赖的log4j版本较新。slf4j-log4j12这是slf4j中实现,依赖的log4j版本较低。实验可以互相替代,但是注意配置文件的不同。
- logback-classic:slf4j到logback的桥梁
- slf4j-jcl:slf4j到commons-logging的桥梁
3、管理依赖项目中的日志框架,拦截且重定向到slf4j中
- jul-to-slf4j:jdk-logging到slf4j的桥梁
- log4j-over-slf4j:log4j1到slf4j的桥梁
- jcl-over-slf4j:commons-logging到slf4j的桥梁
3、循环引用问题
想象这样一种情况,通过桥接器将log4j的日志全部重定向到slf4j,然后slf4j又再次绑定到了log4j实现,死循环了。
三、Java日志框架各个jar包作用总结
1、各个jar包的总结
-
log4j1
- log4j:log4j1的全部内容
-
log4j2
- log4j-api:log4j2定义的API
- log4j-core:log4j2上述API的实现
-
logback
- logback-core:logback的核心包
- logback-classic:logback实现了slf4j的API
-
commons-logging
- commons-logging:commons-logging的原生全部内容
- log4j-jcl:commons-logging到log4j2的桥梁
- jcl-over-slf4j:commons-logging到slf4j的桥梁
-
slf4j转向某个实际的日志框架 (如:使用slf4j的API进行编程,底层想使用log4j1来进行实际的日志输出,这就是slf4j-log4j12干的事。)
- slf4j-jdk14:slf4j到jdk-logging的桥梁
- slf4j-log4j12:slf4j到log4j1的桥梁
- log4j-slf4j-impl:slf4j到log4j2的桥梁
- logback-classic:slf4j到logback的桥梁
- slf4j-jcl:slf4j到commons-logging的桥梁
-
某个实际的日志框架转向slf4j(如使用log4j1的API进行编程,但是想最终通过logback来进行输出,所以就需要先将log4j1的日志输出转交给slf4j来输出,slf4j再交给logback来输出。将log4j1的输出转给slf4j,这就是log4j-over-slf4j做的事)
- jul-to-slf4j:jdk-logging到slf4j的桥梁
- log4j-over-slf4j:log4j1到slf4j的桥梁
- jcl-over-slf4j:commons-logging到slf4j的桥梁
2、集成总结
commons-logging与其他日志框架集成
- commons-logging与jdk-logging集成,需要的jar包:
- commons-logging
- commons-logging与log4j1集成,需要的jar包:
- commons-logging
- log4j
- commons-logging与log4j2集成,需要的jar包:
- commons-logging
- log4j-api
- log4j-core
- log4j-jcl(集成包)
- commons-logging与logback,需要的jar包:
- logback-core
- logback-classic
- slf4j-api、jcl-over-slf4j(2个集成包,可以不再需要commons-logging)
- commons-logging与slf4j集成,需要的jar包:
- jcl-over-slf4j(集成包,不再需要commons-logging)
- slf4j-api
slf4j与其他日志框架集成
- slf4j与jdk-logging集成,需要的jar包:
- slf4j-api
- slf4j-jdk14(集成包)
- slf4j与log4j1集成,需要的jar包:
- slf4j-api
- log4j
- slf4j-log4j12(集成包)
- slf4j与log4j2集成,需要的jar包:
- slf4j-api
- log4j-api
- log4j-core
- log4j-slf4j-impl(集成包)
- slf4j与logback集成,需要的jar包:
- slf4j-api
- logback-core
- logback-classic(集成包)
slf4j与commons-logging集成,需要的jar包: - slf4j-api
- commons-logging
- slf4j-jcl(集成包)
四、Java日志框架最佳实践
1、最佳实践建议:
- 总是使用 Log Facade,而不是具体的 Log Implementation
- 只添加一个 Log Implementation 依赖
- 具体的日志依赖应该设置为 optional,并使用 runtime scope
设为optional,依赖不会传递,这样如果你是个lib项目,然后别的项目使用了你这个lib,不会被引入不想要的Log Implementation 依赖;
Scope设置为runtime,是为了防止开发人员在项目中直接使用Log Implementation中的类,而不使用Log Facade中的类。 - 如果有必要, 排除依赖的第三方库中的Log Impementation依赖
这是很常见的一个问题,第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为optional,然后你的项目继承了这些依赖——具体的日志实现未必是你想使用的,比如他依赖了Log4j,你想使用Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和Log实现,也极容易形成环。
这种情况下,推荐的处理方法,是使用exclude来排除所有的这些Log实现和桥接器的依赖,只保留第三方库里面对Log Facade的依赖。
2、阿里Java日志规范
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架
SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);