Java中日志组件详解

lanhy
发布于 2020-9-1 11:35
浏览
0收藏

作为开发人员,我相信您对日志记录工具并不陌生。 Java还具有功能强大且功能强大的日志记录库。 但是拥有如此众多的日志记录工具和第三方程序包,如何确保每个组件都可以使用约定日志工具?

 

本文将和大家介绍一下 Java 主流的日志工具,以及相对应的使用场景。

 

基本介绍
在 java 的世界里有许多实现日志功能的工具,最早得到广泛使用的是 log4j,现在比较流行的是 slf4j+logback。作为开发人员,我们有时候需要封装一些组件(二方包)提供给其他人员使用,但是那么多的日志工具,根本没法保证每个组件里都能使用约定好的日志工具,况且还有很多第三方的包,鬼知道他会用什么日志工具。假如一个应用程序用到了两个组件,恰好两个组件使用不同的日志工具,那么应用程序就会有两份日志输出了,蛋疼吧。。

下面简单介绍下常见的日志工具:

JUL
JUL 全称 java.util.logging.Logger,JDK 自带的日志系统,从 JDK1.4 就有了。因为 log4j 的存在,这个 logger 一直沉默着,其实在一些测试性的代码中,jdk 自带的 logger 比 log4j 更方便。JUL 是自带具体实现的,与 log4j、logback 等类似,而不是像 JCL、slf4j 那样的日志接口封装。

 

import java.util.logging.Level;
import java.util.logging.Logger;

private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName());

相同名字的 Logger 对象全局只有一个;
一般使用圆点分隔的层次命名空间来命名 Logger;Logger 名称可以是任意的字符串,但是它们一般应该基于被记录组件的包名或类名,如 java.net 或 javax.swing;
配置文件默认使用 jre/lib/logging.properties,日志级别默认为 INFO;
可以通过系统属性 java.util.logging.config.file 指定路径覆盖系统默认文件;
日志级别由高到低依次为:SEVERE(严重)、WARNING(警告)、INFO(信息)、CONFIG(配置)、FINE(详细)、FINER(较详细)、FINEST(非常详细)。另外还有两个全局开关:OFF「关闭日志记录」和 ALL「启用所有消息日志记录」。
《logging.properties》文件中,默认日志级别可以通过.level= ALL 来控制,也可以基于层次命名空间来控制,按照 Logger 名字进行前缀匹配,匹配度最高的优先采用;日志级别只认大写;
JUL 通过 handler 来完成实际的日志输出,可以通过配置文件指定一个或者多个 hanlder,多个 handler 之间使用逗号分隔;handler 上也有一个日志级别,作为该 handler 可以接收的日志最低级别,低于该级别的日志,将不进行实际的输出;handler 上可以绑定日志格式化器,比如 java.util.logging.ConsoleHandler 就是使用的 String.format 来支持的;
配置文件示例:

handlers= java.util.logging.ConsoleHandler

.level= ALL
com.suian.logger.jul.xxx.level = CONFIG
com.suian.logger.jul.xxx.demo2.level = FINE
com.suian.logger.jul.xxx.demo3.level = FINER

java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%1$tF %1$tT [%4$s] %3$s -  %5$s %n

Apache Commons Logging
之前叫 Jakarta Commons Logging,简称 JCL,是 Apache 提供的一个通用日志 API,可以让应用程序不再依赖于具体的日志实现工具。

commons-logging 包中对其它一些日志工具,包括 Log4J、Avalon LogKit、JUL 等,进行了简单的包装,可以让应用程序在运行时,直接将 JCL API 打点的日志适配到对应的日志实现工具中。

common-logging 通过动态查找的机制,在程序运行时自动找出真正使用的日志库。这一点与 slf4j 不同,slf4j 是在编译时静态绑定真正的 Log 实现库。

commons-logging 包里的包装类和简单实现列举如下:

org.apache.commons.logging.impl.Jdk14Logger,适配 JDK1.4 里的 JUL;
org.apache.commons.logging.impl.Log4JLogger,适配 Log4J;
org.apache.commons.logging.impl.LogKitLogger,适配 avalon-Logkit;
org.apache.commons.logging.impl.SimpleLog,common-logging 自带日志实现类,它实现了 Log 接口,把日志消息都输出到系统错误流 System.err 中;
org.apache.commons.logging.impl.NoOpLog,common-logging 自带日志实现类,它实现了 Log 接口,其输出日志的方法中不进行任何操作;
如果只引入 Apache Commons Logging,也没有通过配置文件《commons-logging.properties》进行适配器绑定,也没有通过系统属性或者 SPI 重新定义 LogFactory 实现,默认使用的就是 jdk 自带的 java.util.logging.Logger 来进行日志输出。

