写于:2019-06-01 21:52:37

# 为什么需要日志

在学习中,我们通过写笔记,写博客来记录自己的学习收获、心得,在以后需要复习回顾的时候进行翻阅。在生活中,我们通过写日记的方式,记录自己的生活点滴,在未来的某天翻看日记,追忆逝去的青春。在工作中,我们通过写周报记录每周工作进度,该工作进度作为绩效的评判标准之一。

在程序开发中,我们通过记录日志来了解程序的运行状况。比如:收集请求链路调用信息,汇总形成链路调用信息,用于系统分析。比如:通过打印错误日志,定位系统bug。比如:通过打印系统执行过程日志,定位错误。比如:通过打印日志,进行系统调试等。

# 谈到日志工具,你可能会听到的那些名字

Java【基础篇】带你捋清楚Java混乱的日志体系_听说过的那些日志工具名称

# 针对这些日志工具进行分类

# 根据分类看看日志工具如何进行日志打印

首先我们分类中的日志工具如何进行日志打印操作

再来日志门面如何进行日志打印

# 简单的得出结论

在众多日志工具中,存在两类日志工具。

  • 日志打印工具 能够直接进行日志打印操作的工具类。
  • 日志门面 本身不具备日志打印功能。通过调用其他能够进行打印操作的工具类来实现日志打印。

类比:同样是开发一个软件(同样是打印日志记录),可以直接选择有资质的个人进行开发(可以选择受认可的日志工具进行日志打印),也可以选择有资质的工作室,然后由工作室指派个人进行开发(也可以选择受认可的日志门面工具,然后由这个日志门面指定日志工具进行日志打印)。

# JCL 和 SLF4j 日志门面,如何进行日志工具的选择

# JCL

JCL 简单打印日志Demo

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

/**
 * <p> JCL 日志打印测试 </p>
 * @author WTF名字好难取
 */
public class JCLDemo {
    public static void main(String[] args) {
        // pom 依赖中只有 jcl 门面工具类其他的什么都没有
        // 结论:默认使用的 JUL 进行日志打印
        // for循环中的第二个选项
        Log jclLog = LogFactory.getLog("jcl_JUL");
        jclLog.info("jcl_log_JUL");

        // pom 依赖中 除了 jcl 门面工具类,增加 log4j 的依赖
        // 结论:当依赖中有 log4j 的工具类时,使用log4j 进行日志打印
	    // for循环中的第一个选项
        Log log4jLog = LogFactory.getLog("jcl_log4j");
        log4jLog.info("jcl_log_log4j");
    }
}

源码跟踪:LogFactory.getLog("jcl_JUL") 方法作为入口。

追踪链路:LogFactory#getLog("") ->LogFactory#getInstance(String var1) -> LogFactoryImpl#getInstance(String name) -> LogFactoryImpl#newInstance(String name) -> LogFactoryImpl#discoverLogImplementation(String logCategory)

在 LogFactoryImpl#discoverLogImplementation(String logCategory) 中我们可以看到如下关键代码:

public class LogFactoryImpl extends LogFactory {public class LogFactoryImpl extends LogFactory {
    private static final String[] classesToDiscover = new String[]{
	"org.apache.commons.logging.impl.Log4JLogger", 
	"org.apache.commons.logging.impl.Jdk14Logger", 
	"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
 	"org.apache.commons.logging.impl.SimpleLog"};
    
    /** 根据硬编码日志工具类的全限定名,循环尝试实例化日志工具类 **/
	 private Log discoverLogImplementation(String logCategory) throws LogConfigurationException {
     	Log result = null;
         for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
         	result = this.createLogFromClass(classesToDiscover[i], logCategory, true);
         }
     }
    
    /** 通过 class.forName 根据类的全限定名 获取类 **/
    private Log createLogFromClass(String logAdapterClassName, String logCategory, boolean affectState){
		Log logAdapter = null;
		Constructor constructor = null;
         Class c;
         try {
         	c = Class.forName(logAdapterClassName, true, currentCL);
         } catch (ClassNotFoundException var15) {......}
		constructor = c.getConstructor(this.logConstructorSignature);
         Object o = constructor.newInstance(params);
        if (o instanceof Log) {
         	logAdapter = (Log)o;
            break;
         }
    }
}

通过源码我们可以看到,JCL 日志门面是通过硬编码的方式,将日志工具的全限定名硬编码 在代码中,使用for循环,通过反射尝试实例化日志工具对象,依次来进行日志工具的实例化选择。

# SLF4J

下面是一张来自 SLF4j 官方的图片,和上面针对 SLF4j日志门面的画图 差不多。

根据上图我们可以把日志打印调用分为三层。

