logback-官网文档部分翻译2-slf4j

SLF4J 手册 --- SLF4J Manual

Simple Logging Facade for Java (SLF4J) 用作各种日志框架(例如 java.util.logging、log4j 1.x、reload4j 和 logback)的简单外观或抽象。 SLF4J 允许最终用户在部署时插入所需的日志记录框架。请注意,启用 SLF4J 的库/应用程序意味着仅添加一个强制依赖项,即 slf4j-api-2.0.13.jar。

如前所述,SLF4J 旨在作为各种日志框架的外观。 SLF4J 发行版附带了多个称为“提供程序”的 jar 文件,每个提供程序都对应于一个受支持的日志记录框架。
请注意,SLF4J 1.7 版及更早版本对提供者使用术语“绑定”。

这是提供者工件的部分(即非详尽)列表。

slf4j-log4j12-2.0.13.jar
log4j 1.2 版的绑定/提供程序,一种广泛使用的日志框架。鉴于 log4j 1.x 已于 2015 年和 2022 年被宣布 EOL,从 SLF4J 1.7.35 开始,slf4j-log4j 模块在构建时自动重定向到 slf4j-reload4j 模块。假设您希望继续使用 log4j 1.x 框架,我们强烈建议您改用 slf4j-reload4j 。见下文。

 

slf4j-reload4j-2.0.13.jar

自 1.7.33 起 reload4j 框架的绑定/提供程序。 Reload4j 是 log4j 版本 1.2.7 的直接替代品。您还需要将 reload4j.jar 放在类路径中。

slf4j-jdk14-2.0.13.jar
java.util.logging 的绑定/提供程序,也称为 JDK 1.4 日志记录

 

slf4j-nop-2.0.13.jar
NOP 的绑定/提供者,默默地丢弃所有日志记录。

 

slf4j-simple-2.0.13.jar
用于简单实现的绑定/提供程序,将所有事件输出到 System.err。仅打印 INFO 及更高级别的消息。此绑定在小型应用程序的上下文中可能很有用。

 

slf4j-jcl-2.0.13.jar
Apache Commons 日志记录的绑定/提供者。此绑定会将所有 SLF4J 日志记录委托给 Apache Commons Logging 又名 Jakarta Commons Logging (JCL) 。

 

logback-classic-1.4.6.jar 用于 Jakarta EE,需要 logback-core-1.4.6.jar
or
logback-classic-1.3.6.jar 用于 Javax EE,需要 logback-core-1.3.6.jar
原生实现 SLF4J 项目外部还有 SLF4J 绑定/提供程序,例如logback 本身实现了 SLF4J。 Logback 的 ch.qos.logback.classic.Logger 类是 SLF4J 的 org.slf4j.Logger 接口的直接实现。因此,将 SLF4J 与 logback 结合使用涉及严格的零内存和计算开销。

 

要切换日志框架,只需替换类路径上的 slf4j 绑定即可。例如,要从 java.util.logging 切换到 reload4j,只需将 slf4j-jdk14-2.0.13.jar 替换为 slf4j-reload4j-2.0.13.jar 即可。

SINCE 2.0.0 从版本 2.0.0 开始,SLF4J 绑定称为提供者。尽管如此,总体思路仍然是一样的。 SLF4J API 版本 2.0.0 依赖 ServiceLoader 机制来查找其日志记录后端。有关更多详细信息,请参阅相关的常见问题解答条目。

这是总体思路的图形说明。

click to enlarge

SLF4J 接口及其各种适配器非常简单。大多数熟悉 Java 语言的开发人员应该能够在一小时内阅读并完全理解代码。
不需要了解类加载器,因为 SLF4J 不使用也不直接访问任何类加载器。
因此,SLF4J 不会出现 Jakarta Commons Logging (JCL)(也称为 Apache Commons Logging)中观察到的类加载器问题或内存泄漏问题。

鉴于 SLF4J 接口及其部署模型的简单性,新日志框架的开发人员应该会发现编写 SLF4J 提供程序非常容易。

从 2.0.9 开始,您可以通过“slf4j.provider”系统属性显式指定提供程序类。这绕过了用于查找提供者的服务加载器机制,并可能缩短 SLF4J 初始化时间。

广泛分布的组件和库的作者可能会针对 SLF4J 接口进行编码,以避免将日志框架强加给最终用户。
因此,最终用户可以在部署时通过在类路径上插入相应的 slf4j 绑定来选择所需的日志记录框架,稍后可以通过将现有绑定替换为类路径上的另一个绑定并重新启动应用程序来更改该绑定。
事实证明,这种方法简单且非常稳健。

从 SLF4J 版本 1.6.0 开始,如果在类路径上找不到绑定,则 slf4j-api 将默认为无操作实现,丢弃所有日志请求。因此,SLF4J 版本 1.6.0 及更高版本不会因为缺少 org.slf4j.impl.StaticLoggerBinder 类而抛出 NoClassDefFoundError ,而是会发出一条有关缺少绑定的警告消息,并继续丢弃所有记录请求而不进一步抗议。例如,让 Wombat 成为一些依赖于 SLF4J 进行日志记录的生物学相关框架。
为了避免将日志框架强加给最终用户,Wombat 的发行版包含 slf4j-api.jar 但没有绑定。即使类路径上没有任何 SLF4J 绑定,Wombat 的发行版仍然可以开箱即用,并且不需要最终用户从 SLF4J 的网站下载绑定。
仅当最终用户决定启用日志记录时,她才需要安装与她选择的日志记录框架相对应的 SLF4J 绑定。

基本规则 嵌入式组件(例如库或框架)不应声明对任何 SLF4J 绑定/提供程序的依赖,而仅依赖于 slf4j-api。当库声明对特定绑定的传递依赖时,该绑定就会强加给最终用户,从而否定 SLF4J 的目的。
请注意,声明对绑定的非传递依赖(例如用于测试)不会影响最终用户。

常见问题解答中还讨论了 SLF4J 在嵌入式组件中的使用,涉及日志记录配置、依赖性减少和测试。

考虑到 Maven 的传递依赖规则,对于“常规”项目(不是库或框架),可以通过单个依赖项声明来完成声明日志记录依赖项。

SLF4J API SLF4J API 包含在“org.slf4j:slf4j-api”工件中。您可以在 pom.xml 文件中显式声明对其的依赖关系,如下所示。请注意,大多数日志记录实现都会自动引入 slf4j-api 作为依赖项。
然而,声明对 slf4j-api 的显式依赖关系通常是一个好主意,以便借助 Maven 的“最近定义”依赖关系中介规则修复项目中 slf4j-api 的正确版本。

<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>2.0.13</version>
</dependency>

LOGBACK-CLASSIC 1.3.X (JAVAX EE) 如果您希望使用 Javax EE 的 logback-classic 作为底层日志记录框架,您需要做的就是将“ch.qos.logback:logback-classic”声明为依赖项你的 pom.xml 文件如下所示。除了 logback-classic-1.3.6.jar 之外,这还会将 slf4j-api-2.0.13.jar 以及 logback-core-1.3.6.jar 引入您的项目中。请注意,显式声明对 logback-core-1.3.6 或 slf4j-api-2.0.13.jar 的依赖关系并没有错,并且可能需要借助 Maven 的“最近定义”依赖关系中介规则来强制使用所述工件的正确版本。

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

 

LOGBACK-CLASSIC 1.4.X (JAKARTA EE) 如果您希望使用 Jakarta EE 的 logback-classic 作为底层日志框架,您需要做的就是将“ch.qos.logback:logback-classic”声明为依赖项你的 pom.xml 文件如下所示。除了 logback-classic-1.4.6.jar 之外,这还会将 slf4j-api-2.0.13.jar 以及 logback-core-1.4.6.jar 引入您的项目中。请注意,显式声明对 logback-core-1.4.6 或 slf4j-api-2.0.13.jar 的依赖关系并没有错,并且可能需要借助 Maven 的“最近定义”依赖关系中介规则来强制使用所述工件的正确版本。

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

 