JCL 使用配置文件 commons-logging.properties,可以在该文件中指定具体使用哪个日志工具。不配置的话,默认会使用 JUL 来输出日志。配置示例:

Avalon LogKit
Avalon LogKit 是一个高速日志记录工具集,Avalon 里的各个组件 Framework、Excalibur、Cornerstone 和 Phoenix 都用到它。它的模型与 JDK 1.4 Logging package 采用相同的原理,但与 JDK 1.2+ 兼容。使用 LogKit 的原因是:Context 和 LogTargets。

使用 Log4j 的时候,日志的内容只能是一句话,而使用 LogKit,你可以记录很多项内容,甚至可以各项内容记录到对应的数据库字段中。如果使用 Log4j 存储日志到不同的存储介质,如数据库,需要使用 Appender,而 LogKit 已经可以支持多种存储目标。

log4j
Log4j 是 Apache 的一个开放源代码项目,通过使用 Log4j,我们可以控制日志信息输送的目的地是控制台、文件、数据库等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

Log4j 有 7 种不同的 log 级别,按照等级从低到高依次为:TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF。如果配置为 OFF 级别,表示关闭 log。Log4j 支持两种格式的配置文件:properties 和 xml。包含三个主要的组件:Logger、appender、Layout。

SLF4J
SLF4J 全称 The Simple Logging Facade for Java,简单日志门面,这个不是具体的日志解决方案,而是通过门面模式提供一些 Java Logging API,类似于 JCL。题外话,作者当时创建 SLF4J 的目的就是为了替代 Jakarta Commons Logging(JCL)。

SLF4J 提供的核心 API 是一些接口以及一个 LoggerFactory 的工厂类。在使用 SLF4J 的时候,不需要在代码中或配置文件中指定你打算使用哪个具体的日志系统,可以在部署的时候不修改任何配置即可接入一种日志实现方案,在编译时静态绑定真正的 Log 库。

使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。SLF4J 提供了统一的记录日志的接口,只要按照其提供的方法记录即可,最终日志的格式、记录级别、输出方式等通过具体日志系统的配置来实现,因此可以在应用中灵活切换日志系统。

logback 是 slf4j-api 的天然实现,不需要桥接包就可以使用。另外 slf4j 还封装了很多其他的桥接包,可以使用到其他的日志实现中,比如 slf4j-log4j12,就可以使用 log4j 进行底层日志输出,再比如 slf4j-jdk14,可以使用 JUL 进行日志输出。

Logback
Logback,一个“可靠、通用、快速而又灵活的 Java 日志框架”。Logback 当前分成三个模块:logback-core,logback- classic 和 logback-access。logback-core 是其它两个模块的基础模块。logback-classic 是 log4j 的一个改良版本,完整实现了 SLF4J API。

logback-access 模块与 Servlet 容器集成提供通过 Http 来访问日志的功能。Logback 依赖配置文件 logback.xml,当然也支持 groovy 方式。Logback 相比 log4j,有很多很多的优点,网上一搜一大片,此处就不再赘述了。

Log4j2
Log4j 2 是 log4j 1.x 和 logback 的改进版,据说采用了一些新技术(无锁异步等等),使得日志的吞吐量、性能比 log4j 1.x 提高 10 倍,并解决了一些死锁的 bug,而且配置更加简单灵活。

Log4j2 支持插件式结构,可以根据需要自行扩展 Log4j2,实现自己的 appender、logger、filter 等。在配置文件中可以引用属性,还可以直接替代或传递到组件,而且支持 json 格式的配置文件。不像其他的日志框架,它在重新配置的时候不会丢失之前的日志文件。

Log4j2 利用 Java5 中的并发特性支持,尽可能地执行最低层次的加锁。解决了在 log4j 1.x 中存留的死锁的问题。Log4j 2 是基于 LMAX Disruptor 库的。在多线程的场景下,和已有的日志框架相比,异步 logger 拥有 10 倍左右的效率提升。

Log4j2 体系结构:Java中日志组件详解-鸿蒙开发者社区

使用场景
只使用 java.util.logging.Logger
最简单的场景,正式系统一般不会这么用,自己写点小 demo、测试用例啥的是可以这么用。不要任何第三方依赖,jdk 原生支持。

