一篇文章弄懂 Java日志体系

714 阅读16分钟

Java日志,我们先按照名词分开来理解,Java 是一门编程语言,日志是指记录一件事情发生的过程,所以Java日志指的就是 Java 程序在执行过程中,用于记录程序的执行过程的工具。当程序执行的时候,你如何追踪程序的执行过程?还像开发的时候 debug ?假如程序执行的过程中出现了错误,你又如何定位分析?如果让你优化一个程序功能,你又该如何判断优化哪一段呢?如果你仔细思考过这个问题,你觉得你会怎么做?我想大家应该都是通过日志去完成,通过日志记录执行程序的时间,参数,还有错误的信息,我们将很方便的进行分析判断。如下图:

Java日志的发展时间线:

image.png

Java日志界的大佬Ceki Gülcü

如果说对Java日志贡献最大的人是谁,感觉应该当属于Ceki Gülcü了。

Ceki Gülcü 这位大神专注日志开发五百年。Java 日志领域的“卷王”,在1996年初,E.U.SMPER   (欧洲安全电子市场)项目决定编写自己的日志API,最后该API演变为 Log4j,该日志软件包一经推出就备受欢迎,当然这里必须要提到一个人,就是 Log4j 的主要贡献者 Ceki Gülcü,后来 Log4j 成为了 Apache 基金会项目中的一员。

这期间 JUL 的出现,引发了日志界 Log4j  与 JUL “楚汉争霸” 局面。为解决这个问题,2002年8月 Apache 又推出了日志接口 JCL ,也就是日志抽象层。希望达到天下之大,唯我 JCL 独尊的地位,但是好景不长,JCL 的名声并不是很好。同时 Ceki Gülcü 由于一些原因离开了  Apache ,这里大家自行脑部各种宫斗场景,哈哈哈。自己创业成立了 qos 公司,并推出了和 JCL 相同的产品 Slf4j 。

由于 Slf4j  出来的较晚,而且还只是一个日志接口,所以之前已经出现的日志产品,如 JUL 和 Log4j 都是没有实现这个接口的,所以尴尬的是光有一个接口,没有实现的产品也是很憋屈啊,就算开发者想用 Slf4j 也是用不了,这时候,大佬Ceki Gülcü发话了 :别急,我早帮你们想好了,要让 sum 或者 Apache  这两个庞然大物来实现我的接口,太南啦,老铁,但…  我帮你们实现,不就完了么… 卧槽,大佬还是我大佬,于是大佬 Ceki Gülcü 连夜撸出了之前提到的桥接包,也就是类似适配器模式。完美解决了与 JUL ,Log4j  这类日志框架包的兼容问题。

Slf4j 虽然可以用,但是想想本质还是使用的别人家的孩子,之前的日志产品都不是正统的 Slf4j 的实现,大佬 Ceki Gülcü 想了想不行不行呀,我得有一个自己的亲孩子。因此,还是出自 Ceki Gülcü 之手的日志产品 Logback 应运而生了【此时作者已经离开了Apache,这名字你细品,我回归了,哈哈哈,是不是很有意思】。Logback 是完美实现了 Slf4j。大佬 Ceki Gülcü 说这就是我的亲孩子。

就这样大家也都看起来相安无事,但 Slf4j  + Logback 的模式,显然很冲击 JCL + Log4j ,并且本身Logback 确实比 Log4j 性能更优,设计更为合理,所以老东家 Apache  可就坐不住了。

在2012年,Apache  直接推出新项目,不是 Log4j.x 升级,而是新项目 Log4j2,因为 Log4j2 是完全不兼容  Log4j.x  的。并且很微妙的,Log4j2 几乎涵盖 Logback 所有的特性,更甚者的 Log4j2 也搞了分离的设计,分化成 log4j-api 和 log4j-core ,这个 log4j-api 也是日志接口,log4j-core 才是日志产品。Apache  为了让大家可以接入自己的 Log4j2 ,那不就是桥嘛,Log4j2 也麻溜的推出了它的桥接包。

至此也就形成了现在 Slf4j 与 Log4j2 南北对持的局面,这也是Java日志界,相爱相杀的历史。

总结一下: Log4j  Slf4j  Logback 是同一个作者,Log4j  后来捐献给 Apache了,作者后来创业又开发了Slf4j 和 Logback 。 JCL  和 Slf4j  都是日志门面接口,具体的实现是通过桥接模式

主要产品

1. JDK日志【java.util.logging   简称 JUL】

从 jdk1.4起,JDK开始自带一套日志系统。JDK Logger最大的优点就是不需要任何类库的支持,只要有Java的运行环境就可以使用。相对于其他的日志框架,JDK自带的日志可谓是鸡肋,无论易用性,功能还是扩展性都要稍逊一筹,所以在商业系统中很少直接使用。

