300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > 深入掌握Java日志体系 再也不迷路了

深入掌握Java日志体系 再也不迷路了

时间:2023-07-22 19:27:12

相关推荐

深入掌握Java日志体系 再也不迷路了

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文GitHuborg_hejianhui/JavaStudy 已收录,有我的系列文章。

前言

对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。java领域存在多种日志框架,目前常用的日志框架包括Log4j 1,Log4j 2,Commons Logging,Slf4j,Logback,Jul。但是在我们的系统里面到底该怎么使用日志框架?还在为弄不清commons-logging.jar、log4j.jar、sl4j-api.jar等日志框架之间复杂的关系而感到烦恼吗?还在为如何统一系统的日志输出而感到不知所措嘛?比如,要更改Spring的日志输出为Log4j 2,却不知该引哪些jar包,只知道去百度一下所谓的博客,照着人家复制,却无法弄懂其中的原理?本文将弄懂其中的原理,只要你静下心看本文,你就能随心所欲更改你系统里的日志框架,统一日志输出!

日志框架类别

记录型日志框架

Jul(Java Util Logging):JDK中的日志记录工具,也常称为JDKLog、jdk-logging,自Java1.4以来的官方日志实现。Log4j:Apache Log4j是一个基于Java的日志记录工具。它是由Ceki Gülcü首创的,现在则是Apache软件基金会的一个项目。 Log4j是几种Java日志框架之一。Log4j2:一个具体的日志实现框架,是Log4j 1的下一个版本,与Log4j 1发生了很大的变化,Log4j 2不兼容Log4j 1Logback:一个具体的日志实现框架,和Slf4j是同一个作者,但其性能更好(推荐使用)。

门面型日志框架

JCL:Apache基金会所属的项目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名为Commons LoggingSLF4J:是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)

看到这么多日志框架是否会觉得比较混乱,这些日志框架之间有什么异同,都是由谁在维护,在项目中应该如何选择日志框架,应该如何使用? 不要急,我们先把这些术语概念先有个印象。让我们先了解一它们的发展历史。

日志框架发展史

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。后来(),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大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。底有人分析了GitHub上30000个项目,统计出了最流行的100个Libraries,可以看出Slf4j的发展趋势更好。Apache眼看有被Logback反超的势头,于-07重写了Log4j 1.x,成立了新的项目Log4j 2, Log4j 2具有Logback的所有特性。

大神Ceki

log4j

早年,你工作的时候,在日志里使用了log4j框架来输出,于是你代码是这么写的

import org.apache.log4j.Logger;//省略...Logger logger = Logger.getLogger(Test.class);logger.trace("trace");//省略...

jul

但是,岁月流逝,sun公司对于log4j的出现内心隐隐表示嫉妒。于是在jdk1.4版本后,增加了一个包为java.util.logging,简称为jul,用以对抗log4j。于是,你的领导要你把日志框架改为jul,这时候你只能一行行的将log4j的api改为jul的api,如下所示:

import java.util.logging.Logger;//省略...Logger loggger = Logger.getLogger(Test.class.getName()); logger.finest("finest");//省略...

可以看出,api完全是不同的。那有没有办法,将这些api抽象出接口,这样以后调用的时候,就调用这些接口就好了呢?

jcl

这个时候jcl(Jakarta Commons Logging)出现了,说jcl可能大家有点陌生,讲commons-logging-xx.jar组件,大家总有印象吧。JCL 只提供 log 接口,具体的实现则在运行时动态寻找。这样一来组件开发者只需要针对 JCL 接口开发,而调用组件的应用程序则可以在运行时搭配自己喜好的日志实践工具。JCL可以实现的集成方案如下图所示

jcl默认的配置:如果能找到Log4j 则默认使用log4j 实现,如果没有则使用jul(jdk自带的) 实现,再没有则使用jcl内部提供的SimpleLog 实现。

于是,你在代码里变成这么写了

import mons.logging.Log;import mons.logging.LogFactory;//省略...Log log =LogFactory.getLog(Test.class);log.trace('trace');//省略...

至于这个Log具体的实现类,JCL会在ClassLoader中进行查找。这么做,有三个缺点:

缺点一是效率较低二是容易引发混乱三是在使用了自定义ClassLoader的程序中,使用JCL会引发内存泄露。

slf4j

于是log4j的作者(Ceki)觉得jcl不好用,自己又写了一个新的接口api,那么就是slf4j。

我们在代码中需要写日志,变成下面这么写

import org.slf4j.Logger;import org.slf4j.LoggerFactory;//省略...Logger logger = LoggerFactory.getLogger(Test.class);//省略...logger.info("info");