只使用 Apache Commons Logging
需要引入 commons-logging 包,示例如下:

      <dependency>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
          <version>1.2</version>
      </dependency>

Apache Commons Logging 和 log4j 结合使用
需要引入 commons-logging 包和 log4j 包,示例如下:

      <dependency>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
          <version>1.2</version>
      </dependency>
      <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.17</version>
      </dependency>

该模式下可以使用的打点 api:

org.apache.commons.logging.Log,commons-logging 里的 api;
org.apache.log4j.Logger,log4j 里的 api;
无论使用哪种 api 打点,最终日志都会通过 log4j 进行实际的日志记录。推荐用 commons-logging 里的 api,如果直接用 log4j 里的 api,就跟单用 log4j 没区别,就没有再引入 commons-logging 包的必要了。

既然最终是通过 log4j 实现日志记录,那么日志输出的 level、target 等也就是通过 log4j 的配置文件进行控制了。下面是一个 log4j 配置文件《log4j.properties》的简单示例:

log4j.logger.com.suian.logtest = trace,console

#输出源 console 输出到控制台
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c - [log4j]%m%n

既然是推荐使用 commons-logging 里的 api 打点,为了能找到 log4j 的日志实现,必须通过《commons-logging.properties》配置文件显式的确定关联,示例如下:

 
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger

 

代码中使用 JCL api 进行日志打点,底层使用 log4j 进行日志输出。日志输出控制依托于 log4j 的配置文件,另外需要在 commons-logging.properties 配置文件中显式指定与 log4j 的绑定关系。

单独使用 log4j
这个是早几年最最流行的用法了,现在因为 log4j 本身的问题以及新的日志框架的涌现,已经逐步退出历史舞台了。具体怎么用自己去百度吧。

SLF4J 结合 Logback
当下最流行的用法,SLF4J 为使用场景最广泛的日志门面,加上 Logback 的天然实现,简单、统一、快速。

需要引入第三方依赖:

 

      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>${slf4j.version}</version>
      </dependency>
      <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-core</artifactId>
          <version>${logback.version}</version>
      </dependency>
      <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>${logback.version}</version>
      </dependency>

单独使用 Log4j2
Log4j2 感觉就是 SLF4J+Logback。log4j-api 等价于 SLF4J,log4j-core 等价于 Logback。

需要引入第三方依赖:

      <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-api</artifactId>
          <version>2.6.2</version>
      </dependency>
      <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-core</artifactId>
          <version>2.6.2</version>
      </dependency>

冲突处理
理论上各种日志输出方式是可以共存的,比如 log4j 和 log4j2 以及 logback 等,但是麻烦的是我们得维护多个配置文件,必须充分了解每个组件使用的是那种日志组件,然后进行对应的配置文件配置。

如何解决呢?每一个想做通用日志解决方案的,都对兼容性问题进行了特殊处理。目前只有 slf4j 和 log4j2 提供了这样的整合机制,其他的基本都很弱。

代码中可能使用的日志打点 Api 列举:

java.util.logging.Logger,jdk 自带的;
org.apache.commons.logging.Log,commons-logging 包里的 api;
org.apache.log4j.Logger,log4j 包里的 api;
org.apache.logging.log4j.Logger,log4j2 提供的 api,在 log4j-api 包里;
org.slf4j.Logger,slf4j 提供的 api,在 slf4j-api 包里;
上述打点方式,在一个应用中是有可能共存的,即使自己写的代码可以确保都使用同一类 api,但是引入的第三方依赖里就可能各式各样了。该怎么处理呢?

前面已经提过了,现在能够对各类冲突支持比较到位的就是 slf4j 和 log4j2,他们都提供了很多的绑定器和桥接器。

所谓的绑定器,也可以称之为适配器或者包装类,就是将特定 api 打点的日志绑定到具体日志实现组件来输出。比如 JCL 可以绑定到 log4j 输出,也可以绑定到 JUL 输出;再比如 slf4j,可以通过 logback 输出,也可以绑定到 log4j、log4j2、JUL 等;
所谓的桥接器就是一个假的日志实现工具,比如当你把 jcl-over-slf4j.jar 放到 CLASS_PATH 时,即使某个组件原本是通过 JCL 输出日志的,现在却会被 jcl-over-slf4j “骗到”SLF4J 里,然后 SLF4J 又会根据绑定器把日志交给具体的日志实现工具。
slf4j 整合日志输出
java.util.logging.Logger
将 JUL 日志整合到 slf4j 统一输出,需要引入 slf4j 提供的依赖包:

      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jul-to-slf4j</artifactId>
          <version>1.7.22</version>
      </dependency>

