Blog

Make it work, make it right, make it fast

Feb 13, 2022 - 2 minute read - Java 编程

主流Java日志框架分析

背景

  • Log4j and JUL

在早期日志框架还没有出现以前,当时的程序员调试、记录日志只能使用System.out标准输出,该方式缺点很明显,无法进行细粒度的控制,并且无法定制化输出。在这种环境下,Apache基金会发布了Log4j,一经推出广为欢迎,甚至成为了事实上的标准。后来Apache建议将其纳入Jdk中,然而被sun公司拒绝。虽然sun没有采纳Log4j,但是在JDK1.4版本中也引入了JUL框架,该框架很大程度上参考了Log4j。前文详细介绍了JDK1.4引入的JUL日志框架,虽然是JDK内置的框架,但是该日志框架却一直不是很受欢迎。

  • JCL

为了解耦日志的实现,Apache推出了JCL(Jakarta Commons Logging)框架。JCL只包含一套接口即日志标准,没有具体的实现,这样在使用的时候可以直接引用JCL接口,无需关心具体的实现类,并在更换日志实现类的时候无需修改已有的代码。JCL支持以下日志实现(优先级按照顺序排列):Log4j、JUL、simpleLog。

  • Slf4j and Logback

由于Ceki Gülcü和Apache的分歧,并且觉得JCL接口设计的不好,容易让开发者写出有性能问题的代码,于是开发了Slf4j。Slf4j作为一套标准接口,可以实现无缝与多种实现框架进行对接。它也是现在比较常用的日志集成方式。之后Ceki Gülcü又顺带着开发完成了Logback作为Slf4j的默认实现。Slf4j和Logback已经成为了目前主流的日志门面与框架。

  • Log4j2

2012年,Apache重写了Log4j,实现了Log4j2,不仅具有Logback所有功能特性,并且在运行效率上也大大提高(PS:前不久Log4j2出现了远程代码执行漏洞)。

Slf4j Logback组合介绍

首先在pom.xml中引入slf4j-api。

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

试着直接使用slf4j看看效果:

package com.example;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogBackTest {
    
    @Test
    public void Slf4jTest() {
        Logger logger = LoggerFactory.getLogger(this.getClass());
        logger.trace("it's a trace level log");
        logger.debug("it's a debug level log");
        logger.info("it's a info level log");
        logger.warn("it's a warn level log");
        logger.error("it's a error level log");
    }
}

slf4j输出

可以看到,在没有引入实现类,直接使用slf4j的情况下,控制台会提示错误。接下来我们引入Logback,再次运行测试类,可以看到控制台正常输出了日志信息。

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

引入logback后日志输出

在maven中引入Logback包后,我们并没有对源代码进行改动,并且如果日后有需要可以直接将Logback切换成其他日志实现也是一样的,这就是日志门面技术。

这里Logback引入和能够直接使用,是因为Logback是根据slf4j实现的。在slf4j出现之前的日志框架如果要与slf4j集成使用,则需要引入相应的桥接包(如下图所示)。

桥接包示意图

Logback配置

可以看到logback会默认寻找配置文件,在没有找到的情况下采用了默认的配置。

logback.xml配置

在resources下新建logback.xml后,logback会自动加载该配置。

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

Logback配置示例

接着运行Slf4jTest方法,可以看到以下输出,输出格式和我们设置的一致,并且输出级别被限制在了debug。

配置日志输出到文件

logback配置如下:

<configuration>
    <property name="PATTERN" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
    <property name="USER_HOME" value="/Users/wag"/>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>${USER_HOME}/myApp.log</file>
        <encoder>
            <pattern>${PATTERN}</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="FILE"/>
    </root>
</configuration>

这里使用了property标签,主要的作用是定义变量,方便上下文引用。打开用户目录可以看到日志文件已经生成。

日志文件生成示例

通常情况下,使用最多的就是按照日期、文件大小对日志进行拆分。logback也提供了该实现,我们可以使用RollingFileAppender进行具体配置。

<configuration>
    <property name="PATTERN" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
    <property name="USER_HOME" value="/Users/wag" />

    <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${USER_HOME}/myApp.log</file>

        <encoder>
            <pattern>${PATTERN}</pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${USER_HOME}/myApp.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
            <maxFileSize>10KB</maxFileSize>
            <maxHistory>60</maxHistory>
            <totalSizeCap>20GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <root level="trace">
        <appender-ref ref="ROLLING" />
    </root>
</configuration>

接下来修改代码,增加日志输出,然后查看拆分后的文件。

package com.example;

import java.util.stream.IntStream;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;

public class LogBackTest {

    @Test
    public void Slf4jTest() {
        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        StatusPrinter.print(lc);
        Logger logger = LoggerFactory.getLogger(this.getClass());
        IntStream.range(0, 100000).forEach(item -> {
            logger.trace("it's a trace level log");
            logger.debug("it's a debug level log");
            logger.info("it's a info level log");
            logger.warn("it's a warn level log");
            logger.error("it's a error level log");
        });
    }
}

日志拆分示例

可以看到,日志按照我们想要的方式被拆分了,并且在日期变化时,会重新以新日期命名。在文件大小达到指定大小时,会根据序号拆分生成新的文件。

异步日志

在记录日志的过程中,会阻塞正常程序的运行,这时则需要配置异步日志。logback异步日志的配置较为简单,只需要添加asyncAppender,然后将需要异步记录的Appender配置在asyncAppender中即可。

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

    <property name="PATTERN" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="STDOUT" />
    </appender>

    <root level="trace">
        <appender-ref ref="ASYNC" />
    </root>
</configuration>

接下来修改代码,将一个标准输出记录在日志输出后面,查看控制台。

package com.example;

import java.util.stream.IntStream;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;

public class LogBackTest {

    @Test
    public void Slf4jTest() {
        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        StatusPrinter.print(lc);
        Logger logger = LoggerFactory.getLogger(this.getClass());
        IntStream.range(0, 10).forEach(item -> {
            logger.trace("it's a trace level log");
            logger.debug("it's a debug level log");
            logger.info("it's a info level log");
            logger.warn("it's a warn level log");
            logger.error("it's a error level log");
        });
        System.out.println("it‘s normal output");
    }
}

异步日志示例

可以看到标准输出并没有在日志输出结束执行,证明我们配置的异步日志生效了。