2. Log4j 和 Log4j2 系列日志 【现在都是 Apache 项目】

Apache的一个开放源代码项目,通过使用 log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件、甚至是套接口服务 器、NT的事件记录器、UNIX Syslog守护进程等。用户也可以控制每一条日志的输出格式,通过定义每一条日志信息的级别,用户能够更加细致地控制日志的生成过程。这些可以通过一个配置文件来灵活地进行配置,而不需要修改程序代码。log4j2 是完全不兼容log4j 的,可以理解为两个不同的项目。

3. common-logging 日志接口【简称 JCL】

Apache的一个开放源代码项目,也就是日志抽象层。主要的实现方式还是 log4j 和 jul

4. Slf4j 日志接口

slf4j 全称为 Simple Logging Facade For JAVA,java 简单日志门面。类似于 jcl ,是对不同日志框架提供的一个门面封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。但是,他在编译时静态绑定真正的log库。使用SLF4J时,如果你需要使用某一种日志实现,那么你必须选择对应正确的 slf4j 的 jar 包的集合(各种桥接包)。

slf4j-simple、logback 都是 slf4j 的具体实现,log4j 并不直接实现 slf4j,而是有专门的一层桥接slf4j-log4j12 来实现 slf4j。

5. Logback

logback 是由 log4j 创始人设计的又一个开源日记组件。logback 当前分成三个模块:logback-core, logback-classic 和 logback-access 。logback-core 是其它两个模块的基础模块。logback-classic是 log4j 的一个改良版本。此外 logback-classic 完整实现 SLF4J API 使你可以很方便地更换成其它日记系统如 log4j 或 jul 。logback-access 访问模块与 Servlet 容器集成提供通过Http来访问日记的功能。

Java 日志学习

最原始的日志

Java中最原始和最简单的方式自然就是通过 System.out 的方式,以下就是一个简单是事例

public static void mostOriSystemLogger(String message) {
  try {
    // 创建输出文件流
        FileOutputStream fos = new FileOutputStream("output.log", true); // true 表示追加模式
        PrintStream ps = new PrintStream(fos);

        // 重定向 System.out 到文件
        System.setOut(ps);
        System.setErr(ps);

        // 测试输出 日志输出到文件 output.log
        System.out.println(message);
        System.err.println(message);

  } catch (IOException e) {
      e.printStackTrace();
        System.err.println("This error message: " + e.getMessage());
    }
}

System.out 存在一些明显的限制

  • 无法灵活配置:无法轻松将日志输出到文件、网络或其他输出端,只能输出到控制台。
  • 级别:System.out.println() 没有日志级别的概念,无法区分不同重要程度的信息。
  • 日志格式:System.out.println() 输出格式固定,无法统一日志输出的格式和风格。
  • 性能问题:大量使用 System.out.println() 可能会导致性能问题,特别是在高并发的应用中,控制台 I/O 可能成为瓶颈。

也正是因为这些限制,由此诞生了 Log4j

Log4j

Log4解决了System.out的所有限制,还补充支持日志文件的滚动(按大小或时间自动切换日志文件),方便管理和归档大量的日志数据。

简单事例 1. 如果使用 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>

2. ****log4j.properties 配置文件

# 配置根Logger,日志级别为DEBUG,输出到名为console的appender
log4j.rootLogger=DEBUG, console

# 配置console这个appender,类型为ConsoleAppender,使用PatternLayout格式输出
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

3. Java代码

import org.apache.log4j.Logger;

public class Log4jUtils {
    // 获取Logger实例
    private static Logger logger = Logger.getLogger(Log4jUtils.class);

    public static void main(String[] args) {
        logger.debug("This is a debug message");
        logger.info("This is an info message");
        logger.warn("This is a warning message");
        logger.error("This is an error message");
        logger.fatal("This is a fatal message");
    }
}

Log4j 在高并发环境下性能较低,尤其是使用同步输出时,容易成为性能瓶颈和线程安全问题。

有了 Log4j 为什么还需要 JUL ?

很好理解,Log4j  毕竟私生子,不是 sum 正统出生。我 sum 要生当然得生一个亲生的。

JUL

JUL 最大的优点不需要依赖额外的库,JDK中就有了,方便快捷。提供默认配置等。但是 JUL 功能和扩展性远不如 Log4j 完善,自带的 Handlers 有限,性能和可用性上也一般,文档也少。JUL  是在Java1.5以后才有所提升的。简单事例

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

public class JULExample {
    // 创建Logger实例
    private static final Logger logger = Logger.getLogger(JULExample.class.getName());