RELOAD4J 如果您希望使用 reload4j 作为底层日志框架,您所需要做的就是在 pom.xml 文件中声明“org.slf4j:slf4j-reload4j”作为依赖项,如下所示。除了 slf4j-reload4j-2.0.13.jar 之外,这还会将 slf4j-api-2.0.13.jar 以及 reload4j-1.2.25.jar 引入您的项目中。请注意,显式声明对 reload4j-1.2.25.jar 或 slf4j-api-2.0.13.jar 的依赖关系并没有错,并且可能需要凭借 Maven 的“最近定义”依赖关系调解规则来强加所述工件的正确版本。

<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-reload4j</artifactId>
  <version>2.0.13</version>
</dependency>

LOG4J 1.2.X 从 SLF4J 版本 1.7.36 开始,声明对 org.slf4j:slf4j-log4j12 的依赖关系通过 Maven 指令重定向到 org.slf4j:slf4j-reload4j 。

<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>2.0.13</version>
</dependency>

 

JAVA.UTIL.LOGGING 如果您希望使用 java.util.logging 作为底层日志框架,您所需要做的就是在 pom.xml 文件中声明“org.slf4j:slf4j-jdk14”作为依赖项,如下所示。除了 slf4j-jdk14-2.0.13.jar 之外,这还会将 slf4j-api-2.0.13.jar 引入您的项目中。请注意,显式声明对 slf4j-api-2.0.13.jar 的依赖关系并没有错,并且可能需要借助 Maven 的“最近定义”依赖关系中介规则来强制使用所述工件的正确版本。

<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-jdk14</artifactId>
  <version>2.0.13</version>
</dependency>

SLF4J SIMPLE 如果您希望使用 org.slf4j.simple 作为底层日志记录实现,您所需要做的就是在 pom.xml 文件中声明“org.slf4j:slf4j-simple”作为依赖项,如下所示。除了 slf4j-simple-2.0.13.jar 之外,这还会将 slf4j-api-2.0.13.jar 引入您的项目中。请注意,显式声明对 slf4j-api-2.0.13.jar 的依赖关系并没有错,并且可能需要借助 Maven 的“最近定义”依赖关系中介规则来强制使用所述工件的正确版本。

<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-simple</artifactId>
  <version>2.0.13</version>
</dependency>

SLF4J 提供程序/绑定指定一个工件,例如 slf4j-jdk14.jar 或 slf4j-reload4j.jar,用于将 slf4j 绑定到底层日志记录框架,例如 java.util.logging 和 reload4j。

从客户端的角度来看,slf4j-api,更具体地说是 org.slf4j 包中的类,向后兼容所有版本。使用 slf4j-api-N.jar 编译的客户端代码对于任何 N 和 M 都可以与 slf4j-api-M.jar 完美运行。
您只需确保您的提供程序/绑定的版本与 slf4j-api.jar 的版本匹配。您不必担心项目中给定依赖项使用的 slf4j-api.jar 版本。

混合不同版本的 slf4j-api.jar 和 SLF4J 提供程序/绑定可能会导致问题。例如,如果您使用 slf4j-api-2.0.13.jar,那么您还应该使用 slf4j-simple-2.0.13.jar,使用 slf4j-simple-1.5.5.jar 将不起作用。

然而,从客户端的角度来看,SLF4J API(更具体地说是 org.slf4j 包中的类)向后兼容所有版本。对于任何 N 和 M,使用 slf4j-api-N.jar 编译的客户端代码都可以与 slf4j-api-M.jar 完美运行。您只需确保提供程序/绑定的版本与 slf4j-api 的版本匹配。罐。您不必担心项目中给定依赖项使用的 slf4j-api.jar 版本。您始终可以使用任何版本的 slf4j-api.jar,只要 slf4j-api.jar 的版本及其提供者/绑定匹配,就应该没问题。

很多时候,给定的项目将依赖于依赖于 SLF4J 之外的日志记录 API 的各种组件。经常会发现依赖于 JCL、java.util.logging、log4j 和 SLF4J 组合的项目。
然后,需要通过单个通道整合日志记录。 SLF4J 通过为 JCL、java.util.logging 和 log4j 提供桥接模块来满足这种常见的用例。有关更多详细信息,请参阅桥接旧版 API 页面。

SINCE 2.0.0-ALPHA5 slf4j-jdk-platform-logging 模块添加了对 JDK 平台日志记录的支持。

<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-jdk-platform-logging</artifactId>
  <version>2.0.13</version>
</dependency>

“映射的诊断上下文”本质上是由日志框架维护的映射,其中应用程序代码提供键值对,然后日志框架可以将其插入到日志消息中。
MDC 数据对于过滤消息或触发某些操作也非常有帮助。

SLF4J 支持 MDC 或映射诊断上下文。如果底层日志框架提供 MDC 功能,那么 SL​​F4J 将委托给底层框架的 MDC。请注意,目前只有 log4j 和 logback 提供 MDC 功能。
如果底层框架不提供 MDC,例如 java.util.logging,那么 SL​​F4J 仍将存储 MDC 数据,但其中的信息需要通过自定义用户代码检索。

因此,作为 SLF4J 用户,您可以在 log4j 或 logback 存在的情况下利用 MDC 信息,但无需将这些日志记录框架作为依赖项强加给您的用户。

有关 MDC 的更多信息请参阅 logback 手册中有关 MDC 的章节。

 优势  描述
日志 API 和配置分离 鉴于 SLF4J 提供了一个狭窄的 API,仅限于编写日志语句,但没有日志配置,SLF4J 强制执行关注点分离。
日志记录语句是使用 SLF4j API 编写的,并通过底层日志记录后端进行配置,通常在单个位置。
在部署时选择您的日志框架 通过在类路径上插入适当的 jar 文件(提供程序/绑定),可以在部署时插入所需的日志记录框架。
 快速失败操作 在 SLF4J 初始化期间,将很早就搜索提供者。如果 SLF4J 在类路径上找不到提供程序,它将发出一条警告消息并默认为无操作实现。
流行日志框架的提供者 SLF4J 支持流行的日志框架,即 reload4j、log4j 1.x、log4j 2.x、java.util.logging、Simplelogging 和 NOP。 logback、logevents、penna 项目原生支持 SLF4J。
桥接旧版日志记录 API

在 SLF4J 上实现 JCL(即 jcl-over-slf4j.jar)将允许您的项目逐步迁移到 SLF4J,而不会破坏与使用 JCL 的现有软件的兼容性。
同样,log4j-over-slf4j.jar 和 jul-to-slf4j 模块将允许您将 log4j 和 java.util.logging 调用分别重定向到 SLF4J。有关更多详细信息,请参阅桥接旧版 API 页面。

迁移您的源代码 slf4j-migrator 实用程序可以帮助您迁移源以使用 SLF4J。
支持参数化日志消息 所有 SLF4J 提供程序/绑定都支持参数化日志消息,并显着提高了性能结果。

SLF4J 迁移器 --- SLF4J Migrator

Log4j 桥 --- Log4j Bridge

 

Java日志框架那些事儿(上)中我们介绍了 Java 常用的几种技术框架,以及它们与统一日志门面 SLF4J 的整合。经过上面的介绍,相信大家对 Java 常用的日志框架都有了一定认识。那么在实际使用中到底选择哪种日志框架合适呢?今天我就介绍一种笔者认为比较合适的日志框架解决方案,并且给出不同日志框架转换的指南。

最流行的日志框架解决方案

按笔者理解,现在最流的日志框架解决方案莫过于SLF4J + LogBack。其有以下几个优点:

  • LogBack 自身实现了 SLF4J 的日志接口,不需要 SLF4J 去做进一步的适配。
  • LogBack 自身是在 Log4J 的基础上优化而成的,其运行速度和效率都比 LOG4J 高。
  • SLF4J + LogBack 支持占位符,方便日志代码的阅读,而 LOG4J 则不支持。

