侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 292 篇文章
  • 累计创建 132 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

理清JAVA日志体系、框架组成、推荐组合及常见问题处理

孔子说JAVA
2021-09-26 / 0 评论 / 0 点赞 / 137 阅读 / 8,882 字 / 正在检测是否收录...

对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。java领域存在多种日志框架,在具体项目中如何选用适合的日志框架,日志相关的Jar包应该引入哪些,排除那些,互相之间的依赖关系等,时不时会困扰着我们,本文通过介绍日志框架的发展史、组成部分,弄明白每种技术出现的背景/需要应对的场景,理清整体的脉络,区分清楚Java众多日志包的关系(分清SLF4J、Logback、Log4j、Logging等的区别与联系),明白如何选择日志框架及进行日志转换,从全局上把握技术方向。

1、日志框架发展史(按时间排序)

1.1 Log4j

Apache基金会最早实现的一套日志框架,在Java1.4之前只有这一种选择。谁能想到Java1.4之前,JDK都没有内置的日志功能!Apache Log4j 是一个非常古老的日志框架,并且是多年来最受欢迎的日志框架。 它引入了现代日志框架仍在使用的基本概念,如分层日志级别和记录器。

  • 2015 年 8 月 5 日,该项目管理委员会宣布 Log4j 1.x 已达到使用寿命。 建议用户使用 Log4j 1 升级到 Apache Log4j 2。

1.2 J.U.L(jdk-logging)

终于在2002年Java1.4发布,Sun推出了自己的日志库J.U.L(jdk-logging)。但基本上是模仿Log4j的实现。有点儿鸡肋,但最起码解决了有无的问题。从此开发者有了两种选择。

1.3 J.C.L(commons-logging)

因为有了两种选择,所以导致了日志使用的混乱。所以Apache推出了J.C.L(commons-logging),最早叫Jakarta Commons Logging,后更名为Commons Logging。它只是定义了一套日志接口,支持运行时动态加载日志组件。应用层编写代码时,只需要使用J.C.L提供的统一接口来记录日志,在程序运行时会优先找系统是否集成Log4j,如果集成则使用Log4j做为日志实现,如果没找到则使用J.U.L做为日志实现。J.C.L的出现解决了多种日志框架共存的尴尬,也是面向接口编程思想的一种具体体现。

1.4 Slf4j(Simple Logging Facade for Java)

2006年,Log4j的作者Ceki Gülcü离开Apache后,又搞出来一套类似J.C.L的接口类,就是Slf4j(2006年发布第一个版本,最近一个稳定版本在2017年发布)。原因是作者觉得J.C.L这套接口设计的不好,容易让开发者写出有性能问题的代码。Slf4j做为一套标准接口,可以实现无缝与多种实现框架进行对接,本身并无日志的实现,是现在比较常用的日志集成方式。

  • SLF4J(Simple logging Facade for Java)意思为简单日志门面,它是把不同的日志系统的实现进行了具体的抽象化,只提供了统一的日志使用接口,使用时只需要按照其提供的接口方法进行调用即可,由于它只是一个接口,并不是一个具体的可以直接单独使用的日志框架,所以最终日志的格式、记录级别、输出方式等都要通过接口绑定的具体的日志系统来实现,这些具体的日志系统就有log4j,logback,java.util.logging等,它们才实现了具体的日志系统的功能。

1.5 Logback

在搞出来Slf4j之后,Log4j 创始人Ceki Gülcü又顺带开发了Logback,做为Slf4j的默认实现(2006年发布第一个版本,最近一个稳定版本在2017年发布)。在功能完整度和性能上,Logback超越了之前已有的日志实现框架。

Logback 的体系结构足够通用,以便在不同情况下应用。 目前,logback 分为三个模块: logback-core,logback-classic和logback-access

  • logback-core:该模块为其他两个模块的基础。
  • logback-classic:该模块可以被看做是log4j的改进版本。此外,logback-classic 本身实现了 SLF4J API,因此可以在 logback 和其他日志框架(如 log4j 或 java.util.logging(JUL))之间来回切换。
  • logback-access:模块与 Servlet 容器(如 Tomcat 和 Jetty)集成,以提供 HTTP 访问日志功能。

1.6 Log4j2