只引入依赖并不能整合 JUL 日志,该包里只是提供了一个 JUL 的 handler,仍旧需要通过 JUL 的配置文件进行配置,slf4j 绑定器(如 logback)上设置的日志级别等价于 JUL handler 上的日志级别,因此控制 JUL 的日志输出,日志级别仍旧分两个地方控制:JUL 配置文件《logging.properties》和 slf4j 绑定器的配置文件,比如《logback.xml》、《log4j2.xml》等。

建立 jdk14-logger 的配置文件《logger.properties》,加入 handler 配置以及日志级别配置;

handlers= org.slf4j.bridge.SLF4JBridgeHandler
.level= ALL

在启动程序或容器的时候加入 JVM 参数配置 -Djava.util.logging.config.file = /path/logger.properties;当然也可以使用编程方式进行处理,可以在 main 方法或者扩展容器的 listener 来作为系统初始化完成;此种方式有些场景下不如配置 JVM 参数来的彻底,比如想代理 tomcat 的系统输出日志,编程方式就搞不定了。
org.apache.commons.logging.Log
将 JCL 日志整合到 slf4j 统一输出,需要引入 slf4j 提供的依赖包:

      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
          <version>1.7.22</version>
      </dependency>

jcl-over-slf4j 包里所有类的根路径为 org.apache.commons.logging,也有 Log 和 LogFactory 类,相当于以重写 commons-logging 包的代价来实现对 JCL 的桥接。Log 与 commons-logging 包里的一模一样,LogFactory 的实现,代码写死使用的是 org.apache.commons.logging.impl.SLF4JLogFactory。

commons-logging 包里默认使用的是 org.apache.commons.logging.impl.LogFactoryImpl。以这样的代价来实现桥接,可以实现无缝对接,不像 JUL 那样还得添加额外配置,但是有一个坏处就是需要处理类库冲突了。commons-logging 包和 jcl-over-slf4j 包肯定是不能共存的,需要将 commons-logging 包在 classpath 里排掉。

题外话,因为 JCL 本身就支持通过配置文件《commons-logging.properties》绑定适配器,所以个人感觉更倾向于封装一个适配器的方式来支持,就像 commons-logging 包里的 org.apache.commons.logging.impl.Log4JLogger,这样更符合程序员的思维,明明白白。

桥接包的命名也是很讲究的,覆写的这种,命名为 xxx-over-slf4j,如本例的 jcl-over-slf4j;纯桥接的,命名为 xxx-to-slf4j,如文章前面提到的 jul-to-slf4j。

org.apache.log4j.Logger
将 log4j 日志整合到 slf4j 统一输出,需要引入 slf4j 提供的依赖包:

      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>log4j-over-slf4j</artifactId>
          <version>1.7.22</version>
      </dependency>

看桥接包的名字就知道了,log4j-over-slf4j 肯定是覆写了 log4j:log4j 包,因此使用起来只需要引入依赖即可,不需要其他额外的配置。但是仍旧是要处理冲突的,log4j 包和 log4j-over-slf4j 是不能共存的哦。

org.apache.logging.log4j.Logger
将 log4j2 日志整合到 slf4j 统一输出,slf4j 没有提供桥接包,但是 log4j2 提供了,原理是一样的,首先引入 log4j2 的桥接包:

      <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-to-slf4j</artifactId>
          <version>2.6.2</version>
      </dependency>

log4j2 提供的依赖包有 org.apache.logging.log4j:log4j-api 和 org.apache.logging.log4j:log4j-core,其作用看包名就清楚了。log4j-core 是 log4j-api 的标准实现,同样 log4j-to-slf4j 也是 log4j-api 的一个实现。

log4j-to-slf4j 用于将 log4j2 输出的日志桥接到 slf4j 进行实际的输出,作用上来讲,log4j-core 和 log4j-to-slf4j 是不能共存的,因为会存在两个 log4j2 的实现。