从上面几点来看,SLF4J + LogBack是一个较好的选择。

LogBack 被分为3个组件:logback-core、logback-classic 和 logback-access。

  • logback-core 提供了 LogBack 的核心功能,是另外两个组件的基础。
  • logback-classic 则实现了 SLF4J 的API,所以当想配合 SLF4J 使用时,需要将 logback-classic 引入依赖中。
  • logback-access 是为了集成Servlet环境而准备的,可提供HTTP-access的日志接口。

了解 LogBack 日志的日志流向,对于我们后面学习日志框架的配置很有帮助。从下图可以看出 LogBack 的日志记录数据流是从 Class 或 Package 流到 Logger,再从Logger到Appender,最后从Appender到具体的输出终端。

LogBack日志流向描述图

LogBack配置文件可以分为几个节点,其中 Configuration 是根节点,Appender、Logger、Root是Configuration的子节点。

appender节点

<appender>是<configuration>的子节点,是负责写日志的组件。appender有两个必要属性name、class 。name指定appender的名称,class指定appender的全限定名
class,主要包括:

  • ch.qos.logback.core.ConsoleAppender 控制台输出
  • ch.qos.logback.core.FileAppender 文件输出
  • ch.qos.logback.core.RollingFileAppender 文件滚动输出
<?xml version="1.0" encoding="utf-8"?> 
<configuration debug="true" scan="true" scanPeriod="2">
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
    </appender>

    <!-- conf file out -->
    <appender name="file_out" class="ch.qos.logback.core.FileAppender">
    </appender>

    <!-- conf file out -->
    <appender name="file_out" class="ch.qos.logback.core.RollingFileAppender">
    </appender>

    <root></root>
    <logger></logger>
</configuration>

ConsoleAppender

把日志添加到控制台,有如下节点:

  • <encoder> : 对日志进行格式化。
  • <target> : 字符串System.out 或者 System.err, 默认 System.out;
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- conf consoel out -->
  <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
        </encoder>
  </appender>

  <root level="INFO">             
    <appender-ref ref="console_out" />   
  </root>     
</configuration>

FileAppender

把日志添加到文件,有如下节点:

  • <file>:被写入的文件名,可以是相对目录 , 也可以是绝对目录 , 如果目录不存在则会自动创建。
  • <append>:如果是true , 日志被追加到文件结尾 , 如果是false,清空现存文件 , 默认是true。
  • <encoder>:对日志进行格式化 [具体的转换符说明请参见官网.]
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appender name="file_out" class="ch.qos.logback.core.FileAppender">
        <file>logs/debug.log</file>
        <encoder>
            <pattern>%date [%thread] %-5level %logger - %message%newline</pattern>
        </encoder>
    </appender>
</configuration>

rollingFileAppender

滚动纪录文件,先将日志记录到指定文件,当符合某种条件时,将日志记录到其他文件,有如下节点:

  • <file>:被写入的文件名,可以是相对目录,也可以解决目录,如果目录不存在则自动创建。
  • <append>:如果是true,日志被追加到文件结尾,如果是false,清空现存文件,默认是true。
  • <encoder>:对日志进行格式化。
  • <rollingPolicy>:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。

rollingPolicy

  • TimeBaseRollingPolicy :最常用的滚动策略,根据时间来制定滚动策略,即负责滚动也负责触发滚动。有如下节点;

    • <fileNamePattern>:必要节点,包含文件及“%d” 转换符,“%d”可以包含一个java.text.SimpleDateFormat 制定的时间格式,如:%d{yyyy-MM},如果直接使用%d ,默认格式是 yyyy-MM-dd。
    • <maxHistory>:可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件,假设设置每个月滚动,且<maxHistory> 是 6,则只保存最近6个月的文件,删除之前的旧文件,注意:删除旧文件是哪些为了归档而创建的目录也会被删除。
    • <filenamePattern>:必须包含“%i” 例如:设置最小值,和最大值分别为1和2,命名模式为 log%i.log,会产生归档文件log1.log和log2.log,还可以指定文件压缩选项,例如:log%i.log.gz 或者 log%i.log.zip
  • triggeringPolicy:告知RollingFileAppender,激活RollingFileAppender滚动。
<!-- 03:conf errorAppender out -->
<appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
    <file>logs/error.log</file>
    <!-- 设置滚动策略 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
        <!--设置日志命名模式--> 
        <fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
        <!--最多保留30天log-->
        <maxHistory>30</maxHistory>
    </rollingPolicy>
    <!-- 超过150MB时,触发滚动策略 -->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
        <maxFileSize>150</maxFileSize>
    </triggeringPolicy>
    <encoder>
        <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
    </encoder>
</appender>

logger节点

logger是<configuration>的子节点,来设置某一个包或者具体的某一个类的日志打印级别,以及指定<appender>。logger仅有一个name属性,两个可选属性 level/addtivity。

  • name:用来指定受此loger约束的某一个包或者具体的某一个类。
  • level:用来设置打印级别,大小写无关。可选值有TRACE、DEBUG、INFO、WARN、ERROR、ALL和OFF。还有一个特俗值INHERITED 或者 同义词NULL,代表强制执行上级的级别。如果未设置此属性,那么当前logger将会继承上级的级别。
  • addtivity:是否向上级logger传递打印信息,默认为true;

<logger>可以包含零个或多个<appender-ref>元素,表示这个appender将会添加到logger。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤掉非INFO级别 -->
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!--  conf infoAppender out -->
    <appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">
        <file>logs/info.log</file>
        <!-- 设置滚动策略 -->
        <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <!--设置日志命名模式--> 
            <fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--最多保留30天log-->
            <maxHistory>30</maxHistory>
        </rollingPoliy>
        <!-- 超过150MB时,触发滚动策略 -->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>150</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
        </encoder>
    </appender>

    <!-- 添加两个appender节点 -->
    <logger name="logback.olf.log" level="info">
        <appender-ref ref = "console_out"/>
        <appender-ref ref = "infoAppender"/>
    </logger>
</configuration>

root节点

元素配置根logger。该元素有一个level属性,没有name属性,因为已经被命名 为root。Level属性的值大小写无关,其值为下面其中一个字符串:TRACE、DEBUG、INFO、 WARN、ERROR、ALL 和 OFF。如果 root 元素没 有引用任何 appender,就会失去所有 appender。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤掉非INFO级别 -->
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 01:conf infoAppender out -->
    <appender name="infoAppender" class="ch.qos.logback.core.RollingFileAppender">

        <file>logs/info.log</file>
        <!-- 设置滚动策略 -->
        <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <!--设置日志命名模式--> 
            <fileNamePattern>infoFile.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--最多保留30天log-->
            <maxHistory>30</maxHistory>
        </rollingPoliy>
        <!-- 超过150MB时,触发滚动策略 -->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>150</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
        </encoder>
    </appender>

    <!-- 02:conf debugAppender out -->
    <appender name="debugAppender" class="ch.qos.logback.core.RollingFileAppender">
        <file>logs/debug.log</file>
        <!-- 设置滚动策略 -->
        <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <!--设置日志命名模式--> 
            <fileNamePattern>debugFile.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--最多保留30天log-->
            <maxHistory>30</maxHistory>
        </rollingPoliy>
        <!-- 超过150MB时,触发滚动策略 -->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>150</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
        </encoder>
    </appender>

    <!-- 03:conf errorAppender out -->
    <appender name="errorAppender" class="ch.qos.logback.core.RollingFileAppender">
        <file>logs/error.log</file>
        <!-- 设置滚动策略 -->
        <rollingPoliy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <!--设置日志命名模式--> 
            <fileNamePattern>errorFile.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--最多保留30天log-->
            <maxHistory>30</maxHistory>
        </rollingPoliy>
        <!-- 超过150MB时,触发滚动策略 -->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>150</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>%d [%p] %-5level %logger - %msg%newline</pattern>
        </encoder>
    </appender>

    <root level="ALL">
        <appender-ref ref="infoAppender"/>
        <appender-ref ref="debugAppender"/>
        <appender-ref ref="errorAppender"/>
    </root>