2012年,Apache重写了Log4j,实现了Log4j2。在功能上面具有Logback的所有特性。算是目前功能最完善的日志框架。Log4j2 是 Log4j 的下一个版本,与 Log4j2 发生了很大的变化,Log4j2 不兼容 Log4j1.x,设计上很大程度上模仿了 SLF4J/Logback,并提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些固有问题,性能上获得了很大的提升。Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j2-api 和 log4j2-core。

  • 与 Logback 一样,Log4j2 提供对 SLF4J 的支持,自动重新加载日志配置,并支持高级过滤选项。 除了这些功能外,它还允许基于 lambda 表达式对日志语句进行延迟评估,为低延迟系统提供异步记录器,并提供无垃圾模式以避免由垃圾收集器操作引起的任何延迟。所有这些功能使 Log4j2 成为现有日志框架中最先进和最快的

2、日志框架组成部分

项目中有一堆日志相关的Jar包,到底应该引入哪些?排除哪些?哪些与哪些互相冲突?哪些与哪些又是相互依赖的?这类问题有时候容易让人崩溃!因此理清Java众多日志包的关系,明白如何进行日志转换就显得尤为重要。

按日志包的作用对众多的日志相关Jar包进行分类,主要分为三类:

2.1 接口/门面类(3个)

只提供API定义,没有提供具体实现。目的是为应用层提供标准化的使用方式。既所谓的面向接口编程。

SLF4J
J.C.L(commons-logging)
Log4j2

2.2 实现类(4个)

具体的日志实现类,提供对日志的收集/管理功能。受不同需求/不同历史环境影响,各框架功能上有许多不同。但遵循进化论规律。

Log4j
J.U.L(jdk-logging)
Log4j2
Logback

2.3 桥接类(12个)

多种日志实现框架混用情况下,需要借助桥接类进行日志的转换,最后统一成一种进行输出。

slf4j-jdk14
slf4j-jcl
slf4j-log4j12
logback-classic
jul-to-slf4j
jcl-over-slf4j
log4j-over-slf4j
log4j-slf4j-impl
log4j-to-slf4j
log4j-jul
log4j-jcl
log4j-12-api

3、日志框架各部分的作用

3.1 桥接关系图

根据日志相关jar包的分类可以看到,有3个log接口及4个log实现,以及使各个日志框架相互兼容的9个桥接类,下图为各日志接口、实现类、桥接类的关系图(其中紫色背景的为接口类、蓝色背景的为实现类,白色背景的为桥接类)。

image-1649088825017

  • 图上列举出了多种日志实现框架转换成JCL、Slf4j接口和Log4j 2接口绑定多种日志实现框架所涉及到的相关Jar包。通过这些桥接包可以轻松实现项目中日志框架的统一。对于哪些包需要引入/哪些包需要排除也就一目了然了。

3.2 接口、实现类、桥接类的作用

3.2.1 接口/门面类的作用

接口/门面类如SLF4J,是把不同的日志系统的实现进行了具体的抽象化, 只提供了统一的日志使用接口 ,使用时只需要按照其提供的接口方法进行调用即可,由于它只是一个接口,并不是一个具体的可以直接单独使用的日志框架,所以最终日志的格式、记录级别、输出方式等都要通过接口绑定的具体的日志系统来实现,这些 具体的日志系统就有log4j, logback, java.util.logging 等,它们才实现了具体的日志系统的功能。

  • 使用了接口/门面类如SLF4J,有利于根据自己实际的需求更换具体的日志系统,比如,之前使用的具体的日志系统为log4j,想更换为logback时,只需要删除log4j相关的jar,然后加入logback相关的jar和日志配置文件即可,而不需要改动具体的日志输出方法。试想如果没有采用这种方式,当你的系统中日志输出有成千上万条时,你要更换日志系统将是多么庞大的一项工程。如果你开发的是一个面向公众使用的组件或公共服务模块,那么一定要使用slf4j的这种形式,这有利于别人在调用你的模块时保持和他系统中其他模块使用统一的日志输出。

3.2.2 实现类的作用

具体的日志实现类,提供对日志的收集/管理功能。受不同需求/不同历史环境影响,各框架功能上有许多不同。如log4j、common logging、jdk log、logback、log4j2日志实现包等。

3.2.3 桥接器的作用

接口/门面类如SLF4J,只提供一个核心slf4j api(就是slf4j-api.jar包),这个包只有日志的接口,并没有实现,所以如果要使用就得再给它提供一个实现了些接口的日志包,比如:log4j、common logging、jdk log日志实现包等,但是这些日志实现又不能通过接口直接调用,实现上他们根本就和slf4j-api不一致,因此slf4j又增加了一层来转换各日志实现包的使用,当然slf4j-simple除外。

1)SLF4J桥接到具体日志框架

桥接器是对某套API的伪实现,这种实现并不是直接去完成API所声明的功能,而是去调用有类似功能的别的API。这样就完成了从“某套API”到“别的API”的转调。下图可以看到 slf4j与具体日志框架结合 的方案,来自 SL4J官网文档

