ABei A Java Engineer

细说 slf4j 与 log4j

2018-11-29
ABei
Log

Log4j

Log4j 是众多日志框架之一,在后台的开发当中我们通过日志来查找异常原因以及记录系统运行情况。Log4j 的配置也非常简单,但是你了解它内部的原理实现吗? 本文就来看看 Log4j 内部是如何运作的,以及是如何读取配置文件的。

常用的日志框架

  • Apache Log4j 是一个基于 Java 的日志记录工具。它是由 Ceki Gülcü 首创的,现在则是 Apache 软件基金会的一个项目。 Log4j 是几种 Java 日志框架之一。

  • Apache Log4j 2 是 apache 开发的一款 Log4j 的升级产品。

  • Commons Logging Apache 基金会所属的项目,是一套 Java 日志接口,之前叫 Jakarta Commons Logging,后更名为 Commons Logging。

  • SLF4J 类似于 Commons Logging,是一套简易 Java 日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写SLF4J)。

  • Logback 一套日志组件的实现(SLF4J阵营)。

  • Jul (Java Util Logging),自 Java1.4 以来的官方日志实现。

使用 Log4j

先来看看 Log4j 怎么使用的?

前提准备工作:

  1. 加载 Log4j 的 jar 包到 classpath,这里通过 maven 来加载。

    pom.xml 配置如下:

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  2. 配置 log4j.properties 或 log4j.xml 文件。这里就用最常用的 log4j.properties 文件来配置

    log4j.rootLogger=INFO, stdout
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.Target=System.out
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
    

测试代码:

import org.apache.log4j.Logger;
public class Log4jInAction {
    private static final Logger LOGGER = Logger.getLogger(Log4jInAction.class);

    public static void main(String[] args) {
        String message = "Hello Log4j";

        LOGGER.info("This is a test message : " + message);
    }
}

引入 Log4j 的 jar 包,编写 log4j.properties 文件以及在需要添加类日志的类中获取 Logger 实例对象,最后在需要日志的方法中编写相应的日志信息。通过这 4 个步骤就能将 Log4j 应用到你的系统中了。

那么,为什么我们只需要配置 log4j.properties 文件就可以使用 Log4j 了呢?

Log4j 运行原理

测试代码中,通过 Logger.getLogger(Log4jInAction.class) 获取了 Logger 实例对象。下面,我们来看看其内部实现。

static public Logger getLogger(Class clazz) {
    // 根据类的全限定名来生成一个 Logger 对象
    return LogManager.getLogger(clazz.getName());
}

org.apache.log4j.LogManager 这个类才是真正负责生成 Logger 对象的。

在看 LogManager.getLogger(String) 方法之前,先来看一下,在 JVM 加载 LogManager 的时候执行的静态块。

static {
    // 初始 Hierarchy 对象,级别为 DEBUG
    Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
    // 初始化 repositorySelector
    repositorySelector = new DefaultRepositorySelector(h);
    /* DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride"*/
    // 检查是否要自定义初始化 Logger 流程
    String override = OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY, null);
    if(override == null || "false".equalsIgnoreCase(override)) {
        // 查找自定义 log4j 的配置文件名
        String configurationOptionStr = OptionConverter.getSystemProperty(DEFAULT_CONFIGURATION_KEY, null);
        // 检查是否指定了自定义的配置类
        String configuratorClassName = OptionConverter.getSystemProperty(CONFIGURATOR_CLASS_KEY, null);
        URL url = null;
        if(configurationOptionStr == null) {// 如果都没有,那么将进行默认的初始化
            // 在 CLASSPATH 下搜索 log4j.xml 文件的路径	
            url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
            if(url == null) {
                // 在 CLASSPATH 下搜索 log4j.properties 文件的路径
                url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
            }
        } else {
            try {
                // 获取自己指定配置文件的路径
                url = new URL(configurationOptionStr);
            } catch (MalformedURLException ex) {
                url = Loader.getResource(configurationOptionStr);
            }
        }
      
        if(url != null) {
            LogLog.debug("Using URL ["+url+"] for automatic log4j configuration.");
            try {
                // 重点重点,下面会介绍
                OptionConverter.selectAndConfigure(url, configuratorClassName, 
                    LogManager.getLoggerRepository());
            } catch (NoClassDefFoundError e) {
                LogLog.warn("Error during default initialization", e);
            }
        } else {
            LogLog.debug("Could not find resource: ["+configurationOptionStr+"].");
        }
    } else {
        LogLog.debug("Default initialization of overridden by " + 
            DEFAULT_INIT_OVERRIDE_KEY + "property."); 
    }  
} 