在代码中,并不会出现具体日志框架的api。程序根据classpath中的桥接器类型,和日志框架类型,判断出logger.info应该以什么框架输出!注意了,如果classpath中不小心引了两个桥接器,那会直接报错的!

因此,在阿里的开发手册上才有这么一条

强制:应用中不可直接使用日志系统(log4j、logback)中的 API ,而应依赖使用日志框架 SLF4J 中的 API 。使用门面模式的日志框架,有利于维护和各个类的日志处理方式的统一。

Slf4j的使用

Slf4j与其它日志组件的关系说明

Slf4j的设计思想比较简洁,使用了Facade设计模式,Slf4j本身只提供了一个slf4j-api-version.jar包,这个jar中主要是日志的抽象接口,jar中本身并没有对抽象出来的接口做实现。对于不同的日志实现方案(例如Logback,Log4j…),封装出不同的桥接组件(例如logback-classic-version.jar,slf4j-log4j12-version.jar),这样使用过程中可以灵活的选取自己项目里的日志实现。

Slf4j与其它日志组件集成图

如图所示,应用调了sl4j-api,即日志门面接口。日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的桥接器,上图红框中的组件即是对应的各种桥接器!

Slf4j与其他各种日志组件的桥接说明

具体的介入方式参考下图

Slf4j源码分析

slf4j-api-version.jar中几个核心类与接口

Slf4j调用过程源码分析,只加入slf4j-api-version.jar,不加入任何实现包

pom配置

<!--只有slf4j-api依赖--><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.30</version></dependency>

程序入口类

package com.niuh;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class App {final static Logger logger = LoggerFactory.getLogger(App.class);public static void main(String[] args) {logger.info("Hello World");}}

源码追踪分析

调用LoggerFactory的getLogger()方法创建Logger 调用LoggerFactory的getILoggerFactory方法来创建ILoggerFactory 调用LoggerFactory的performInitialization方法来进行初始化 调用LoggerFactory的bind()方法 调用LoggerFactory的findPossibleStaticLoggerBinderPathSet()方法获取StaticLoggerBinderPath集合 调用LoggerFactory的reportMultipleBindingAmbiguity()方法,记录绑定的StaticLoggerBinder信息 LoggerFactory的reportMultipleBindingAmbiguity()方法 LoggerFactory的bind()方法找不到StaticLoggerBinder,抛出NoClassDefFoundError异常 LoggerFactory的bind()方法捕获NoClassDefFoundError异常,匹配到StaticLoggerBinder关键词记录信息到控制台

LoggerFactory的performInitialization()方法内部调用bind()方法结束 LoggerFactory的getLogger()方法内部getILoggerFactory()方法调用完成,创建出NOPLoggerFactory,然后由NOPLoggerFactory调用内部的getLogger()方法,创建出NOPLogger

App类内部的logger实际为NOPLogger,调用logger.info()方法实际调用的是NOPLogger的info方法

Slf4j调用过程源码分析,加入slf4j-api-version.jar,与Logback组件

Slf4j作为门面采用Logback作为实现或者采用其它上面提到过的组件作为实现类似,这里只分析采用Logback组件作为实现

pom配置

<dependencies><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.30</version></dependency><!--logback-classic依赖logback-core,会自动级联引入--><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.3</version></dependency></dependencies>

程序入口类

同上

源码追踪分析

1、2、3、4同上 调用LoggerFactory的findPossibleStaticLoggerBinderPathSet()方法获取StaticLoggerBinderPath集合 调用LoggerFactory的bind()方法的staticLoggerBinderPathSet集合对象赋值 在LoggerFactory的bind()方法中调用loback包下的StaticLoggerBinder创建单例对象 在LoggerFactory的bind()方法中调用reportActualBinding()记录日志加载信息 LoggerFactory中INITIALIZATION_STATE的值为SUCCESSFUL_INITIALIZATION,调用StaticLoggerBinder的单例对象获取ILoggerFactory

此时LoggerFactory中的getLogger()方法中获取到的ILoggerFactory实际上是logback jar下的LoggerContext 此时LoggerFactory调用getLogger()方法获取到的Logger实际上是logback jar下的Logger

最后输出

Slf4j调用过程源码分析,加入slf4j-api-version.jar,同时加入多种日志实现组件

在项目中如果用slf4j-api作为日志门面,有多个日志实现组件同时存在,例如同时存在Logback,slf4j-log4j12,slf4j-jdk14,slf4j-jcl四种实现,则在项目实际运行中,Slf4j的绑定选择绑定方式将有Jvm确定,并且是随机的,这样会和预期不符,实际使用过程中需要避免这种情况。