经测试,就测试结果分析,共存也是木有问题的,何解?log4j2 加载 provider 的时候采用了优先级策略,即使找到多个也能决策出一个可用的 provider 来。在所有提供 log4j2 实现的依赖包中,都有一个 META-INF/log4j-provider.properties 配置文件,里面的 FactoryPriority 属性就是用来配置 provider 优先级的,幸运的是 log4j-to-slf4j(15)的优先级是高于 log4j-core(10)的,因此测试结果符合预期,log4j2 的日志桥接到了 slf4j 中进行输出。

同样,为确保系统的确定性,不会因为 log4j2 的 provider 决策策略变更导致问题,建议还是要在 classpath 里排掉 log4j-core,log4j2 也是推荐这么做的。

log4j2 整合日志输出
java.util.logging.Logger
将 JUL 日志整合到 log4j2 统一输出,需要引入 log4j2 提供的依赖包:

      <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-jul</artifactId>
          <version>2.6.2</version>
      </dependency>

log4j2 整合 JUL 日志的方式与 slf4j 不同,slf4j 只是定义了一个 handler,仍旧依赖 JUL 的配置文件;log4j2 则直接继承重写了 java.util.logging.LogManager。

使用时,只需要通过系统属性 java.util.logging.manager 绑定重写后的 LogManager(org.apache.logging.log4j.jul.LogManager)即可,感觉比 slf4j 的方式要简单不少。

org.apache.commons.logging.Log
将 JCL 日志整合到 log4j2 统一输出,需要引入 log4j2 提供的依赖包:

      <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-jcl</artifactId>
          <version>2.6.2</version>
      </dependency>

基于 log4j-jcl 包整合 JCL 比较简单,只要把 log4j-jcl 包扔到 classpath 就可以了。看起来 slf4j 的整合方式优雅多了,底层原理是这样的:JCL 的 LogFactory 在初始化的时候,查找 LogFactory 的具体实现,是分了几种场景的,简单描述如下:

首先根据系统属性 org.apache.commons.logging.LogFactory 查找 LogFactory 实现类;
如果找不到,则以 SPI 方式查找实现类,META-INF/services/org.apache.commons.logging.LogFactory;log4j-jcl 就是以这种方式支撑的;此种方式必须确保整个应用中,包括应用依赖的第三方 jar 包中,org.apache.commons.logging.LogFactory 文件只有一个,如果存在多个的话,哪个先被加载则以哪个为准。万一存在冲突的话,排查起来也挺麻烦的。
还找不到,则读取《commons-logging.properties》配置文件,使用 org.apache.commons.logging.LogFactory 属性指定的 LogFactory 实现类;
最后再找不到,就使用默认的实现 org.apache.commons.logging.impl.LogFactoryImpl。
org.apache.log4j.Logger
将 log4j 1.x 日志整合到 log4j2 统一输出,需要引入 log4j2 提供的依赖包:

      <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-1.2-api</artifactId>
          <version>2.6.2</version>
      </dependency>

log4j2 里整合 log4j 1.x 日志,也是通过覆写 log4j 1.x api 的方式来实现的,跟 slf4j 的实现原理一致。因此也就存在类库冲突的问题,使用 log4j-1.2-api 的话,必须把 classpath 下所有 log4j 1.x 的包清理掉。

org.slf4j.Logger
将 slf4j 日志整合到 log4j2 统一输出,需要引入 log4j2 提供的依赖包:

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

log4j-slf4j-impl 基于 log4j2 实现了 slf4j 的接口,其就是 slf4j-api 和 log4j2-core 之间的一个桥梁。这里有个问题需要注意下,务必确保 classpath 下 log4j-slf4j-impl 和 log4j-to-slf4j 不要共存,否则会导致事件无止尽地在 SLF4J 和 Log4j2 之间路由。

日志打点 API 绑定实现
slf4j-api 和 log4j-api 都是接口,不提供具体实现,理论上基于这两种 api 输出的日志可以绑定到很多的日志实现上。slf4j 和 log4j2 也确实提供了很多的绑定器。简单列举几种可能的绑定链:

slf4j → logback
slf4j → slf4j-log4j12 → log4j
slf4j → log4j-slf4j-impl → log4j2
slf4j → slf4j-jdk14 → jul
slf4j → slf4j-jcl → jcl
jcl → jul
jcl → log4j
log4j2-api → log4j2-cor
log4j2-api → log4j-to-slf4j → slf4j
来个环图:Java中日志组件详解-鸿蒙开发者社区

本文转载自公众号淘系技术(ID:AlibabaMTT)。

 

 

标签
已于2020-9-2 18:14:14修改
收藏
回复
举报
回复
    相关推荐