image-1649089433641

  • 每种方案的最上层(绿色的应用层)都是统一的,它们向下都是直接调用slf4j提供的API(浅蓝色的抽象API层),依赖slf4j-api.jar。
  • 图中的第二层是浅蓝色的,看左下角的图例可知这代表抽象日志API,也就是说它们不是具体实现。如果像左边第一种方案那样下层没有跟任何具体日志框架实现相结合,那么日志是无法输出来的。然后slf4j API向下再怎么做就非常自由了,几乎可以使用所有的具体日志框架。
  • 图中的第三层中间的两个湖蓝色块,这是适配层,也就是桥接器。左边的slf4j-log4j12.jar桥接器看名字就知道是slf4j到log4j的桥接器,同样,右边的slf4j-jdk14.jar就是slf4j到Java原生日志实现的桥接器了。它们的下一层分别是对应的日志框架实现,log4j的实现代码是log4j.jar,而jul实现代码已经包含在了JVM runtime中,不需要单独的jar包。
  • 图中的第三层其余的三个深蓝色块,是具体的日志框架实现,但是却不需要桥接器,因为它们本身就已经直接实现了slf4j API。slf4j-simple.jar和slf4j-nop.jar这两个不用多说,看名字就知道一个是slf4j的简单实现,一个是slf4j的空实现,平时用处也不大。而logback之所以也实现了slf4j API,是因为logback和slf4j出自同一人之手,这人同时也是log4j的作者。
  • 第三层所有的灰色jar包都带有红框,这表示它们都直接实现了slf4j API,只是湖蓝色的桥接器对slf4j API的实现并不是直接输出日志,而是转去调用别的日志框架的API。

2)其它日志框架API转调回slf4j

如果只存在上面这些从sfl4j到其他日志框架的桥接器,可能还不会出什么问题。但是实际上还有另外一类桥接器,它们的作用跟上面的恰好相反,它们将其它日志框架的API转调到slf4j的API上。下图来自 slf4j官网文档Bridging legacy APIs

image-1649089489925

  • 上图展示了目前为止能安全地从别的日志框架API转调回slf4j的所有三种情形。

  • 以左上角第一种情形为例,当slf4j底层桥接到logback框架的时候,上层允许桥接回slf4j的日志框架API有log4j和jul。jcl虽然不是什么日志框架的具体实现,但是它的API仍然是能够被转调回slf4j的。要想实现转调,方法就是图上列出的用特定的桥接器jar替换掉原有的日志框架jar。需要注意的是这里不包含logback API到slf4j API的转调,因为logback本来就是slf4j API的实现。

看完三种情形以后,会发现几乎所有其他日志框架的API,包括jcl的API,都能够随意的转调回slf4j。但是有一个唯一的限制就是转调回slf4j的日志框架不能跟slf4j当前桥接到的日志框架相同。这个限制就是为了防止A-to-B.jar跟B-to-A.jar同时出现在类路径中,从而导致A和B一直不停地互相递归调用,最后堆栈溢出。目前这个限制并不是通过技术保证的,仅仅靠开发者自己保证,这也是为什么slf4j官网上要强调所有合理的方式只有上图的三种情形。

3.3 日志框架处理流程

slf4j 结合 jdk-logging 输出日志,需要slf4j-jdk14.jar 桥接器,具体的日志操作的过程:首先调用 slf4j 的 API 接口,然后 slf4j 将请求转发给slf4j-jdk14来处理,这个请求转发到jdk14是在编译期间静态绑定好的(具体日志实现是在运行时动态绑定的),然后slf4j-jdk14最后通过jdk14(java.util.logging)来完成日志操作。java.util.logging实现代码已经包含在了JVM runtime中,不需要单独的jar包。调用过程如下:

slf4j-api(接口层) 
   | 
各日志实现包的连接层( slf4j-jdk14, slf4j-log4j) 
   | 
各日志实现包

  • 注意事项:如果项目中有桥接需要,注意在类路径中不要同时出现互相递归调用的情况。比如:a-to-b.jar和b-to-a.jar同时出现在类路径中,从而导致a和b一直不停地互相递归调用,最后堆栈溢出。

3.4 SLF4J结合具体日志实现框架所需jar

