「Spring」2. Spring 日志 - 从 JUL 到 SLF4J

1,425 阅读3分钟

源码仓库:

主流日志技术

JUL

java.util.logging.Logger

这是 jdk 中自带的

代码

package com.example.springmaven.log;

import org.junit.jupiter.api.Test;

import java.util.logging.Logger;

public class TestJul {

    @Test
    public void jul() {
        Logger logger = Logger.getLogger("a");
        logger.info("aaa");
    }
}

默认效果如下

Log4j

代码

log4j 一代的依赖如下

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

提一嘴:二代的依赖如下

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

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

创建 log4j.properties 文件

log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=log4j %5p [%t] - %m%n

测试代码如下

package com.example.springmaven.log;

import org.apache.log4j.Logger;
import org.junit.jupiter.api.Test;

public class TestLog4j {

    @Test
    public void log4j() {
        Logger logger = Logger.getLogger("a");
        logger.info("11");
    }
}

缺点

这时候 JUL 和 Log4j 互不影响,但这也会产生一些问题,比如我整个项目的日志就不是统一的,有各种格式,为了解决这个问题,诞生了 commons-logging。

commons-longging

全程叫 jarkartCommins-logging ,简称 JCL。

代码

依赖:

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

测试代码如下

package com.example.springmaven.log;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;

public class TestJCL {
    @Test
    public void jcl() {
        Log log = LogFactory.getLog("a");
        log.info("jjjjjjj");
    }
}

为什么是log4j去打印的呢?

jcl 并不是一个具体的日志打印技术,他是一个抽象类,可以看下他的 getLog() 方法。

    public static Log getLog(String name) throws LogConfigurationException {
        return getFactory().getInstance(name);
    }
    public Log getInstance(String name) throws LogConfigurationException {
        Log instance = (Log) instances.get(name);
        if (instance == null) {
            instance = newInstance(name);
            instances.put(name, instance);
        }
        return instance;
    }

主要看下 newInstance(name)方法

 protected Log newInstance(String name) throws LogConfigurationException {
        Log instance;
        try {
            if (logConstructor == null) {
                instance = discoverLogImplementation(name);
            }
            else {
                Object params[] = { name };
                instance = (Log) logConstructor.newInstance(params);
            }

            if (logMethod != null) {
                Object params[] = { this };
                logMethod.invoke(instance, params);
            }

            return instance;

        } catch (LogConfigurationException lce) {

            // this type of exception means there was a problem in discovery
            // and we've already output diagnostics about the issue, etc.;
            // just pass it on
            throw lce;

        } catch (InvocationTargetException e) {
            // A problem occurred invoking the Constructor or Method
            // previously discovered
            Throwable c = e.getTargetException();
            throw new LogConfigurationException(c == null ? e : c);
        } catch (Throwable t) {
            handleThrowable(t); // may re-throw t
            // A problem occurred invoking the Constructor or Method
            // previously discovered
            throw new LogConfigurationException(t);
        }
    }

主要看下 discoverLogImplementation(name) 方法里,里面有个 classesToDiscover 对象

    private static final String[] classesToDiscover = {
            LOGGING_IMPL_LOG4J_LOGGER,
            "org.apache.commons.logging.impl.Jdk14Logger",
            "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
            "org.apache.commons.logging.impl.SimpleLog"
    };

classesToDiscover 会依次遍历这四个类,调用 createLogFromClass方法

        for(int i=0; i<classesToDiscover.length && result == null; ++i) {
            result = createLogFromClass(classesToDiscover[i], logCategory, true);
        }

createLogFromClass 方法中会用 Class.forName(logAdapterClassName) 去加载,如果加载到了,会调用如下方法去实例化。

constructor = c.getConstructor(logConstructorSignature);
Object o = constructor.newInstance(params);

但这里实例化的时候其实还包了一层,log4j的类名 private static final String LOGGING_IMPL_LOG4J_LOGGER = "org.apache.commons.logging.impl.Log4JLogger"; 其实还是 commons.logging 包下面的,它实例化的时候是通过 getLogger() 方法去实例化 log4j 本体。

    public Logger getLogger() {
        Logger result = logger;
        if (result == null) {
            synchronized(this) {
                result = logger;
                if (result == null) {
                    logger = result = Logger.getLogger(name);
                }
            }
        }
        return result;
    }

所以这里实际是log4j打印的日志。

流程图

去掉 log4j

所以,如果把 log4j 的依赖去掉,就是 jul 去打印日志了

番外

Class.forName() 的时候能看它也打印了日志,这时候其实日志打印都还没准备好,点进去看下它是用什么打印的

点到最后发现用的是 PrintStream,所以我们经常看到的日志相关的错误都是直接红字显示的。

private static PrintStream diagnosticsStream = null;

缺点

  1. 还是存在硬编码。
  2. 之前假如有log4j打印的,还是不会变,必须 LogFactory.getLog("a") 这样写才能做到统一。

Slf4j

和 commons logging 不一样,slf4j 采用的是绑定器的思路,可以看下图

流程图

代码

依赖如下

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

测试代码如下

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

public class TestSlf4j {
    @Test
    public void testJCL() {
        Logger logger = LoggerFactory.getLogger("a");
        logger.info("slf4j");
    }
}

这时候执行会报错,可以看到错误就是没有绑定器。

这时候我们项目虽然有 slf4j 和 log4j的依赖,但我们并没有把他们绑定在一起,所以 slf4j 并不知道要用什么日志技术去打印。

从 slf4j 的官网可以看到有哪些官方的绑定器 www.slf4j.org/manual.html

所以我们加入 slf4j 和 log4j 的绑定器

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

再执行测试代码就正常了

这时候假如把 log4j 的绑定器换成 jul 的,log4j 本身的依赖还是不动

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

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

打印日志就变成 jul 打印了

缺点

绑定器还是没有解决之前代码用别的日志框架打印的日志。

但slf4j其实也有解决方式,就是 slf4j 的桥接器。

桥接器

现在依赖如下

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

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

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

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

logback-classic 包含了 logback 本身和绑定器。也就是说现在我们用 Slf4j 打印的应该是 logback 打印的,但如果是 log4j 打印还是 log4j。如下:

这时候,我们想让 log4j 打印出来的还是用 slf4j 输出,那就要用到桥接器。

依赖如下:

<!-- https://mvnrepository.com/artifact/org.slf4j/log4j-over-slf4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.32</version>
</dependency>

去掉一些冲突依赖,最终依赖如下

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

        <!-- https://mvnrepository.com/artifact/org.slf4j/log4j-over-slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.32</version>
        </dependency>

可以看到如下打印,虽然是 log4j 实例化的对象打印的,但还是用 logback 打印的。