Java 日志系统

日志体系

Java 日志框架有很多, 什么 JCL (Apache Commons Loggins), SLF4J (Simple Logging Facade for Java), Logback, JUL (Java Util Logging), Log4J, 十分让人摸不着头脑.

首先, JCL, SLF4J 这两个和其它的各种日志框架是要区别开来的. 他们都不能算作真正的日志框架, 就像 SLF4J 的名字里说的, 它们实现的是个 Facade 设计模式, 即门面设计模式. 当用户使用他们打印日志时, 他们内部会调用其它具体的日志框架实现 (比如 Logback, JUL) 打印日志.

为什么要这样呢? 假如你现在写的是一个 Java 的框架 (包括库), 你需要打印日志, 但是作为一个框架, 你肯定不能帮用户选择使用哪个日志框架, 所以这时候你就需要 JCL 或者 SLF4J 这种框架了, 他们让你和具体的日志框架解耦.

以后看到 SLF4J + Logback 这样的组合你就知道是什么意思了, 他们说的是使用 SLF4J 做日志门面, Logback 做具体的日志实现.

实现原理

那么 JCL 和 SLF4J 是怎么找到具体的日志框架实现的呢? 两者的实现方式有点区别, JCL 通过动态加载, SLF4J 通过 “静态绑定”.

JCL

动态加载就是使用 ClassLoader 动态加载类. 当我们使用 JCL 作为日志门面时, 我们在代码中是这样调用的:

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

class Foo {
    private static final Log LOG = LogFactory.getLog(Foo.class);

    public void bar() {
        if (LOG.isInfoEnabled()) {
            LOG.info("some message");
        }
    }
}

当我们调用 LogFactory.getLogger(...) 时, JCL 会先拿 LogFactory 接口的实例:

public static Log getLog(Class clazz) throws LogConfigurationException {
    return getFactory().getInstance(clazz);
}

getFactory() 方法会按照如下顺序取得实例:

  1. 看 Java System Property 是否设置了 org.apache.commons.logging.LogFactory (值应该为实现了 LogFactory 接口的类的 binary name) 如果设置了就实例化对应的类
  2. 使用 Java SPI 找对应的类 (JCL 是通过 ClassLoader.getResourceAsStream() 来读取 META-INF/services/org.apache.commons.logging.LogFactory 文件的内容再实例化)
  3. 看 JCL 的配置文件 (commons-logging.properties) 里是否设置了 org.apache.commons.logging.LogFactory 属性, 有就实例化对应的类
  4. 如果通过上面的方式都找不到的话, 就会实例化默认的实现类 org.apache.commons.logging.impl.LogFactoryImpl

通常使用的都是默认的 LogFactoryImpl. 找到 LogFactory 后, 就会去拿 Log 接口的实例, 默认的 LogFactoryImpl 会按照如下顺序去找实例 (关键的代码在 LogFactoryImpl.discoverLogImplementation() 里):

  1. 看 JCL 配置文件里有没有设置 org.apache.commons.logging.Log
  2. 看 Java System Property 里有没有 org.apache.commons.logging.Log
  3. 尝试加载 Log4J 的包装类 org.apache.commons.logging.impl.Log4JLogger (如果没有依赖 Log4J 那么加载会失败)
  4. 尝试加载 JUL 的包装类 org.apache.commons.logging.impl.Jdk14Logger 或者 org.apache.commons.logging.impl.Jdk13LumberjackLogger (根据 JDK 版本决定)
  5. 使用默认的 org.apache.commons.logging.impl.SimpleLog

可以看到 JCL 主要使用 ClassLoader 动态加载类, 所以容易遇到 ClassLoader 相关的问题, 尤其是 JCL 早期的版本 (Taxonomy of class loader problems encountered when using Jakarta Commons Logging).

SLF4J

SLF4J 就是因为早期的 JCL 有 ClassLoader 相关的问题才出现的. SLF4J 是通过 “静态绑定” 来找到相关的类的. 所谓的 “静态绑定” 就是说, SLF4J 会在编译的时候确定使用哪个日志框架, 不能理解? 看看 SLF4J 是怎么实现的就明白了, 下面简单分析下.

如果使用 SLF4J 做日志门面, 我们会这么使用:

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

class Foo {
    private static final Logger LOGGER = LoggerFactory.getLogger(Foo.class);

    public void bar() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("some {}", "message");
        }
    }
}

和 JCL 类似 LoggerFactory.getLogger(...) 里会拿 ILoggerFactory 接口的实例再拿 logger. 获取 ILoggerFactory 最关键的就是 org.slf4j.impl.StaticLoggerBinder 这个类. 不过神奇的是, SLF4J 的核心包 (slf4j-api) 里没有这个类! 那这个类在哪里呢? 如果使用 SLF4J, 根据你使用的具体日志框架实现不同, 你还需要添加相应的依赖, 比如: slf4j-log4j12-1.7.21.jar, slf4j-jdk14-1.7.21.jar 等等. 在这些包里会有 org.slf4j.impl.StaticLoggerBinder 类. 这就是所谓的 “静态绑定” 了.

Spring 日志

Spring 使用 JCL + Logback 作为默认日志系统. 如果没什么特殊要求的话, 正常使用 JCL 就行了.

如果希望使用 SLF4J 的话, 那么需要添加 slf4j-apijcl-over-slf4j 依赖. jcl-over-slf4j 通过 SPI (看上面 JCL LogFactory 查找的顺序) 让 JCL 使用 SLF4JLogFactor 作为 LogFactory 的实现. 最后, 我们在项目中, 按照正常方式使用 SLF4J 就行.

总结

知道了 Java 日志系统里有个 门面层, 那么其它就很好理解了. 首先我们确定日志门面, 然后决定使用哪个具体的日志框架, 使用哪个就去它的官网查具体的怎么配置 (用 Logback 就去查 Logback, 用 Log4J 就去查 Log4J).

目前项目中使用大多数人推荐的 SLF4J + Logback 组合.

SLF4J user manual

Apache Commons Logging - User Guide