</configuration>

filter过滤节点

级别过滤器(LevelFilter)

LevelFilter 根据记录级别对记录事件进行过滤。如果事件的级别等于配置的级别,过滤 器会根据 onMatch 和 onMismatch 属性接受或拒绝事件。下面是个配置文件例子:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤掉非INFO级别 -->
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>

        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="console_out" />
    </root>
</configuration>

临界值过滤器(ThresholdFilter)

ThresholdFilter过滤掉低于指定临界值的事件。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">  
        <!-- 过滤掉TRACE和DEBUG级别的日志 -->
            <level>INFO</level> 
        </filter>

        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="console_out" />
    </root>
</configuration>

求值过滤器(EvaluatorFilter)

评估是否符合指定的条件

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.EvaluatorFilter">  
             <evaluator>
             <!--过滤掉所有日志中不包含hello字符的日志-->
                <expression>
                    message.contains("hello")
                </expression>
                <onMatch>NEUTRAL</onMatch>
                <onMismatch>DENY</onMismatch>
             </evaluator>
        </filter>

        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="console_out" />
    </root>
</configuration>

匹配器(Matchers)

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <!-- conf consoel out -->
    <appender name ="console_out" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.EvaluatorFilter">  
             <evaluator> 
                <matcher>
                    <Name>odd</Name>
                    <!-- 过滤掉序号为奇数的语句-->
                    <regex>statement [13579]</regex>
                </matcher>
                <expression>odd.matches(formattedMessage)</expression>
                <onMatch>NEUTRAL</onMatch>
                <onMismatch>DENY</onMismatch>
             </evaluator>
        </filter>

        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{30} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="console_out" />
    </root>
</configuration>

下面是一个我常用的logback.xml配置文件,供大家参考:

<?xml version="1.0" encoding="UTF-8"?>

<configuration debug="true" scan="true" scanPeriod="30 seconds"> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%level] - %m%n</pattern>

        <!-- 常用的Pattern变量,大家可打开该pattern进行输出观察 -->
        <!-- 
          <pattern>
              %d{yyyy-MM-dd HH:mm:ss} [%level] - %msg%n
              Logger: %logger
              Class: %class
              File: %file
              Caller: %caller
              Line: %line
              Message: %m
              Method: %M
              Relative: %relative
              Thread: %thread
              Exception: %ex
              xException: %xEx
              nopException: %nopex
              rException: %rEx
              Marker: %marker
              %n

          </pattern>
           -->
    </encoder>
  </appender>

  <!-- 按日期区分的滚动日志 -->
  <appender name="ERROR-OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>logs/error.log</file>

    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%class:%line] - %m%n</pattern>
    </encoder>

      <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>ERROR</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>

      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!-- daily rollover -->
      <fileNamePattern>error.%d{yyyy-MM-dd}.log.zip</fileNamePattern>

      <!-- keep 30 days' worth of history -->
      <maxHistory>30</maxHistory>
    </rollingPolicy>
  </appender>

  <!-- 按文件大小区分的滚动日志 -->
  <appender name="INFO-OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>logs/info.log</file>

    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%class:%line] - %m%n</pattern>
    </encoder>

      <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>

      <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
      <fileNamePattern>info.%i.log</fileNamePattern>
      <minIndex>1</minIndex>
      <maxIndex>3</maxIndex>
    </rollingPolicy>

    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>5MB</maxFileSize>
    </triggeringPolicy>

  </appender>

  <!-- 按日期和大小区分的滚动日志 -->
  <appender name="DEBUG-OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>logs/debug.log</file>

    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%class:%line] - %m%n</pattern>
    </encoder>

      <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>DEBUG</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>

      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!-- rollover daily -->
      <fileNamePattern>debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>

      <timeBasedFileNamingAndTriggeringPolicy
            class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <!-- or whenever the file size reaches 100MB -->
        <maxFileSize>100MB</maxFileSize>
      </timeBasedFileNamingAndTriggeringPolicy>

    </rollingPolicy>

  </appender>

   <!-- 级别阀值过滤 -->
  <appender name="SUM-OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>logs/sum.log</file>

    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%class:%line] - %m%n</pattern>
    </encoder>

    <!-- deny all events with a level below INFO, that is TRACE and DEBUG -->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>INFO</level>
    </filter>

      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!-- rollover daily -->
      <fileNamePattern>debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>

      <timeBasedFileNamingAndTriggeringPolicy
            class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <!-- or whenever the file size reaches 100MB -->
        <maxFileSize>100MB</maxFileSize>
      </timeBasedFileNamingAndTriggeringPolicy>

    </rollingPolicy>

  </appender>

  <root level="debug">
    <appender-ref ref="STDOUT" />
    <appender-ref ref="ERROR-OUT" />
    <appender-ref ref="INFO-OUT" />
    <appender-ref ref="DEBUG-OUT" />
    <appender-ref ref="SUM-OUT" />
  </root>
</configuration>

日志系统转换指南

在实际的日志转换过程中,SLF4J其实是充当了一个中介的角色。例如当我们一个项目原来是使用LOG4J进行日志记录,但是我们要换成LogBack进行日志记录。此时我们需要先将LOG4J转换成SLF4J日志系统,再从SLF4J日志系统转成LogBack日志系统。

从日志框架转向SLF4J

  • jul-to-slf4j:jdk-logging到slf4j的桥梁
  • log4j-over-slf4j:log4j1到slf4j的桥梁
  • jcl-over-slf4j:commons-logging到slf4j的桥梁

从SLF4J转向具体的日志框架

  • slf4j-jdk14:slf4j到jdk-logging的桥梁
  • slf4j-log4j12:slf4j到log4j1的桥梁
  • log4j-slf4j-impl:slf4j到log4j2的桥梁
  • logback-classic:slf4j到logback的桥梁
  • slf4j-jcl:slf4j到commons-logging的桥梁

日志技术框架一览

  • JUL:JDK中的日志记录工具,也常称为JDKLog、jdk-logging。
  • LOG4J1:一个具体的日志实现框架。
  • LOG4J2:一个具体的日志实现框架,是LOG4J1的下一个版本。
  • LOGBACK:一个具体的日志实现框架,但其性能更好。
  • JCL:一个日志门面,提供统一的日志记录接口,也常称为commons-logging。
  • SLF4J:一个日志门面,与JCL一样提供统一的日志记录接口,可以方便地切换看具体的实现框架。

JUL、LOG4J1、LOG4J2、LOGBACK是日志实现框架,而JCL、SLF4J是日志实现门面

在项目开发过程中,我们可以通过 debug 查找问题。而在线上环境我们查找问题只能通过打印日志的方式查找问题。因此对于一个项目而言,日志记录是一个非常重要的问题。因此,如何选择一个合适的日志记录框架也非常重要。

在Java开发中,常用的日志记录框架有JDKLog、Log4J、LogBack、SLF4J、SLF4J。这些日志记录框架各有各的特点,各有各的应用场景。了解这些框架的特点及应用场景,有利于我们做技术选型的时候做出正确的判断。

JDKLog:日志小刀

JDKLog是JDK官方提供的一个记录日志的方式,直接在JDK中就可以使用。

import java.util.logging.Logger;

/****
 ** JDKLog Demo
 **/
public class JDKLog
{
    public static void main( String[] args )
    {
        Logger logger = Logger.getLogger("JDKLog");
        logger.info("Hello World.");
    }
}

JDKLog 的有点是使用非常简单,直接在 JDK 中就可以使用。但 JDKLog 功能比较太过于简单,不支持占位符显示,拓展性比较差,所以现在用的人也很少。