假设:我们系统此时选择SLF4J作为日志面门,通过 log4J2日志功能进行日志打印。这时候在pom 文件依赖中我们需要引入 3种jar 包。分别是:

  • SLF4J日志门面 jar 包:slf4j-api.jar
  • log4J2 binding :log4j-slf4j-impl.jar(该 jar 包中,已经依赖了 slf4j 和 log4j2 的依赖,开发中只需引入该包即可
  • log4J2 依赖:log4j-core ,log4j-api

项目中的依赖如下:(可以删除 slf4j 依赖 和 log4j2 依赖,只保留 binding 。因为 binding 引入了所有相关的 jar 包

下面是一个简单的 SLF4J 的demo

package qguofneg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p> slf4j 测试demo </p>
 * @author WTF名字好难取
 */
public class Slf4jDemo {

    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger("slf4j");
        logger.info("slf4j");
    }
}

源码跟踪:LoggerFactory.getLogger("slf4j");作为入口

跟踪链路:LoggerFactory#getLogger(String name) -> LoggerFactory#getILoggerFactory()

public final class LoggerFactory {
    // 初始值
    static volatile int INITIALIZATION_STATE = 0;
    public static ILoggerFactory getILoggerFactory() {
    	// INITIALIZATION_STATE 初始值为 0 所以必定进入 performInitialization() 方法中
		 if (INITIALIZATION_STATE == 0) {
             INITIALIZATION_STATE = 1;
             performInitialization();
         }
        // 调用完 performInitialization() 方法,根据不同的状况,设置不同的 INITIALIZATION_STATE 状态
        // 根据 不同的状态执行不同的操作,当 INITIALIZATION_STATE = 3 时,表示获取 日志对象成功。
        switch(INITIALIZATION_STATE) {
        case 1:
            return SUBST_FACTORY;
        case 2:
            throw new IllegalStateException("org.slf4j.LoggerFactory in failed state. Original exception was thrown EARLIER. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
        case 3:
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case 4:
            return NOP_FALLBACK_FACTORY;
        default:
            throw new IllegalStateException("Unreachable code");
        }
    }
}

getILoggerFactory()中 INITIALIZATION_STATE 初始值为 0 所以必定进入 performInitialization() 方法中。来看看 performInitialization() 方法源码

public final class LoggerFactory { 
	private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
	private static final void performInitialization() {
        // 寻找并binding 日志工具类
    	bind();
        // 如果找到对应的日志工具类 标识 INITIALIZATION_STATE 为3,检查日志工具类是否能够使用
        if (INITIALIZATION_STATE == 3) {
            versionSanityCheck();
        }
    }
}

在 performInitialization() 中我们需要关注的是 bind() 和 versionSanityCheck(); 方法。

首先来看看:bind() 方法。

public final class LoggerFactory { 
    private static final void bind() {
        String msg;
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            if (!isAndroid()) {
                // 获取所有 org/slf4j/impl/StaticLoggerBinder.class 的类,并放入 set 中
                // 可以联想到,当我们引入多个日志 binding ,会存在多个 StaticLoggerBinder.class
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
			   // 如果存在多个日志 binding 的时候,会打印相关的日志信息
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
		   // 日志对象进行实例化	
            // 根据JVM 类加载机制,在多个 StaticLoggerBinder.class 时,只有一个class 会被加载
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = 3;
            
        } catch (NoClassDefFoundError var2) {
            INITIALIZATION_STATE = 4;
        } catch (NoSuchMethodError var3) {
                INITIALIZATION_STATE = 2;
            throw var3;
        } catch (Exception var4) { ...... }
    }
}

在 bind () 方法中主要是用来加载 org/slf4j/impl/StaticLoggerBinder.class ,并进行实例化操作,实例化成功之后,设置 INITIALIZATION_STATE = 3 。

在 INITIALIZATION_STATE = 3 时,会调用,versionSanityCheck() 方法。该方法主要用来验证 binding 的版本号是否与 SLF4J 的版本号相对应。(猜测:不同的版本号可能有不同的方法调用,版本号不兼容可能会出现相关的报错异常。有兴趣的可以深入查看

最后回到 LoggerFactory#getILoggerFactory() 中,由于 INITIALIZATION_STATE = 3 ,这时候 switch () 会匹配并执行:StaticLoggerBinder.getSingleton().getLoggerFactory(); 方法,最后获取到对应的日志实现类。

扩展:根据上面的 LoggerFactory#bind() 方法我们能够知道:在使用 SLF4J 作为日志门面,同时引入多个日志 binding 工具,比如:sl4j2 和 logback 。此时:SLF4J能够正常进行日志打印,不过 使用哪种日志工具进行日志打印,取决于 JVM 先加载了那个 binding 中的 StaticLoggerBinder.class。同时 SLF4J 会给出相应的提示信息。

# 如何进行日志工具选择

无论使用哪种日志工具,我们最先应该考虑的是选择如 JCL 和 SLF4J 这样的日志门面。通过日志门面调用相关的具体日志工具类。这样便于后期进行扩展。比如:当前系统以 SLF4J 作为日志门面,使用的时 logback进行日志打印。如果此时我们需要 更换日志打印工具为 log4j2 ,此时我们只需要更改相对应的 binding 以及 log4j2 对应的日志依赖即可,代码无需做任何改变。

# 对比 JCL 和 SLF4J

我们先来对比看看 这两种日志门面在 Maven 仓库中的版本更新状况。

从图中可以看到 JCL 最新的一个版本是 2014 年的,而SLF4J 今年 2019 年还有在更新。

还有就是上面针对 JCL 和 SLF4J 的相关源码分析,JCL 采用的是硬编码的方式进行日志选择,并且可以说仅仅支持 JUL 和 sl4j 这两种日志工具。而 SLF4J 使用的是 binding 的概念。如:通过 log4j2 和 log4j2 对应的 binding 就能够进行日志打印。换句话说,如果此时市面上出现了一个新的好用的日志工具,官方只需要在开发一个相关的 binding ,我们就能够无缝的切换到相关的日志工具上去。

通过对比,高下立判。

精彩内容推送,请关注公众号!
最近更新时间: 3/24/2020, 9:44:42 PM