在学习中,我们通过写笔记,写博客来记录自己的学习收获、心得,在以后需要复习回顾的时候进行翻阅。在生活中,我们通过写日记的方式,记录自己的生活点滴,在未来的某天翻看日记,追忆逝去的青春。在工作中,我们通过写周报记录每周工作进度,该工作进度作为绩效的评判标准之一。
在程序开发中,我们通过记录日志来了解程序的运行状况。比如:收集请求链路调用信息,汇总形成链路调用信息,用于系统分析。比如:通过打印错误日志,定位系统bug。比如:通过打印系统执行过程日志,定位错误。比如:通过打印日志,进行系统调试等。
首先我们分类中的日志工具如何进行日志打印操作
在众多日志工具中,存在两类日志工具。
类比:同样是开发一个软件(同样是打印日志记录),可以直接选择有资质的个人进行开发(可以选择受认可的日志工具进行日志打印),也可以选择有资质的工作室,然后由工作室指派个人进行开发(也可以选择受认可的日志门面工具,然后由这个日志门面指定日志工具进行日志打印)。
JCL 简单打印日志Demo
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* JCL 日志打印测试
* @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作为日志面门,通过 log4J2日志功能进行日志打印。这时候在pom 文件依赖中我们需要引入 3种jar 包。分别是:
项目中的依赖如下:(可以删除 slf4j 依赖 和 log4j2 依赖,只保留 binding 。因为 binding 引入了所有相关的 jar 包)
下面是一个简单的 SLF4J 的demo
package qguofneg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* slf4j 测试demo
* @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 对应的日志依赖即可,代码无需做任何改变。
我们先来对比看看 这两种日志门面在 Maven 仓库中的版本更新状况。
从图中可以看到 JCL 最新的一个版本是 2014 年的,而SLF4J 今年 2019 年还有在更新。
还有就是上面针对 JCL 和 SLF4J 的相关源码分析,JCL 采用的是硬编码的方式进行日志选择,并且可以说仅仅支持 JUL 和 sl4j 这两种日志工具。而 SLF4J 使用的是 binding 的概念。如:通过 log4j2 和 log4j2 对应的 binding 就能够进行日志打印。换句话说,如果此时市面上出现了一个新的好用的日志工具,官方只需要在开发一个相关的 binding ,我们就能够无缝的切换到相关的日志工具上去。
通过对比,高下立判。