Log4J:日志大炮

Log4J 是 Apache 的一个日志开源框架,有多个分级(DEBUG/INFO/WARN/ERROR)记录级别,可以很好地将不同日志级别的日志分开记录,极大地方便了日志的查看。

Log4J 有 1.X 版本和 2.X 版本,现在官方推荐使用 2.X 版本,2.X 版本在架构上进行了一些升级,配置文件也发生了一些变化。但好在官方的配置说明文档非常清楚,通过查阅文档能解决大部分的问题。

使用 Log4J 框架首先需要引入依赖的包:

<!-- Log4J -->
<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>

增加配置文件 log4j2.xml 放在 resource 目录下:

<?xml version="1.0" encoding="UTF-8"?>
<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="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

其中<Root>节点的 level 属性表示输出的最低级别。

最后编写一个测试类:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/****
 ** Log4J Demo
 **/
public class Log4jLog {
    public static void main(String args[]) {
        Logger logger = LogManager.getLogger(Log4jLog.class);
        logger.debug("Debug Level");
        logger.info("Info Level");
        logger.warn("Warn Level");
        logger.error("Error Level");
    }
}

运行测试类输出结果:

10:16:08.279 [main] INFO  com.chanshuyi.Log4jLog - Info Level
10:16:08.280 [main] WARN  com.chanshuyi.Log4jLog - Warn Level
10:16:08.280 [main] ERROR com.chanshuyi.Log4jLog - Error Level

如果没有配置 log4j2.xml 配置文件,那么LOG4J将自动启用类似于下面的的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<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="error">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

使用默认配置文件的输出结果:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
11:40:07.377 [main] ERROR com.chanshuyi.Log4jLog - Error Level

从上面的使用步骤可以看出 Log4J 的使用稍微复杂一些,但是条理还是很清晰的。而且因为 Log4J 有多个分级(DEBUG/INFO/WARN/ERROR)记录级别,所以可以很好地记录不同业务问题。因为这些优点,所以在几年前几乎所有人都使用 Log4J 作为日志记录框架,群众基础可谓非常深厚。

但 Log4J 本身也存在一些缺点,比如不支持使用占位符,不利于代码阅读等缺点。但是相比起 JDKLog,Log4J 可以说是非常好的日志记录框架了。

LogBack:日志火箭

LogBack 其实可以说是 Log4J 的进化版,因为它们两个都是同一个人(Ceki Gülcü)设计的开源日志组件。LogBack 除了具备 Log4j 的所有优点之外,还解决了 Log4J 不能使用占位符的问题。

使用 LogBack 需要首先引入依赖:

<!-- LogBack -->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.1.7</version>
</dependency>

配置 logback.xml 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
        </layout>
    </appender>
    <logger name="com.chanshuyi" level="TRACE"/>

    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

LogBack 的日志级别区分可以细分到类或者包,这样就可以使日志记录变得更加灵活。之后在类文件中引入Logger类,并进行日志记录:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/****
 ** LogBack Demo
 **/
public class LogBack {
    static final Logger logger = LoggerFactory.getLogger(LogBack.class);
    public static void main(String[] args) {
        logger.trace("Trace Level.");
        logger.debug("Debug Level.");
        logger.info("Info Level.");
        logger.warn("Warn Level.");
        logger.error("Error Level.");
    }
}

输出结果:

14:34:45.747 [main] TRACE com.chanshuyi.LogBack - Trace Level.
14:34:45.749 [main] DEBUG com.chanshuyi.LogBack - Debug Level.
14:34:45.749 [main] INFO  com.chanshuyi.LogBack - Info Level.
14:34:45.749 [main] WARN  com.chanshuyi.LogBack - Warn Level.
14:34:45.749 [main] ERROR com.chanshuyi.LogBack - Error Level.

LogBack 解决了 Log4J 不能使用占位符的问题,这使得阅读日志代码非常方便。除此之外,LogBack 比 Log4J 有更快的运行速度,更好的内部实现。并且 LogBack 内部集成了 SLF4J 可以更原生地实现一些日志记录的实现。

SLF4J:适配器

上面说了 JDKLog、Log4J、LogBack 这几个常用的日志记录框架,它们都有各自的优缺点,适合在不同的场景下使用。可能简单的项目直接用 JDKLog 就可以了,而复杂的项目需要用上 Log4J。

很多时候我们做项目都是从简单到复杂,也就是我们很可能一开始使用的是 JDKLog,之后业务复杂了需要使用 Log4J,这时候我们如何将原来写好的日志用新的日志框架输出呢?

一个最死板的方法就是一行行代码修改,把之前用 JDKLog 的日志代码全部修改成 Log4J 的日志接口。但是这种方式不仅效率低下,而且做的工作都是重复性的工作,这怎么能忍呢。

正式因为在实际的项目应用中,有时候可能会从一个日志框架切换到另外一个日志框架的需求,这时候往往需要在代码上进行很大的改动。为了避免切换日志组件时要改动代码,这时候一个叫做 SLF4J(Simple Logging Facade for Java,即Java简单日志记录接口集)的东西出现了。

SLF4J(Simple Logging Facade for Java,即Java简单日志记录接口集)是一个日志的接口规范,它对用户提供了统一的日志接口,屏蔽了不同日志组件的差异。这样我们在编写代码的时候只需要看 SLF4J 这个接口文档即可,不需要去理会不同日之框架的区别。而当我们需要更换日志组件的时候,我们只需要更换一个具体的日志组件Jar包就可以了。

而整合 SLF4J 和日志框架使用也是一件很简单的事情。

SLF4J+JDKLog

SLF4J + JDKLog 需要在 Maven 中导入以下依赖包:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.21</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-jdk14</artifactId>
  <version>1.7.21</version>
</dependency>

编写测试类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/****
 ** SLF4J + JDKLog
 **/
public class Slf4jJDKLog {
    final static Logger logger = LoggerFactory.getLogger(Slf4jJDKLog.class);
    public static void main(String[] args) {
        logger.trace("Trace Level.");
        logger.info("Info Level.");
        logger.warn("Warn Level.");
        logger.error("Error Level.");
    }
}

输出结果:

七月 15, 2016 3:30:02 下午 com.chanshuyi.slf4j.Slf4jJDKLog main
信息: Info Level.
七月 15, 2016 3:30:02 下午 com.chanshuyi.slf4j.Slf4jJDKLog main
警告: Warn Level.
七月 15, 2016 3:30:02 下午 com.chanshuyi.slf4j.Slf4jJDKLog main
严重: Error Level.

SLF4J+LOG4J

需要依赖的 Jar 包:slf4j-api.jar、slf4j-412.jar、log4j.jar,导入Maven依赖:

<!-- 2.SLF4J + Log4J -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.21</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.7.21</version>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

配置 log4j.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/' >

    <appender name="myConsole" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"
                   value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n" />
        </layout>
        <!--过滤器设置输出的级别-->
        <filter class="org.apache.log4j.varia.LevelRangeFilter">
            <param name="levelMin" value="debug" />
            <param name="levelMax" value="error" />
            <param name="AcceptOnMatch" value="true" />
        </filter>
    </appender>

    <!-- 根logger的设置-->
    <root>
        <priority value ="debug"/>
        <appender-ref ref="myConsole"/>
    </root>
</log4j:configuration>

我们还是用上面的代码,无需做改变,运行结果为:

[15 16:04:06,371 DEBUG] [main] slf4j.SLF4JLog - Debug Level.
[15 16:04:06,371 INFO ] [main] slf4j.SLF4JLog - Info Level.
[15 16:04:06,371 WARN ] [main] slf4j.SLF4JLog - Warn Level.
[15 16:04:06,371 ERROR] [main] slf4j.SLF4JLog - Error Level.

SLF4J+LogBack

导入依赖:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.21</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.1.7</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-core</artifactId>
  <version>1.1.7</version>
</dependency>