pom配置

<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.30</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.3</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-jdk14</artifactId><version>1.7.30</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-jcl</artifactId><version>1.7.30</version></dependency></dependencies>

程序入口类

同上

源码追踪分析

基本步骤同上,这里只追踪主要不同点(1) 追踪LoggerFactory的bind()方法内部调用findPossibleStaticLoggerBinderPathSet()方法后,从classpath下4个jar包内找到StaticLoggerBinder (2) 此时LoggerFactory的bind()方法内部调用reportMultipleBindingAmbiguity()方法,给出警告信息classpath下同时存在多个StaticLoggerBinder,JVM会随机选择一个StaticLoggerBinder

使用Slf4j时如何桥接遗留的api

在实际环境中我们经常会遇到不同的组件使用的日志框架不同的情况,例如Spring Framework使用的是日志组件是Commons Logging,XSocket依赖的则是Java Util Logging。当我们在同一项目中使用不同的组件时应该如果解决不同组件依赖的日志组件不一致的情况呢?现在我们需要统一日志方案,统一使用Slf4j,把他们的日志输出重定向到Slf4j,然后Slf4j又会根据绑定器把日志交给具体的日志实现工具。Slf4j带有几个桥接模块,可以重定向Log4j,JCL和java.util.logging中的Api到Slf4j。

遗留的api桥接方案

桥接方式参见下图

使用Slf4j桥接注意事项

在使用Slf4j桥接时要注意避免形成死循环,在项目依赖的jar包中不要存在以下情况。

JCL与Slf4j实现机制对比

前面介绍过门面型的日志框架主要就两个JCL(Commons Logging)和Slf4j,我们来简单了解下它们的区别:

JCL(Commons Logging)实现机制

JCL(Commons Logging) 是通过动态查找机制,在程序运行时,使用自己的ClassLoader寻找和载入本地具体的实现。详细策略可以查看commons-logging-*.jar包中的mons.logging.impl.LogFactoryImpl.java文件。由于Osgi不同的插件使用独立的ClassLoader,Osgi的这种机制保证了插件互相独立, 其机制限制了Commons Logging在Osgi中的正常使用。

Slf4j实现机制

Slf4j在编译期间,静态绑定本地的Log库,因此可以在Osgi中正常使用。它是通过查找类路径下org.slf4j.impl.StaticLoggerBinder,然后在StaticLoggerBinder中进行绑定。

日志实战

案例一

一个项目,一个模块用log4j,另一个模块用slf4j+log4j2,如何统一输出?

其实在某些中小型公司,这种情况很常见。我曾经见过某公司的项目,因为研发不懂底层的日志原理,日志文件里头既有log4j.properties,又有log4j2.xml,各种API混用,惨不忍睹!

还有人用着jul的API,然后拿着log4j.properties,跑来问我,为什么配置不生效!简直是一言难尽!

OK,回到我们的问题,如何统一输出!OK,这里就要用上slf4j的适配器,slf4j提供了各种各样的适配器,用来将某种日志框架委托给slf4j。其最明显的集成工作方式有如下:

进行选择填空,将我们的案例里的条件填入,根据题意应该选log4j-over-slf4j适配器,于是就变成下面这张图

就可以实现日志统一为log4j2来输出!

PS:根据适配器工作原理的不同,被适配的日志框架并不是一定要删除!以上图为例,log4j这个日志框架删不删都可以,你只要能保证log4j的加载顺序在log4j-over-slf4j后即可。因为log4j-over-slf4j这个适配器的工作原理是,内部提供了和log4j一模一样的api接口,因此你在程序中调用log4j的api的时候,你必须想办法让其走适配器的api。如果你删了log4j这个框架,那你程序里肯定是走log4j-over-slf4j这个组件里的api。如果不删log4j,只要保证其在classpth里的顺序比log4j前即可!

案例二

如何让Spring以log4j2的形式输出?

Spring默认使用的是jcl输出日志,由于你此时并没有引入Log4j的日志框架,jcl会以jul做为日志框架。此时集成图如下

而你的应用中,采用了slf4j+log4j-core,即log4j2进行日志记录,那么此时集成图如下

那我们现在需要让Spring以log4j2的形式输出?怎么办?

OK,第一种方案,走jcl-over-slf4j适配器,此时集成图就变成下面这样了

在这种方案下,spring框架中遇到日志输出的语句,就会如上图红线流程一样,最终以log4J2的形式输出!

OK,有第二种方案么?

有,走jul-to-slf4j适配器,此时集成图如下

PS:这种情况下,记得在代码中执行

