title: 【Java深刻学习系列】三. 那些年咱们用过的日志框架
date: 2016-10-16 15:32:50html
目前常见的Java日志框架和facades(中文彷佛不太好翻译)有一下几种:java
其中,①-③为同一个做者(Ceki)所写。④被不少开源项目所用,⑤是Java原生库(如下用j.u.l简写来代替),可是在Java 1.4中才被引入。数据库
这么多的日志库,咱们该如何选择呢,我认为,这并不是一道非此即彼的选择题,可是在了解它们的历史渊源和优劣以及相互关系的基础上才能更好地适配本身的项目。apache
下面我将上述这些框架串起来说一下,若有疏漏请见谅。segmentfault
在上古时期,Java打日志依赖System.out.println()
, System.err.println()
或者e.printStackTrace()
。Debug日志被写入STDOUT
流,错误日志被写入STDERR
流。api
这种方式目前小脚本中也依然使用普遍。可是在生产环境或大的项目中,Debug日志一般被重定向到/dev/null中: >/dev/null
, 错误日志被重定向到本地文件中: 2>stderr.log
。看起来很完美,是吗?实则否则,这样打日志有一个很是大的缺陷:没法可定制化。bash
具体来说,没有一个相似开关的东东来切换是否打印Debug日志,当咱们定位问题时须要输出Debug日志到文件去查看,而不是到/dev/null
里,是吗?日志没法定制化,咱们只能硬编码到代码里,不须要时再注释掉相关代码,从新编译。app
还有一些缺陷,好比:没法更细粒度地输出日志,换句话说,缺乏当前成熟的日志框架常见的LOG LEVEL控制。框架
而Java自己也没有提供相应的Library,在这样恶劣的境况下,Log4j勇敢地站了出来,拯救劳苦大众。jvm
Log4j能够说是一个里程碑式的框架,它提出的一些基本理念,深深地影响了后来者,直至今天,这些理念也依然在被普遍使用:
咱们来看下维基百科对Logger
的定义:
A Logger is an object that allows the application to log without regard to where the output is sent/stored. The application logs a message by passing an object or an object and an exception with an optional severity level to the logger object under a given a name/identifier.
Logger是一个容许应用记录日志的对象,开发者没必要需考虑输出位置。应用可将具体须要打印的信息经过一个Object传递。每一个Logger互相独立,经过名字或标识符来区分。
每一个appender可独立配置记录日志的设备,能够是文件、数据库、消息系统等。
每一个打印日志均可以单独制定日志级别。外部经过配置文件来控制输出级别,不一样的输出级别打印不一样的日志信息。
J.U.L
姗姗来迟后来,Sun公司开始意识到JDK须要一个记录日志的特性。受Log4j的启发,Sun在Java1.4版本中引入了一个新的API, 叫java.util.logging
, 可是,j.u.l
功能远不如Log4j完善,若是开发者要使用它,就意味着须要本身写Appenders
(Sun称它为Handlers
),并且,只有两个Handlers
可被使用:Console
和File
,这就意味着,开发者只能将日志写入Console和文件。
如前面所述,j.u.l
在Java 1.4才被引入,在这以前,并无官方的日志库供开发者使用。因而便有了不少日志相关的"轮子"。我想这应该是当前会有如此多日志框架的一个很重要的缘由。
回顾历史,一方面,在Java 1.4以前,第三方日志库已经被普遍使用了,占得了先机。另外一方面,j.u.l
在被引入时性能和可用性都不好,直到1.5甚至之后才有了显著提高。
因为项目的日志打印必然依赖以上两个框架中至少一个,不管是j.u.l
仍是log4j
,开发者必须去两个都配置。这时候,Apache的commons-logging
出现了。本质上来说,commons-logging
并不是一个日志打印框架,而是一个API bridge, 它起到一个链接和沟通的做用,开发者可使用它来兼容logging frameworks(j.u.l
和log4j
)。有了它,第三方库就可使用commons-logging
来作一个中间层,去灵活选择j.u.l
或者log4j
,而没必要强加依赖。
然而commons-logging
对j.u.l
和log4j
的配置问题兼容得并很差,更糟糕的是,使用commons-logging
可能会遇到类加载问题,致使NoClassDefFoundError
的错误出现。
最终,log4j
的创始人Ceki发起了另外一个项目,这即是大名鼎鼎的SLF4j
日志框架,该框架能够当作是log4j
的升级版。须要说明的是,log4j 2.0已经被加入Apache基金会,过去几年已经被大幅改善,社区活跃度也很是高,借助开源社区的力量,log4j 2.0目前被加入愈来愈多得现代化特性,必定程度上,甚至超越了log4j
的升级版logback
(稍后介绍),关于log4j 2.0的新特性,请参见这篇文章:THE NEW LOG4J 2.0
据slf4j
的做者Ceki说,首先,slf4j是不只仅是一个logging framework, 并且一个logging facdes, 借助slf4j
的log4j adapter, 开发者从slf4j切换到log4j不须要额外改动一行代码,只须要从CLASS_PATH中排除掉slf4j-log4j12.jar。若是想从log4j
迁移到logback
, 在CLASS_PATH添加slf4j-log4j12.jar, 并将log4j.properties转换为logback.xml便可,这里有一个在线工具能够自动完成转换: logback.xml translator。
slf4j提供了很大的灵活度,开发者能够借助它去灵活选择底层的日志框架。好比,当下更多的开发者比较倾向于使用log4j的升级版logback,由于它具备较log4j更多更好的特性:
须要说明的是,logback是slf4j接口的一套具体实现,又是同一个做者,于是保证了其和log4j相近的使用方式,也具备slf4j的所有特性。
此外,对于一些大型框架及服务的开发者,须要考虑客户端用户的体验。好比jstorm, 你不能只考虑本身的喜爱,或许有人偏好使用slf4j
开发jstorm topology, 而另外一些人喜欢用logback。这种状况下,你应该使用slf4j
,把最终logging framework的选择权留给用户。
最后,除了slf4j
比j.u.l
或者log4j
更好用,还有一个选择slf4j
的现实缘由:Java圈的很是多开发者更钟情于slf4j
做为他们的logging API, 随大流有时候能少不少没必要要的麻烦。
slf4j除了包含该log4j的所有特性外,还提供了parameterized logging特性。这个特性很是有用,它容许开发者在打印日志时借助{}
来实现参数化打印:
logger.debug("The attribute value is {}", fooIns.getAttribute());
logback复用了slf4j的API,这意味着使用logback其实是在使用slf4j的API,不难看出,logback一样支持parameterized logging特性。
以上日志框架,有些是为了解决现有框架的不足,有些是功能的扩展升级,有些是从头至尾从新写的,根据各自出现前后次序,能够将它们放在同一时间线上:
注意箭头仅表明时间走向,分支不具备fork的含义。
slf4j的使用有两种方式,一种是混合绑定(concrete-bindings), 另外一种是桥接遗产(bridging-legacy).
concrete-bindings模式指在新项目中 即开发者直接使用sl4j的api来打印日志, 而底层绑定任意一种日志框架,如logback, log4j, j.u.l等.
混合绑定根据实现原理,基本上有两种形式, 分别为有适配器(adapter)的绑定和无适配器的绑定.
有适配器的混合绑定是指底层没有实现slf4j的接口,而是经过适配器直接调用底层日志框架的Logger, 无适配器的绑定不须要调用其它日志框架的Logger, 其自己就实现了slf4j的所有接口.
几个混合绑定的包分别是:
以上几种绑定能够无缝切换, 不须要改动内部代码. 不管哪一种绑定,均依赖slf4j-api.jar.
此外, 适配器绑定须要一种具体的日志框架, 如log4j绑定slf4j-log4j12-1.7.21.jar依赖log4j.jar, j.u.l绑定slf4j-jdk14-1.7.21.jar依赖j.u.l(java runtime提供); 无适配器的直接实现, logback-classic依赖logback-core提供底层功能, slf4j-simple则不依赖其它库.
以上四种绑定的示例图以下:
下面来分析两个典型绑定log4j和logback的用法.
①log4j适配器绑定(slf4j-log4j12)
配置:
<!--pom.xml--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency>
注意: 添加上述适配器绑定配置后会自动拉下来两个依赖库, 分别是slf4j-api-1.7.21.jar和log4j-1.2.17.jar
基本逻辑: 用户层 <- 中间层 <- 底层基础日志框架层
org.slf4j.impl.Log4jLoggerFactory <- StaticLoggerBinder.getSingleton().getLoggerFactory()<- org.sl4j.impl.StaticLoggerBinder.getSingleton().getLoggerFactory() <- 具体的日志框库的Logger
其中org.slf4j.impl.Log4jLoggerFactory在应用层调用, StaticLoggerBinder在中间层实现, 获取具体的日志框库的Logger
绑定实例图以下:
应用层(slf4j-api-1.7.21.jar)
用户调用org.slf4j.impl.Log4jLoggerFactory.getLogger获取底层具体的Logger:
// Foo.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Foo { private final static Logger logger = LoggerFactory.getLogger(CommuteNaviInfoParser.class); public static void main() { logger.info("info:{}..", "hello, sl4j"); } }
适配层(slf4j-log4j12-1.7.21.jar)
由应用层org.slf4j.impl.Log4jLoggerFactory.getLogger内部建立适配层的StaticLoggerBinder:
public static Logger getLogger(Class<?> clazz) { return StaticLoggerBinder.getSingleton().getLoggerFactory(); }
接下来直接由StaticLoggerBinder获取具体的Logger:
private StaticLoggerBinder() { loggerFactory = new Log4jLoggerFactory(); } public Log4jLoggerFactory() { // force log4j to initialize org.apache.log4j.LogManager.getRootLogger(); }
注意, 各个StaticLoggerBinder均在适配层实现, 放在org.slf4j.impl中.
② slf4j绑定到logback-classic上
配置:
<!--pom.xml--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.7</version> </dependency>
注意: 添加上述适配器绑定配置后会自动拉下来两个依赖库, 分别是slf4j-api-1.7.21.jar和logback-core-1.0.13.jar
logback-classic没有适配器层, 而是在logback-classic-1.0.13.jar的ch.qos.logback.classic.Logger
直接实现了slf4j的org.slf4j.Logger
, 并强依赖ch.qos.logback.core
中的大量基础类:
import org.slf4j.LoggerFactory; import org.slf4j.Marker; import org.slf4j.spi.LocationAwareLogger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.classic.util.LoggerNameUtil; import ch.qos.logback.core.Appender; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.spi.AppenderAttachable; import ch.qos.logback.core.spi.AppenderAttachableImpl; import ch.qos.logback.core.spi.FilterReply; public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {}
绑定示例图:
桥接遗产用法主要针对历史遗留项目, 不管是用log4j写的, j.c.l写的,仍是j.u.l写的, 均可以在不改动代码的状况下具备另一种日志框架的能力.
好比,你的项目使用java提供的原生日志库j.u.l写的, 使用slf4j的bridging-legacy模式,即可在不改动一行代码的状况下瞬间具备log4j的所有特性.
说得更直白一些,就是你的项目代码多是5年前写的, 当时因为没得选择, 用了一个比较垃圾的日志框架, 有各类缺陷和问题, 如不能按天存储, 不能控制大小, 支持的appender不多, 没法存入数据库等. 你很想对这个已完工并在线上运行的项目进行改造, 显然, 直接改代码, 把旧的日志框架替换掉是不现实的, 由于颇有可能引入不可预期的bug.
那么,如何在不修改代码的前提下, 替换掉旧的日志框架,引入更优秀且成熟的日志框架如如log4j和logback呢? slf4j的bridging-legacy模式即是为了解决这个痛点.
slf4j以slf4j-api为中间层, 将上层旧日志框架的消息转发到底层绑定的新日志框架上.
基于不一样的底层框架,以SLF4J做为中转层,有以下几种组合用法:
上述facade将slf4j-api.jar绑定到底层基础日志库j.u.l(jvm runtime)上. slf4j-api和底层日志库的Logger经过适配器链接.
举例说明上述facade的使用, 以便于你们理解.
假如我有一个已完成的使用了旧日志框架commons-loggings的项目,如今想把它替换成log4j以得到更多更好的特性.
项目的maven旧配置以下:
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
项目代码:
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Created by xialeizhou on 16/9/20. */ public class MainTest { private static Log logger = LogFactory.getLog(MainTest.class); public static void main(String[] args) throws InterruptedException { logger.info("hello,world"); } }
项目打印的基于commons-logging的日志显示在console上,具体以下:
十月 23, 2016 6:52:00 下午 MainTest main 信息: hello,world
下面咱们对项目改造, 将commongs-logging框架的日志转发到log4j上. 改造很简单, 咱们将commongs-logging依赖删除, 替换为相应的facade(此处为jcl-over-slf4j.jar), 并在facade下面挂一个5.1的混合绑定便可.
具体来说, 将commons-logging.jar
替换成jcl-over-slf4j.jar
, 并加入适配器slf4j-log412.jar(注意, 加入slf4j-log412.jar后会自动pull下来另外两个jar包), 因此实际最终只需添加facadejcl-over-slf4j.jar
和5.1节混合绑定中相同的jar包slf4j-log412.jar
便可.
改造后的maven配置:
<!--facade--> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.21</version> </dependency> <!--binding--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency>
如今, 咱们的旧项目在没有改一行代码的状况下具备了log4j的所有特性, 下面进行测试.
在resources/
下新建一个log4j.properties
文件, 对commongs-logging库的日志输出进行定制化:
# Root logger option log4j.rootLogger=INFO, stdout, fout # Redirect log messages to console log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.Threshold = INFO log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n # add a FileAppender to the logger fout log4j.appender.fout=org.apache.log4j.FileAppender # create a log file log4j.appender.fout.File=royce-testing.log log4j.appender.fout.layout=org.apache.log4j.PatternLayout # use a more detailed message pattern log4j.appender.fout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
从新编译运行, console输出变为:
2016-10-23 19:26:15 INFO MainTest:11 - hello,world
同时在当前目录生成了一个日志文件:
% cat royce-testing.log INFO 2016-10-23 19:26:15,341 0 MainTest [main] hello,world
可见, 基于facade的日志框架桥接已经生效, 咱们再不改动代码的前提下,让commons-logging日志框架具备了log4j12的所有特性.