    public static void main(String[] args) {
        // 设置日志级别
        logger.setLevel(Level.ALL);

        // 记录不同级别的日志
        logger.severe("This is a SEVERE message");
        logger.warning("This is a WARNING message");
        logger.info("This is an INFO message");
        logger.config("This is a CONFIG message");
        logger.fine("This is a FINE message");
        logger.finer("This is a FINER message");
        logger.finest("This is a FINEST message");

        // 使用 try-catch 记录异常信息
        try {
            int result = 10 / 0// 会抛出异常
        } catch (ArithmeticException ex) {
            logger.log(Level.SEVERE, "Exception caught", ex);
        }
    }
}

如果使用 SLF4J + JDKLog 需要依赖配置

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

为什么又出现了 JCL?Log4j  和 JUL 其实造成了日志界的混乱,他们两个是完全不同的接口实现,当 JUL 出现后,市面上就存在了两套日志框架,假设我 A 包用的 Log4j 日志,B 包用的 jul 日志,我自己工程引用了 A,B 两个包,那我就必须在项目中配置两套日志,各种输出格式,级别配置等等,是不是想想又很扯淡了,于是JCL诞生【其实就是想统一标准接口】

JCL

Java Common Logging (JCL) 是在 Java 社区中为了解决日志抽象层问题而出现的。在 JCL 出现之前,不同的 Java 项目和框架可能使用不同的日志框架(如 JUL、Log4j 等)。这导致在集成多个库时,如果它们使用不同的日志框架,可能会产生冲突或需要重复配置多个日志框架。JCL 提供了一个统一的日志接口,允许开发者在代码中使用同一个 API,而不必关心底层实际使用的日志框架。这使得项目在不同的环境中可以灵活地切换日志实现,例如在开发时使用 Log4j,在生产环境中使用 JUL。简单事例

  1. 首先,如果你使用 Maven 进行依赖管理,你需要在 pom.xml 中添加 JCL 的依赖。
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

2. Java代码

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JCLExample {
    // 创建一个日志记录器
    private static final Log log = LogFactory.getLog(JCLExample.class);

    public static void main(String[] args) {
        log.debug("This is a debug message");
        log.info("This is an info message");
        log.warn("This is a warning message");
        log.error("This is an error message");

        try {
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            log.fatal("This is a fatal error message", e);
        }
    }
}

JCL 会在 ClassLoader 中进行查找,如果能找到 Log4j 则默认使用Log4j 实现,如果没有则使用 JUL 实现,再没有则使用 JCL 内部提供的 SimpleLog 实现。

JCL 缺点也很明显

  • 性能问题:JCL 的自动探测机制会在应用启动时进行绑定检测,这在某些情况下会导致性能问题。

  • 复杂性:JCL 增加了一层抽象,这有时会使问题的排查变得更加复杂。

  • 线程安全问题:早期版本的 JCL 在多线程环境中存在一些问题,可很大的可能会引发内存泄露。同时,JCL 的书写存在一个不太优雅的地方

// 假如要输出一条debug日志,而一般情况下,生产环境 log 级别都会设到 info 或者以上,那这条 log 是不会被输出的。于是,在代码里就出现了

logger.debug("this is a debug info , message :" + msg);

// 这个有什么问题呢?虽然生产不会打出日志,但是这其中都会做一个字符串连接操作,然后生成一个新的字符串。如果这条语句在循环或者被调用很多次的函数中,就会多做很多无用的字符串连接,影响性能。
// 所以,JCL 推荐的写法如下

if (logger.isDebugEnabled()) {    
 logger.debug("this is a debug info , message :" + msg);
}

// 虽然解决了问题,但是这个代码实在看上去不怎么舒服...

为什么又出现了 SLF4J

其实看到这里,大家基本都可以猜到 SLF4J  出现的原因了吧,直白点就是 JCL 不行,那我 SLF4J  来吧。

SLF4J + Logback

SLF4J (Simple Logging Facade for Java) 和 JCL 一样都是定义了接口。SLF4J 是一个日志抽象层或日志门面,它提供了一组统一的日志 API。SLF4J 允许你在应用程序中使用统一的日志接口,而具体的日志实现可以通过绑定来选择。SLF4J 本身并不负责实际的日志记录操作,它只定义了日志记录的接口。Logback 是一个具体的日志实现框架,它实现了 SLF4J 的 API。Logback 提供了实际的日志记录功能,并且是 SLF4J 的一个推荐实现。Logback 由 Log4j 的作者开发,旨在提供更好的性能和更丰富的功能。简单事例下面分别给出使用 SLF4J 和 Logback 的代码示例。注意,Logback 是 SLF4J 的一种具体实现,通常在项目中同时使用 SLF4J API 和 Logback。

  1. 引入依赖包
<dependency>
  <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>2.0.9</version>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <version>1.4.11</version>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-core</artifactId>
   <version>1.4.11</version>
</dependency>

2. Logback 配置文件(logback.xml)

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

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

3. Java代码

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

public class LogbackExample {
    private static final Logger logger = LoggerFactory.getLogger(LogbackExample.class);