在上面的静态块中,LogManager 在 CLASSPATH 下面检查一些 key 来确定用户是否要自定义 Log4j 的初始化流程。如果没有自定义,就启用默认的配置文件 —— log4j.xmllog4j.properties。 除此之外,还初始化了 Hierarchy 对象。Hierarchy 是 LogManager 中默认对 LoggerRepository 的实现。 Hierarchy.png

接着再确定好配置文件的路径之后,开始执行初始化配置的工作.

OptionConverter.selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy)

public static void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) {
    Configurator configurator = null;
    String filename = url.getFile();
    // log4j.xml
    if(clazz == null && filename != null && filename.endsWith(".xml")) {
        clazz = "org.apache.log4j.xml.DOMConfigurator";
    }
    // 自定义配置类
    // 需要注意的是,自定义的配置类必须要实现 Configurator 接口
    if(clazz != null) {
        LogLog.debug("Preferred configurator class: " + clazz);
        configurator = (Configurator) instantiateByClassName(clazz,
                                Configurator.class,
                                null);
        if(configurator == null) {
            LogLog.error("Could not instantiate configurator ["+clazz+"].");
            return;
        }
    } else {
        // log4.properties
        configurator = new PropertyConfigurator();
    }
    // 在确定使用哪种配置类之后,开始配置工作
    configurator.doConfigure(url, hierarchy);
}

doConfigure(URL url, LoggerRepository repository)

本文使用 log4j.properties 来做配置文件,下面就来看 PropertyConfigurator 中的 doConfigure 方法。

public void doConfigure(java.net.URL configURL, LoggerRepository hierarchy) {
    Properties props = new Properties();
    LogLog.debug("Reading configuration from URL " + configURL);
    InputStream istream = null;
    URLConnection uConn = null;
    try {
      uConn = configURL.openConnection();
      uConn.setUseCaches(false);
      istream = uConn.getInputStream();
      props.load(istream);
    }
    catch (Exception e) {
      ... ...
      // 避免篇幅过长,省略一些代码
      ... ...
    }
    finally {
        ... ...
        // 关闭资源
        ... ...
    }
    // 将 log4j.properties 配置文件转换成 java.util.Properties 对象
    doConfigure(props, hierarchy);
}

doConfigure(Properties, LoggerRepository)

public void doConfigure(Properties properties, LoggerRepository hierarchy) {
    repository = hierarchy;
    /* LogLog.DEBUG_KEY = "log4j.debug"*/
    // 可以通过在 log4j.properties 中设置 log4j.debug 的值或者 log4j.configDebug 的值
    // 来控制 LogLog 的日志级别
    // LogLog 在我看来就是给 Log4j 自己用的内部日志
    String value = properties.getProperty(LogLog.DEBUG_KEY);
    if(value == null) {
        value = properties.getProperty("log4j.configDebug");
        if(value != null)
            LogLog.warn("[log4j.configDebug] is deprecated. Use [log4j.debug] instead.");
    }

    if(value != null) {
        LogLog.setInternalDebugging(OptionConverter.toBoolean(value, true));
    }

    //  if log4j.reset=true then reset hierarchy
    String reset = properties.getProperty(RESET_KEY);
    if (reset != null && OptionConverter.toBoolean(reset, false)) {
        hierarchy.resetConfiguration();
    }

    String thresholdStr = OptionConverter.findAndSubst(THRESHOLD_PREFIX, properties);
    if(thresholdStr != null) {
        hierarchy.setThreshold(OptionConverter.toLevel(thresholdStr,
                                (Level) Level.ALL));
        LogLog.debug("Hierarchy threshold set to ["+hierarchy.getThreshold()+"].");
    }

    // 这里就开始根据 properties 对 Logger 对象进行一些配置,有兴趣可以深入下面 3 个方法
    configureRootCategory(properties, hierarchy);
    configureLoggerFactory(properties);
    parseCatsAndRenderers(properties, hierarchy);

    LogLog.debug("Finished configuring.");
    // We don't want to hold references to appenders preventing their
    // garbage collection.
    registry.clear();
}

第一次加载 Logger 对象需要设置一些参数,我们上面基本已经都介绍到了。下面,回到主线上来。

LogManager.getLogger(String)

public static Logger getLogger(final String name) {
    // Delegate the actual manufacturing of the logger to the logger repository.
    return getLoggerRepository().getLogger(name);
}

这里 getLoggerRepository() 方法返回的就是静态块中初始化的 Hierarchy 对象。

Hierarchy.getLogger(String)

public Logger getLogger(String name) {
    return getLogger(name, defaultFactory);
}