配置 logback.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
        </layout>
    </appender>
    <logger name="com.chanshuyi" level="TRACE"/>

    <root level="warn">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

我们还是用上面的代码,无需做改变,运行结果为:

16:08:01.040 [main] TRACE com.chanshuyi.slf4j.SLF4JLog - Trace Level.
16:08:01.042 [main] DEBUG com.chanshuyi.slf4j.SLF4JLog - Debug Level.
16:08:01.043 [main] INFO  com.chanshuyi.slf4j.SLF4JLog - Info Level.
16:08:01.043 [main] WARN  com.chanshuyi.slf4j.SLF4JLog - Warn Level.
16:08:01.043 [main] ERROR com.chanshuyi.slf4j.SLF4JLog - Error Level.

经过上面的介绍,相信大家对 Java 常用的日志框架都有了一定认识。

1. 什么是logback

Logback是一个用于Java应用程序的日志框架,它由log4j框架的创始人Ceki Gülcü开发。logback是log4j框架的继承者和改进版,并提供了更好的性能、可扩展性和灵活性。与Log4j相比,Logback提供了更快的速度和更低的内存占用,这使得它成为大型企业级应用程序的理想选择。

Logback包括三个模块:logback-core、logback-classic和logback-access。

  1. logback-core提供了通用的日志记录功能;
  2. logback-classic提供了与SLF4J API和log4j API兼容的日志记录功能;
  3. logback-access提供了HTTP访问日志的功能。

同时,Logback还支持异步日志记录、多线程环境下的高性能、动态配置等特性,使得它成为Java应用程序中广泛使用的日志框架之一。

2. logback的日志级别

Logger有五个日志级别,分别是TRACEDEBUGINFOWARNERROR
这五个级别优先级为,TRACE<DEBUG<INFO<WARN<ERROR

在打印日志的时候,只会打印当前日志级别高于或者等于当前日志级别的日志信息。例如当前日志级别为INFO,那么只会记录日志级别为INFO、WARN、ERROR的日志信息。

查看springboot项目的日志级别,使用下面代码进行测试。

@Service
public class LogTest {
    private static final Logger logger = LoggerFactory.getLogger(LogTest.class);
    public  void test() {
        logger.trace("===trace===");
        logger.debug("===debug===");
        logger.info("===info===");
        logger.warn("===warn===");
        logger.error("===error===");
    }
}

测试代码

@SpringBootTest
class LogbackApplicationTests {

    @Resource
    private LogTest logTest;
    @Test
    void testqwe() {
        logTest.test();
    }
}

测试结果
在这里插入图片描述
所以SpringBoot项目中logback默认日志级别是INFO

3. 日志级别的层级

一般日志的最高层级就是root。例如现在创建一个logger是com.example.logback下的logger,结构如下。
在这里插入图片描述
这里的日志级别如果没有指定,也会继承父类。

4. logback配置文件

logback配置文件一般在springboot项目的src/main/resources目录下创建名为logback-spring.xml的文件

配置文件基本结构如下图:
在这里插入图片描述

4.1 logger标签

logger就是日志记录器,用来控制要输出哪些日志记录语句,对日志信息进行级别限制。

有level属性、name属性、additivity属性,其中name属性必须要写,指定到哪一个类或者哪一个包,additivity表示是否向上一层传递打印信息,默认为true。可以包含 appender-ref 元素
使用示例:

<configuration>
    <logger level="ERROR" name="com.example.testlog.LogTest" additivity="false">
    	<appender-ref ref="STDOUT"></appender-ref>
    </logger>
</configuration>

4.2 root标签

root标签指定最基础的的日志输出级别,它只有一个level属性,可以包含 appender-ref 元素。
level属性可以选择,ALLTRACEDEBUGINFOWARNERRORNULLOFFINHERITED
使用示例:

<configuration>
    <root level="debug">
        <appender-ref ref="STDOUT"></appender-ref>
    </root>
</configuration>

4.3 appender标签

appender就是附加器,日志记录器会将输出的任务交给附加器完成,不同的附加器会将日志输出到不同的地方,例如控制台、文件、网络等

几个常见的附加器:

  1. 控制台附加器:ch.qos.logback.core.ConsoleAppender
  2. 文件附加器:ch.qos.logback.core.FileAppender
  3. 滚动文件附加器:ch.qos.logback.core.rolling.RollingFileAppender

属性有name、class,class用来指定附加器,name来表示当前附件器的名字。其他需要指定附件器的标签,可以通过appender-ref标签中的ref来指定。可以包含encoder元素、fileter元素等

使用示例:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
</configuration>

4.4 filter标签

filter是过滤器,过滤器是附件器的一个组件,它是用于判断附件器是否输出日志的。一个附件器可以包含多个过滤器。
过滤器只能有三个值,DENYNEUTRALACCEPT
DENY是不输出日志
NEUTRAL是不决定是否输出日志
ACCEPT是输出日志。

可以有三个元素,level元素、onMatch元素、onMismatch元素。<level>:设置过滤级别
<onMatch>:用于配置符合过滤条件的操作
<onMismatch>:用于配置不符合过滤条件的操作。
使用示例:

<configuration>
 	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <filter>
            <level>info</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
</configuration>

4.5 encoder标签

encoder最主要的就是pattern标签,用于控制输出日志的格式

%d: 表示日期
%-5level:日志级别
%thread:表示线程名
%logger:输出日志的类名
logger{length}:对输出日志的类名缩写展示
%msg:日志输出内容
%n:换行符
-:左对齐

使用示例:

<encoder>
 	<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</encoder>

4.6 property标签

property标签用来定义变量, 有两个属性,name和value;其中name的值是变量的名称,value的值时变量定义的值

使用示例:

<property name="HOME" value="../log">

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
	<file>${HOME}/log.log</file>
</appender>

5. 整体演示

5.1 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration  xmlns="http://ch.qos.logback/xml/ns/logback"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback
                https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>E:/workspace/logback/logfile.txt</file>
        <append>true</append>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>



    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger level="trace" name="com.example.testlog.LogTest" additivity="false">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </logger>

    

</configuration>

5.2 运行结果

控制台打印的信息
在这里插入图片描述

文件夹输出的信息

在这里插入图片描述

写这篇文章的初衷,是想在团队内做一次Java日志的分享,因为日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式。但在准备分享、补充细节的过程中,我又进一步发现目前日志相关的文章,都只是专注于某一个方面,或者讲历史和原理,或者解决包冲突,却都没有把整个Java日志知识串联起来。最终这篇文章超越了之前的定位,越写越丰富,为了让大家看得不累,我的文章将以系列的形式展示。

一、前言

日志发展到今天,被抽象成了三层:接口层、实现层、适配层:
  • 接口层:或者叫日志门面(facade),就是interface,只定义接口,等着别人实现。
  • 实现层:真正干活的、能够把日志内容记录下来的工具。但请注意它不是上边接口实现,因为它不感知也不直接实现接口,仅仅是独立的实现。
  • 适配层:一般称为Adapter,它才是上边接口的implements。因为接口层和适配层并非都出自一家之手,它们之间无法直接匹配。而鲁迅曾经说过:「计算机科学领域的任何问题都可以通过增加一个中间层来解决」(All problems in computer science can be solved by another level of indirection. -- David Wheeler[1]),所以就有了适配层。

适配层又可以分为绑定(Binding)和桥接(Bridging)两种能力:

  • 绑定(Binding):将接口层绑定到某个实现层(实现一个接口层,并调用实现层的方法)
  • 桥接(Bridging):将接口层桥接到另一个接口层(实现一个接口层,并调用另一个接口层的接口),主要作用是方便用户低成本的在各接口层和适配层之间迁移

如果你觉得上面的描述比较抽象生硬,可以先跳过,等把本篇看完自然就明白了。
接下来我们就以时间顺序,回顾一下Java日志的发展史,这有助于指导我们后续的实践,真正做到知其所以然。