    public static void main(String[] args) {
        logger.debug("This is a debug message");
        logger.info("This is an info message");
        logger.warn("This is a warning message");
        logger.error("This is an error message");

        try {
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            logger.error("An exception occurred", e);
        }
    }
}

// slf4j 最大的特点就是打日志的时候使用了{}占位符,这样就不会有字符串拼接操作,减少了无用String对象的数量,节省了内存。
// 并且,记住,在生产最终日志信息的字符串之前,这个方法会检查一个特定的日志级别是不是打开了,这不仅降低了内存消耗而且预先降低了CPU去处理字符串连接命令的时间。
logger.info("This is a test message: {}", message);

Logback 作为 SLF4J 的推荐实现,旨在与 SLF4J 接口紧密集成,提供一致的日志记录体验。Logback 的特点

  • 异步日志:Logback 支持异步日志记录,利用异步日志可以显著提高日志记录的性能,尤其是在高并发环境中。
  • 高级配置:Logback 提供了灵活的配置选项,可以基于日志的级别、条件、时间等进行更精细的日志记录配置。
  • 多种日志输出:Logback 支持多种日志输出方式,包括控制台、文件、滚动文件、数据库等,且支持日志的滚动和归档。
  • 条件化日志:可以基于运行时的条件动态配置日志的输出和级别。
  • 性能优化:Logback 在性能方面做了许多优化,包括减少日志记录时的延迟和系统资源的使用。

至此其实 Logback + SLF4J 可以说是最完美的组合了,为什么又出现了 Log4j2 呢?

Log4j2

Log4j2 是 Log4j 框架的升级版本,旨在克服 Log4j 1.x 的一些限制,同时结合 Logback 和其他日志框架的优点,提供更高的性能、灵活性和可扩展性。他和 Logback + SLF4J 个有千秋,总体来说都不差。

简单事例

  1. 依赖引用
<dependencies>
    <!-- Log4j2 Core -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.20.0</version>
    </dependency>

    <!-- Log4j2 API -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.20.0</version>
    </dependency>
</dependencies>

2. 配置文件 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>
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2Example {
    private static final Logger logger = LogManager.getLogger(Log4j2Example.class);

    public static void main(String[] args) {
        logger.debug("This is a debug message");
        logger.info("This is an info message");
        logger.warn("This is a warning message");
        logger.error("This is an error message");
        logger.fatal("This is a fatal message");
    }
}

Log4j2 的特点

  • 高性能:Log4j 2 支持异步日志记录,显著提高了日志性能,特别是在高并发环境中。

  • 灵活性:提供丰富的插件和扩展机制,支持动态配置和多种日志格式。

  • 线程安全:比 Log4j 1.x 更好地处理了线程安全问题。

  • 复杂应用的支持:适合需要高可用性和复杂日志需求的企业应用。

总结

特性SLF4JLogbackJULJCLLog4jLog4j2
优点统一日志接口,兼容多种日志框架,支持绑定不同的日志实现SLF4J 的原生实现,高性能,丰富的功能,支持异步日志JDK 自带,易于使用,无需外部依赖提供统一的日志接口,方便切换底层日志框架经典框架,广泛使用,配置简单支持异步日志,灵活扩展,适合复杂企业应用
缺点仅提供接口,需与其他日志框架结合使用功能较复杂,配置需要一定学习成本功能简单,性能一般,配置灵活性差动态查找日志实现性能低,JAR 冲突问题性能相对较低,线程安全性问题配置和学习复杂
使用场景适合需要灵活选择或切换底层日志实现的应用适合需要高性能和丰富日志功能的中大型应用适合简单应用和对日志需求不高的系统适合希望灵活切换底层日志框架的场景适合传统应用和简单日志需求适合高并发环境和复杂的企业应用

致谢

更多内容欢迎点击阅读原文,喜欢文章的话,也希望能给小编点个赞或者转发,你们的喜欢与支持是小编最大的鼓励,小巫编程室感谢您的关注与支持。好好学习,天天向上(good good study day day up)。

  • OPENBOOT[1]
  • log4j官方配置文件说明[2]
  • log4j1 源码地址[3]
  • log4j2 源码地址[4]
  • slf4j 官网[5]
  • logback 官网[6]
  • csdn[7]
  • csdn[8]

参考资料

[1] OPENBOOT: gitee.com/openboot

[2] log4j官方配置文件说明: logging.apache.org/log4j/2.x/m…

[3] log4j1 源码地址: github.com/apache/logg…

[4] log4j2 源码地址: github.com/apache/logg…

[5] slf4j 官网: www.slf4j.org/

[6] logback 官网: logback.qos.ch/

[7] csdn: blog.csdn.net/qq_28165595…

[8] csdn: blog.csdn.net/imjcoder/ar…