public Logger getLogger(String name, LoggerFactory factory) {
    CategoryKey key = new CategoryKey(name);
    // Synchronize to prevent write conflicts. Read conflicts (in
    // getChainedLevel method) are possible only if variable
    // assignments are non-atomic.
    Logger logger;

    synchronized(ht) {
        Object o = ht.get(key);
        if(o == null) {// 该类的全限定名还没有注册到 LoggerFactory 中
            // 生成新的 Logger 对象
            logger = factory.makeNewLoggerInstance(name);
            // 设置层级
            logger.setHierarchy(this);
            // 添加到 HashTable 中,你可以理解为注册表
            ht.put(key, logger);
            updateParents(logger);
            return logger;
        } else if(o instanceof Logger) {// 已经注册过
            return (Logger) o;
        } else if (o instanceof ProvisionNode) {
            //System.out.println("("+name+") ht.get(this) returned ProvisionNode");
            logger = factory.makeNewLoggerInstance(name);
            logger.setHierarchy(this);
            ht.put(key, logger);
            updateChildren((ProvisionNode) o, logger);
            updateParents(logger);
            return logger;
        } else {
            // It should be impossible to arrive here
            return null;  // but let's keep the compiler happy.
        }
    }
  }

至此,对 Log4j 是如何读取配置文件,Logger 对象是如何生成的有了一个大概的了解了。

SLF4J

前面在介绍几种日志框架的时候提到过 SLF4J。SLF4J 本身没有日志的功能,但是它能够绑定一种日志框架,用这个被绑定的日志框架来实现它的日志功能。

使用 SLF4J

这里我们用 Log4j 当做 SLF4J 的具体实现方案。

pom.xml

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>

log.properties 如上

测试代码:

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

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

    public static void main(String[] args ) {
        String message = "Hello SLF4J";
        LOGGER.info("This is a test message : {}", message);
    }
}

SLF4J 与 Log4j 使用上也是有一些不同,虽然实际上用的是 Log4j,但打印日志的方式也有一些不同。比如,SLF4J 打印信息可以使用占位符。

SLF4J 运行原理

我们就从 LoggerFactory.getLogger(Class) 作为入口来观察 SLF4J 内部是如何操作的。

public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName());
    if (DETECT_LOGGER_NAME_MISMATCH) {
        Class<?> autoComputedCallingClass = Util.getCallingClass();
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
            Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                            autoComputedCallingClass.getName()));
            Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
        }
    }
    return logger;
}

// getLogger(Class) 内部调用了 getLogger(String)
public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

从上面的代码可以看出,getLogger(Class) 最终通过 ILoggerFactory 来获取 Logger 实例对象。 通过 getILoggerFactory() 方法来获取 ILoggerFactory 对象。

public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {// 如果未进行初始化,就先初始化工作
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                // 执行初始化(有兴趣可以继续深入看)
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
        // 成功初始化之后,通过 StaticLoggerBinder 来获取 getILoggerFactory 对象
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        return SUBST_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}

来简单看一下 StaticLoggerBinder 内部构造:

public class StaticLoggerBinder implements LoggerFactoryBinder {

    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
    public static final StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }
    public static String REQUESTED_API_VERSION = "1.6.99"; // !final
    private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();

    private final ILoggerFactory loggerFactory;

    private StaticLoggerBinder() {
        loggerFactory = new Log4jLoggerFactory();
        try {
            @SuppressWarnings("unused")
            Level level = Level.TRACE;
        } catch (NoSuchFieldError nsfe) {
            Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
        }
    }

    public ILoggerFactory getLoggerFactory() {
        return loggerFactory;
    }

    public String getLoggerFactoryClassStr() {
        return loggerFactoryClassStr;
    }
}

原来 StaticLoggerBinder 中初始化的是 Log4jLoggerFactory。细枝末节的东西就不再深入,有兴趣可以自己看,结合上面对 Log4j 的实现原理应该很快就能理解。

为什么要选 SLF4J 而不是直接选用 Log4j

  1. 在你的开源或内部类库中使用 SLF4J 会使得它独立于任何一个特定的日志实现,这意味着不需要管理多个日志配置或者多个日志类库,你的客户端会很感激这点。
  2. SLF4J 提供了基于占位符的日志方法,这通过去除检查 isDebugEnabled(),isInfoEnabled()等等,提高了代码可读性。
  3. 通过使用 SLF4J 的日志方法,你可以延迟构建日志信息(Srting)的开销,直到你真正需要,这对于内存和 CPU 都是高效的。
  4. 作为附注,更少的暂时的字符串意味着垃圾回收器(Garbage Collector)需要做更好的工作,这意味着你的应用程序有为更好的吞吐量和性能。

参考


下一篇 Mysql 测试数据

Comments

Content