二、历史演进

 

2.1 标准输出 (<1999)

Java最开始并没有专门记录日志的工具,大家都是用System.out和System.err输出日志。但它们只是简单的信息输出,无法区分错误级别、无法控制输出粒度,也没有什么管理、过滤能力。随着Java工程化的深入,它们的能力就有些捉襟见肘了。

虽然System.out和System.err默认输出到控制台,但它们是有能力将输出保存到文件的:

  •  
  •  
  •  
  •  
  •  
System.setOut(new PrintStream(new FileOutputStream("log.txt", true)));System.out.println("这句将输出到 log.txt 文件中");
System.setErr(new PrintStream(new FileOutputStream("error.txt", true)));System.err.println("这句将输出到 error.txt 文件中");

 

2.2 Log4j (1999)

在1996年,一家名为SEMPER的欧洲公司决定开发一款用于记录日志的工具。经过多次迭代,最终发展成为Log4j。这款工具的主要作者是一位名叫Ceki Gülcü[2]的俄罗斯程序员,请记住他的名字:Ceki,后面还会多次提到他。

到了1999年,Log4j已经被广泛使用,随着用户规模的增长,用户诉求也开始多样化。于是Ceki在2001年选择将Log4j开源,希望借助社区的力量将Log4j发展壮大。不久之后Apache基金会向Log4j抛出了橄榄枝,自然Ceki也加入Apache继续从事 Log4j的开发,从此Log4j改名Apache Log4j[3]并进入发展的快车道。

Log4j相比于System.out提供了更强大的能力,甚至很多思想到现在仍被广泛接受,比如:

  • 日志可以输出到控制台、文件、数据库,甚至远程服务器和电子邮件(被称做 Appender);
  • 日志输出格式(被称做 Layout)允许定制,比如错误日志和普通日志使用不同的展现形式;
  • 日志被分为5个级别(被称作Level),从低到高依次是debug, info, warn, error, fatal,输出前会校验配置的允许级别,小于此级别的日志将被忽略。除此之外还有all, off两个特殊级别,表示完全放开和完全关闭日志输出;
  • 可以在工程中随时指定不同的记录器(被称做Logger),可以为之配置独立的记录位置、日志级别;
  • 支持通过properties或者xml文件进行配置;

随着Log4j的成功,Apache又孵化了Log4Net[4]、Log4cxx[5]、Log4php[6]产品,开源社区也模仿推出了如Log4c[7]、Log4cpp[8]、Log4perl[9]等众多项目。从中也可以印证Log4j在日志处理领域的江湖影响力。

不过Log4j有比较明显的性能短板,在Logback和Log4j 2推出后逐渐式微,最终Apache在2015年宣布终止开发Log4j并全面迁移至Log4j 2[10](可参考【2.7 Log4j 2 (2012)】)。

 

2.3 JUL (2002.2)

随着Java工程的发展,Sun也意识到日志记录非常重要,认为这个能力应该由JRE原生支持。所以在1999年Sun提交了JSR 047[11]提案,标题就叫「Logging API Specification」。不过直到2年后的2002年,Java官方的日志系统才随Java 1.4发布。这套系统称做Java Logging API,包路径是java.util.logging,简称JUL。

在某些追溯历史的文章中提到,「Apache曾希望将 Log4j加入到JRE中作为默认日志实现,但傲慢的Sun没有答应,反而很快推出了自己的日志系统」。对于这个说法我并没有找到出处,无法确认其真实性。

不过从实际推出的产品来看,更晚面世的JUL无论是功能还是性能都落后于Log4j,颇有因被寄予厚望而仓促发布的味道,也许那个八卦并非空穴来风,哈哈。虽然在2004年推出的Java 5.0 (1.5) [12]上JUL进步不小,但它在Log4j面前仍无太多亮点,广大开发者并没有迁移的动力,导致JUL始终未成气候。

我们在后文没有推荐JUL的计划,所以这里也不多介绍了(主要是我也不会)。

 

2.4 JCL (2002.8)

在Log4j和JUL之外,当时市面上还有像Apache Avalon[13](一套服务端开发框架)、 Lumberjack[14](一套跑在JDK 1.2/1.3上的开源日志工具)等日志工具。
对于独立且轻量的项目来说,开发者可以根据喜好使用某个日志方案即可。但更多情况是一套业务系统依赖了大量的三方工具,而众多三方工具会各自使用不同的日志实现,当它们被集成在一起时,必然导致日志记录混乱。

为此Apache在2002年推出了一套接口Jakarta Commons Logging[15],简称 JCL,它的主要作者仍然是Ceki。这套接口主动支持了Log4j、JUL、Apache Avalon、Lumberjack等众多日志工具。开发者如果想打印日志,只需调用JCL的接口即可,至于最终使用的日志实现则由最上层的业务系统决定。我们可以看到,这其实就是典型的接口与实现分离设计。

但因为是先有的实现(Log4j、JUL)后有的接口(JCL),所以JCL配套提供了接口与实现的适配层(没有使用它的最新版,原因会在【1.2.7 Log4j2 (2012)】提到):

简单介绍一下JCL自带的几个适配层/实现层:

  • AvalonLogger/LogKitLogger:用于绑定Apache Avalon的适配层,因为Avalon 不同时期的日志包名不同,适配层也对应有两个

  • Jdk13LumberjackLogger:用于绑定Lumberjack的适配层

  • Jdk14Logger:用于绑定JUL(因为JUL从JDK 1.4开始提供)的适配层

  • Log4JLogger:用于绑定Log4j的适配层

  • NoOpLog:JCL自带的日志实现,但它是空实现,不做任何事情

  • SimpleLog:JCL自带的日志实现 ,让用户哪怕不依赖其他工具也能打印出日志来,只是功能非常简单

当时项目前缀取名Jakarta,是因为它属于Apache与Sun共同推出的Jakarta Project[16]项目(邮件[17])。现在JCL作为Apache Commons[18]的子项目,叫 Apache Commons Logging,与我们常用的Commons Lang[19]、Commons Collections [20]等是师兄弟。但JCL的简写命名被保留了下来,并没有改为ACL。

 

2.5 Slf4j (2005)

Log4j的作者Ceki看到了很多Log4j和JCL的不足,但又无力推动项目快速迭代,加上对Apache的管理不满,认为自己失去了对Log4j项目的控制权(博客[21]、邮件[22]),于是在2005年选择自立门户,并很快推出了一款新作品Simple Logging Facade for Java[23],简称Slf4j。

Slf4j也是一个接口层,接口设计与JCL非常接近(毕竟有师承关系)。相比JCL有一个重要的区别是日志实现层的绑定方式:JCL是动态绑定,即在运行时执行日志记录时判定合适的日志实现;而Slf4j选择的是静态绑定,应用编译时已经确定日志实现,性能自然更好。这就是常被提到的classloader问题,更详细地讨论可以参考What is the issue with the runtime discovery algorithm of Apache Commons Logging[24]以及Ceki自己写的文章Taxonomy of class loader problems encountered when using Jakarta Commons Logging[25]。

在推出Slf4j的时候,市面上已经有了另一套接口层JCL,为了将选择权交给用户(我猜也为了挖JCL的墙角),Slf4j推出了两个桥接层:

  • jcl-over-slf4j:作用是让已经在使用JCL的用户方便的迁移到Slf4j 上来,你以为调的是JCL接口,背后却又转到了Slf4j接口。我说这是在挖JCL的墙角不过分吧?
  • slf4j-jcl:让在使用Slf4j的用户方便的迁移到JCL上,自己的墙角也挖,主打的就是一个公平公正公开。

Slf4j通过推出各种适配层,基本满足了用户的所有场景,我们来看一下它的全家桶:

2.6 Logback (2006)

