Java日志体系居然这么复杂?——架构篇

日志到底是何方神圣?为什么要使用日志框架?

想必大家都有过使用 System.out 来进行输出调试,开发开发环境下这样做当然很方便,但是线上这样做就有麻烦了:

系统一直运行,输出越来越多,磁盘空间逐渐被写满

不同的业务想要把日志输出在不同的位置

有些场合为了更高性能,尽量控制减少日志输出,需要动态调整日志输出量

自动输出日志相关信息,比如:日期、线程、方法名称等等

显然 System.out 解决不了我们的问题,但是我们遇到的问题一定会有前人遇到过,日志也不例外,其中就有一个大牛 Ceki ,整个Java的日志体系几乎都有Ceki参与或者受到了Ceki的深度影响。当然Java日志体系的复杂度也有一部分原因是拜这位大牛所赐。

Java日志的恩怨情仇

1996年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j(由Ceki创建)。

后来Log4j成为Apache基金会项目中的一员,Ceki也加入Apache组织。后来Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了。

2002年Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,Log4j就已经成为一项成熟的技术,使得Log4j在选择上占据了一定的优势。

接着,Apache推出了Jakarta Commons Logging,JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是Log4j,也可以是Java Util Logging。

后来(2006年),Ceki不适应Apache的工作方式,离开了Apache。然后先后创建了Slf4j(日志门面接口,类似于Commons Logging)和Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。

Java日志领域被划分为两大阵营:Commons Logging阵营和Slf4j阵营。

Commons Logging在Apache大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013年底有人分析了GitHub上30000个项目,统计出了最流行的100个Libraries,可以看出Slf4j的发展趋势更好。

Apache眼看有被Logback反超的势头,于2012-07重写了Log4j 1.x,成立了新的项目Log4j 2, Log4j 2具有Logback的所有特性。

如今日志框架已经发展为:Slf4j作为API,实现分为logback与log4j(Commons Logging因为效率和API设计等问题,现在逐渐淡出舞台了)

让我们来瞻仰一下大神,哈哈:

那么如何在混乱的Java日志体系中如何优雅的使用日志呢?

其实在Ceki设计的体系下,日志如同Java的JDBC、Servelt等一样,定义好标准后实现可以互相切换,问题在于定标准的人各自为政搞出来好多标准,JCL、SLF4j等等,官方(Sun公司)又晚又不给力,发展到现在终于被SLF4j以一种巧妙的方式(桥接、绑定,见下文)统一了,标准使用方式如下图:

这个图截取自 slf4j手册 ,简化了多余部分,很清晰的表示了使用方式:

应用引用SLF4j-API(编码时使用SLF4j的接口 org.slf4j.Logger ,而非logback或log4j的实现)

logbak: slf4j会自动查找logback实现(logback默认实现了slf4j)

log4j:使用起来基本一致,只不过多了适配器层,引用了slf4j-log4j12.jar,官方称为绑定(concrete-bindings),就是将SLF4j-API绑定到log4j最终输出日志

具体依赖如下

logback

<dependency>  <groupId>ch.qos.logback</groupId>  <artifactId>logback-classic</artifactId>  <version>1.2.3</version></dependency>

log4j2

<dependency>  <groupId>org.apache.logging.log4j</groupId>  <artifactId>log4j-slf4j-impl</artifactId>  <version>2.12.1</version></dependency><dependency>  <groupId>org.apache.logging.log4j</groupId>  <artifactId>log4j-core</artifactId>  <version>2.12.1</version></dependency>

得益于maven的依赖传递机制,我们不需要显示声明依赖SLF4j-API.jar。

可以看到,log4j 多依赖了 log4j-slf4j-impl.jar,其实就是上图所示的适配器层,读者可能好奇,为什么使用 log4j 会有 适配器 层?其原因在于,slf4j 并不是官方规范,所以没人遵守(也就是自己的日志框架中没有原生实现 org.slf4j.Logger 接口,如 log4j ),而绑定层( log4j-slf4j-impl.jar)的作用就是通过静态查找的方式将使用log4j作为实现(具体原理请关注后续文章),这样就是实现了不依赖log4j而使用log4j输出日志(面向接口编程的最佳实践,Ceki 大神就是用这套思想将 slf4j 做成了 Java 日志的标准,烂牌翻盘的典范)。