SLF4J与各日志框架结合时,除了必需的slf4j-api.jar外,还需要具体日志框架jar及相关桥接类jar。以下列举几个常用的(注意,slf4j和各日志实现包结合使用时最好只使用一种结合,不然的话会提示重复绑定日志,并且会导致日志无法输出)。

  • SLF4J、logback结合所需jar: slf4j-api.jar, logback-classic.jar, logback-core.jar;
  • SLF4J、log4j结合所需jar: slf4j-api.jar, slf4j-log412.jar, log4j.jar;
  • SLF4J、java.util.logging结合所需jar: slf4j-api.jar, slf4j-jdk14.jar;
  • SLF4J、simple(SLF4J本身提供的一个接口的简单实现)结合所需jar: slf4j-api.jar, slf4j-simple.jar。

4、推荐组合

4.1 Slf4j+Logback

Slf4j+Logback 算是JAVA 里一个老牌的日志框架,从06年开始第一个版本,迭代至今也十几年了。使用已经非常成熟了,在网络上也可以找到很多有用的的资源。

  • Slf4j实现机制决定Slf4j限制较少,使用范围更广。由于Slf4j在编译期间,静态绑定本地的LOG库使得通用性要比Commons Logging要好。
  • Logback拥有更好的性能。Logback声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高。这个操作在Logback中需要3纳秒,而在Log4J中则需要30纳秒。LogBack创建记录器(logger)的速度也更快:13毫秒,而在Log4J中需要23毫秒。更重要的是,它获取已存在的记录器只需94纳秒,而Log4J需要2234纳秒,时间减少到了1/23。跟JUL相比的性能提高也是显著的。
  • Logback文档免费。Logback的所有文档是全面免费提供的,不象Log4J那样只提供部分免费文档而需要用户去购买付费文档。

4.2 Log4j2

Apache Log4j2是Log4j1的升级版本。Log4j2基本上把Log4j1版本的核心全部重构掉了,而且基于Log4j1做了很多优化和改变。并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些固有问题,和logback对比有很大的改进。除了内部设计的调整外,主要有以下几点的大升级:

  • 更简化的配置
  • 更强大的参数格式化
  • 最夸张的异步性能

5、日志框架使用常见问题

5.1 log4j-over-slf4j、slf4j-log4j12递归调用导致堆栈异常

日志框架中,当log4j-over-slf4j、slf4j-log4j12这2个桥接jar包同时出现在classpath下时,有可能会递归调用,从而引起 堆栈溢出(stack overflow) 异常。异常信息大致如下(摘自 slf4j官网文档 Detected both log4j-over-slf4j.jar AND slf4j-log4j12.jar on the class path, preempting StackOverflowError):

Exception in thread "main" java.lang.StackOverflowError
  at java.util.Hashtable.containsKey(Hashtable.java:306)
  at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:36)
  at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
  at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
  at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
  at org.apache.log4j.Category.<init>(Category.java:53)
  at org.apache.log4j.Logger..<init>(Logger.java:35)
  at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
  at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
  at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
  at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
  at org.apache.log4j.Category..<init>(Category.java:53)
  at org.apache.log4j.Logger..<init>(Logger.java:35)
  at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
  at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
  subsequent lines omitted...

5.1.1 代码示例

实际代码中即便同时使用了log4j-over-slf4j和slf4j-log4j12,也未必一定会出现异常。下面的代码调用slf4j的API输出日志,slf4j底层桥接到log4j:

public class HelloWorld {
	public static void main(String[] args) {
		org.apache.log4j.BasicConfigurator.configure();
		org.slf4j.Logger logger = org.slf4j.LoggerFactory
				.getLogger(HelloWorld.class);
		logger.info("Hello World");
	}
}

1) 配置classpath上的jar包为(注意log4j在log4j-over-slf4j之前):

image-1649089796017

在这种情况下运行测试程序是能够正常输出日志的,不会出现stack overflow异常。

2) 调整classpath上的jar顺序为:

image-1649089811146

再运行测试程序就出现stack overflow异常了,可以看到明显的周期性重复:

image-1649089823089

5.1.2 序列图分析

image-1649089836928

上图是堆栈溢出的详细调用过程序列图。从调用1开始,依次调用1.1、1.1.1……最后到了1.1.1.1.1.1(图中最后一个调用)的时候,发现它跟1是完全一样的,那么后续的过程就是完全一样的重复了。

  • 需要特别说明的是最开始的导火索并不只有图中所示的LoggerFactory.getLogger()一种,应用程序中能够触发堆栈溢出异常的直接调用还有好几种其它的,比如org.apache.log4j.BasicConfigurator.configure(),但后续的互相无限递归调用过程基本都是跟上图相同的过程。

5.1.3 jcl-over-slf4j.jar、slf4j-jcl.jar组合堆栈异常

该组合导致异常的原因也是递归调用导致的堆栈异常,同log4j-over-slf4j、slf4j-log4j12组合异常一样。见 slf4j官方文档

0

评论区