然而Ceki的目标并不止于Slf4j,面对自己一手创造的Log4j,作为原作者自然是知道它存在哪些问题的。于是在2006年Ceki又推出了一款日志记录实现方案:Logback[26]。无论是易用度、功能、还是性能,Logback 都要优于Log4j,再加上天然支持Slf4j而不需要额外的适配层,自然拥趸者众。目前Logback已经成为Java社区最被广泛接受的日志实现层(Logback自己在2021年的统计是48%的市占率[27])。

相比于Log4j,Logback提供了很多我们现在看起来理所当然的新特性:

  • 支持日志文件切割滚动记录、支持异步写入
  • 针对历史日志,既支持按时间或按硬盘占用自动清理,也支持自动压缩以节省硬盘空间
  • 支持分支语法,通过<if>, <then>, <else>可以按条件配置不同的日志输出逻辑,比如判断仅在开发环境输出更详细的日志信息
  • 大量的日志过滤器,甚至可以做到通过登录用户Session识别每一位用户并输出独立的日志文件
  • 异常堆栈支持打印jar包信息,让我们不但知道调用出自哪个文件哪一行,还可以知道这个文件来自哪个jar包

Logback主要由三部分组成(网上各种文章在介绍classic和access时都描述的语焉不详,我不得不直接翻官网文档找更明确的解释):

  • logback-core:记录/输出日志的核心实现
  • logback-classic:适配层,完整实现了Slf4j接口
  • logback-access[28]:用于将Logback集成到Servlet容器(Tomcat、Jetty)中,让这些容器的HTTP访问日志也可以经由强大的Logback输出

 

2.7 Log4j 2 (2012)

看着Slf4j + Logback搞的风生水起,Apache自然不会坐视不理,终于在2012年憋出一记大招:Apache Log4j 2[29],它自然也有不少亮点:

  • 插件化结构[30],用户可以自己开发插件,实现Appender、Logger、Filter完成扩展
  • 基于LMAX Disruptor的异步化输出[31],在多线程场景下相比Logback有10倍左右的性能提升,Apache官方也把这部分作为主要卖点加以宣传,详细可以看Log4j 2 Performance[32]。

Log4j 2主要由两部分组成:

  • log4j-core:核心实现,功能类似于logback-core
  • log4j-api:接口层,功能类似于Slf4j,里面只包含Log4j 2的接口定义
你会发现Log4j 2的设计别具一格,提供JCL和Slf4j之外的第三个接口层(log4j-api,虽然只是自己的接口),它在官网API Separation[33]一节中解释说,这样设计可以允许用户在一个项目中同时使用不同的接口层与实现层。

不过目前大家一般把Log4j 2作为实现层看待,并引入JCL或Slf4j作为接口层。特别是JCL,在时隔近十年后,于2023年底推出了1.3.0 版[34],增加了针对Log4j 2的适配。还记得我们在【1.2.4 JCL (2002.8)】中没有用最新版的JCL做介绍吗,就是因为这个十年之后的版本把那些已经「作古」的日志适配层@Deprecated掉了。

多说一句,其实Logback和Slf4j就像log4j-core和log4j-api的关系一下,目前如果你想用Logback也只能借助Slf4j。但谁让它们生逢其时呢,大家就会分别讨论认为是两个产品。

虽然Log4j 2发布至今已有十年(本文写于2024年),但它仍然无法撼动Logback的江湖地位,我个人总结下来主要有两点:

  • Log4j 2虽然顶着Log4j的名号,但却是一套完全重写的日志系统,无法只通过修改Log4j版本号完成升级,历史用户升级意愿低
  • Log4j 2比Logback晚面世6年,却没有提供足够亮眼及差异化的能力(前边介绍的两个亮点对普通用户并没有足够吸引力),而Slf4j+Logback这套组合已经非常优秀,先发优势明显

比如,曾有人建议Spring Boot将日志系统从Logback切换到Log4j2[35],但被Phil Webb[36](Spring Boot核心贡献者)否决。他在回复中给出的原因包括:Spring Boot需要保证向前兼容以方便用户升级,而切换Log4j 2是破坏性的;目前绝大部分用户并未面临日志性能问题,Log4j 2所推崇的性能优势并非框架与用户的核心关切;以及如果用户想在Spring Boot中切换到Log4j 2也很方便(如需切换可参考 官方文档[37])。

 

2.8 spring-jcl (2017)

因为目前大部分应用都基于Spring/Spring Boot搭建,所以我额外介绍一下spring-jcl [38]这个包,目前Spring Boot用的就是spring-jcl + Logback这套方案。

Spring曾在它的官方Blog《Logging Dependencies in Spring》[39]中提到,如果可以重来,Spring会选择李白Slf4j而不是JCL作为默认日志接口。

现在Spring又想支持Slf4j,又要保证向前兼容以支持JCL,于是从5.0(Spring Boot 2.0)开始提供了spring-jcl这个包。它顶着Spring的名号,代码中包名却与JCL 一致(org.apache.commons.logging),作用自然也与JCL一致,但它额外适配了Slf4j,并将Slf4j放在查找的第一顺位,从而做到了「既要又要」(你可以回到【1.2.4 JCL (2002.8)】节做一下对比)。

如果你是基于Spring Initialize [40]新创建的应用,可以不必管这个包,它已经在背后默默工作了;如果你在项目开发过程中遇到包冲突,或者需要自己选择日志接口和实现,则可以把spring-jcl当作JCL对待,大胆排除即可。

 

2.9 其他

除了我们上边提到的日志解决方案,还有一些不那么常见的,比如:

  • Flogger[41]:由Google在2018年推出的日志接口层。首字母F的含义是Fluent,这也正是它的最大特点:链式调用(或者叫流式API,Slf4j 2.0也支持Fluent API 了,我们会在后续系列文章中介绍)
  • JBoss Logging[42]:由RedHat在约2010年推出,包含完整的接口层、实现层、适配层
  • slf4j-reload4j[43]:Ceki基于Log4j 1.2.7 fork出的版本,旨在解决Log4j的安全问题,如果你的项目还在使用Log4j且不想迁移,建议平替为此版本。(但也不是所有安全问题都能解决,具体可以参考上边的链接)

因为这些日志框架我们在实际开发中用的很少,此文也不再赘述了(主要是我也不会)。

三、总结

历史介绍完了,但故事并没有结束。两个接口(JCL、Slf4j)四个实现(Log4j、JUL、Logback、Log4j2),再加上无数的适配层,它们之间串联成了一个网,我专门画了一张图:

 

解释/补充一下这张图:

  1. 相同颜色的模块拥有相同的groupId,可以参考图例中给出的具体值。
  2. JCL的适配层是直接在它自己的包中提供的,详情我们在前边已经介绍过,可以回【1.2.4 JCL (2002.8)】查看。
  3. 要想使用Logback,就一定绕不开Slf4j(引用它的适配层也算);同样的,要想使用 Log4j 2,那它的log4j-api也绕不开。

如果你之前在看「1.1 前言」时觉得过于抽象,那么此时建议你再回头看一下,相信会有更多体会。

从这段历史,我也发现了几个有趣的细节:

  • 在Log4j 2面世前后的很长一段时间,Slf4j及Logback因为没有竞争对手而更新缓慢。英雄没有对手只能慢慢垂暮,只有棋逢对手才能笑傲江湖。
  • 技术人的善良与倔强:面世晚的产品都针对前辈产品提供支持;面世早的产品都不搭理它的「后辈」。
  • 计算机科学领域的任何问题都可以通过增加一个中间层来解决,如果不行就两个(桥接层干的事儿)。
  • Ceki一人肩挑Java日志半壁江山25年(还在增长ing),真神人也。(当然在代码界有很多这样的神人,比如Linus Torvalds[44]维护Linux至今已有33年,虽然后期主要作为产品经理参与,再比如已故的Bram Moolenaar[45]老爷子持续维护 Vim 32年之久)。

 
 
posted @ 2024-06-19 15:56  CharyGao  阅读(41)  评论(0编辑  收藏  举报