上面这一段讲解了绑定(concrete-bindings)思想,是本文的精髓,读者一定要理解这里,后面还有桥接思想与之类似,请继续阅读。

小结

至此我们已经完成了日志的整合,但是事情真的这么简单吗?

先梳理一下,如此混乱的日志体系下(slf4j,jul,jcl,logback,log4j)会不会会产生什么问题?答案是一定的,各种第三方库使用了不同的日志框架,如果我们依赖 Spring ,Spring(非boot)的默认日志实现是JCL、又或者我们已有项目已经使用了Log4j,想使用logback的话,难道要逐个类改代码吗(官方有迁移工具)?我们能不能只用一种框架来处理JUL(java.util.logging)、JCL(Jakarta Commons Logging)、Log4j1、Log4j2 呢?

答案是肯定的,Ceki 的 Slf4j 给出了解决方案,就是上文所说的桥接( Bridging legacy),简单来说就是劫持以上所以第三方日志输出并重定向至 SLF4j,最终实现统一日志上层 API(编码) 与下层实现(输出日志位置、格式统一)。我们来看一下图示

上图左侧就是前一张图的 logback 日志实现,为了兼容其他日志,我们需要引用右侧的桥接包:xxx-over/to-slf4j.jar ,xxx对应日志框架,使用 logback 的情况下,除了上文的 logback 依赖,还需要引入以下依赖才能保证所有日志都被桥接至slf4j。

如何桥接?

logback 如下

<dependency>  <groupId>ch.qos.logback</groupId>  <artifactId>logback-classic</artifactId>  <version>1.2.3</version></dependency><dependency>  <groupId>org.slf4j</groupId>  <artifactId>jcl-over-slf4j</artifactId></dependency><dependency>  <groupId>org.slf4j</groupId>  <artifactId>jul-to-slf4j</artifactId></dependency><!-- log4j 桥接包,slf4j官方实现,另有log4j官方实现,二选一即可 log4j-to-slf4j--><dependency>  <groupId>org.slf4j</groupId>  <artifactId>log4j-over-slf4j</artifactId></dependency>

log4j2 如下

<dependency>  <groupId>org.apache.logging.log4j</groupId>  <artifactId>log4j-slf4j-impl</artifactId>  <version>2.12.1</version></dependency><dependency>  <groupId>org.apache.logging.log4j</groupId>  <artifactId>log4j-core</artifactId>  <version>2.12.1</version></dependency><!-- 以下是桥接包,使用了log4j作为底层实现,    不能再桥接log4j,否则会出现无限递归的情况(具体原因请关注后续文章) --><dependency>  <groupId>org.slf4j</groupId>  <artifactId>jcl-over-slf4j</artifactId></dependency><dependency>  <groupId>org.slf4j</groupId>  <artifactId>jul-to-slf4j</artifactId></dependency>

SpringBoot 项目引用了一部分依赖,所以使用起来略微有些不同:

logback 如下

<!-- logback作为内置实现,使用相对简单 --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter</artifactId></dependency><!-- 引入缺少的桥接包 --><dependency>    <groupId>org.slf4j</groupId>    <artifactId>jcl-over-slf4j</artifactId></dependency>

log4j2 如下

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter</artifactId>  <!-- 使用log4j2要排除logback依赖 -->  <exclusions>      <exclusion>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-logging</artifactId>      </exclusion>  </exclusions></dependency><!-- Spring已经写好了一个log4j2-starter但缺少桥接包 --><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-log4j2</artifactId></dependency><!-- 引入缺少的桥接包 --><dependency>  <groupId>org.slf4j</groupId>  <artifactId>jcl-over-slf4j</artifactId></dependency>

结束语

以上两种才是项目中的最佳使用方式,其他笔者不推荐使用。

最后来看一下 slf4j 如何使用:

static final org.slf4j.Logger logger =                    LoggerFactory.getLogger(TestLog.class);logger.trace("A TRACE Message");logger.debug("A DEBUG Message");logger.info("An INFO Message");logger.warn("A WARN Message");logger.error("An ERROR Message");

这样使用我们就可以随意切换日志实现而无需改动代码,操作起来也简单,只需要按照上文切换依赖即可。至于其他使用细节本文不在赘述,关注后续文章(最佳实践、配置文件、原理、扩展等)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容