SLF4JBridgeHandler.removeHandlersForRootLogger();SLF4JBridgeHandler.install();

这样jul-to-slf4j适配器才能正常工作,详情可以查询该适配器工作原理。

案例三

假设,我们在应用中调用了sl4j-api,但是呢,你引了四个jar包,slf4j-api-xx.jar,slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar,于是你就会出现如下尴尬的场面

如上图所示,在这种情况下,你调用了slf4j-api,就会陷入死循环中!slf4j-api去调了slf4j-log4j12,slf4j-log4j12又去调用了log4j,log4j去调用了log4j-over-slf4j。最终,log4j-over-slf4j又调了slf4j-api,陷入死循环!

spring4和spring5日志中的不同

Spring4日志体系

构建spring4项目,采用java+注解的方式快速构建,pom中只引入spring-context包

<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>4.3.21.RELEASE</version></dependency></dependencies>

运行下面的代码,可以看到有日志输出

public class MainClass {public static void main(String[] args) {AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);}}

找到打印日志的地方,debug模式下,查看输出日志的Log是什么log

可以看出是jdk14Logger,这个在JCL中说过,这个指的是JUL,也就是说在默认spring日志体系下,采用的是JUL,

接下来,我们按照之前的方法引入log4j,debug运行上面的程序,再次查看日志类型

<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency>

额,这次在增加log4j jar包和配置文件的情况下,spring4有使用了log4j,这么像JCL呢,木错,让我们在idea中打开spring4的日志依赖结构:

common-logging 这不就是JCL使用到的包吗,可以看出,Spring4使用的是原生的JCL,所以在有log4j的时候使用log4j打印日志,没有的时候使用JUL打印日志。

Spring5日志体系

依赖结构图:

大体结构没变,只是原来common-logging ,换成了spring-jcl,看名字就知道是spring自造的包,jcl,更是标注了,它使用的是JCL日志体系。

我们还是通过看源码来验证,我们只用debug找到spring内部一个Log,看看他的产生方式和类型。这次我给大家找了AbstractApplicationContext里面找到产生Log的地方

进入这个方法的getLog()中,一直深入,找到LogAdapter中的createLog()方法

可以看出来Spring5中对日志的生产,不在像原生JCL中那样使用一个数组,然后进行循环产生,这里用到的是Switch case,这个关键字段logApi又是在哪一部分赋值的呢?如下所示:

我们看到是在静态代码块中赋的值,为了验证,我们准备用其中提到的log4j2验证(注意:log4j不行,因为这里的switch没有log4j选项),首先我们准备log4j2.xml的配置文件

<Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/></Console></Appenders><Loggers><Root level="debug"><AppenderRef ref="Console"/></Root></Loggers></Configuration>

然后准备pom的依赖

<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.8.RELEASE</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.10.0</version></dependency></dependencies>

运行下面的代码

public class MainClass {public static void main(String[] args) {AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);}}

结果有日志打印出来了

所以,在Spring5中,依然使用的是JCL,但是不是原生的,是经过改造的JCL,默认使用的是JUL,而原生JCL中默认使用的是log4j。

项目中选择日志框架选择

如果是在一个新的项目中建议使用Slf4j与Logback组合,这样有如下的几个优点。

Slf4j实现机制决定Slf4j限制较少,使用范围更广。由于Slf4j在编译期间,静态绑定本地的LOG库使得通用性要比Commons Logging要好。

Logback拥有更好的性能。Logback声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高。这个操作在Logback中需要3纳秒,而在Log4J中则需要30纳秒。LogBack创建记录器(logger)的速度也更快:13毫秒,而在Log4J中需要23毫秒。更重要的是,它获取已存在的记录器只需94纳秒,而Log4J需要2234纳秒,时间减少到了1/23。跟JUL相比的性能提高也是显著的。

Commons Logging开销更高

# 在使Commons Logging时为了减少构建日志信息的开销,通常的做法是if(log.isDebugEnabled()){log.debug("User name: " +user.getName() + " buy goods id :" + good.getId());}# 在Slf4j阵营,你只需这么做:log.debug("User name:{} ,buy goods id :{}", user.getName(),good.getId());# 也就是说,Slf4j把构建日志的开销放在了它确认需要显示这条日志之后,减少内存和Cup的开销,使用占位符号,代码也更为简洁

Logback文档免费。Logback的所有文档是全面免费提供的,不象Log4J那样只提供部分免费文档而需要用户去购买付费文档。

参考资料

Slf4j官网Slf4j使用手册1Slf4j使用手册2Logback官网Commons Logging官网/chenhongliang/p/5312517.html#java日志概述/lixinkuan328/article/details/104113227

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。