本篇文章中涉及到的所有代码都已经上传到gitee中: gitee.com/sss123a/log...
JCL
JCL:全称 Jakarta Common Logging ,是 Apache 提供的一个通用日志 API。
它为 "所有的 Java 日志实现"提供了一个统一的接口,它自身也提供了一个日志的实现,但功能非常弱(SimpleLog)。所以,一般不会单独使用它。它允许开发者使用不同的具体日志实现工具,比如:Log4j、JUL。
JCL 有两个基本的抽象类:Log、LogFactory(负责创建 Log)
Hello world!
引入依赖
xml
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
commons-logging包结构如下:
开始编写Hello world!示例代码如下:
java
package com.matio.jcl.helloworld;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class HelloWorld {
public static void main(String[] args) {
// 将commons-logging内部日志都输出到console上
// System.setProperty(LogFactory.DIAGNOSTICS_DEST_PROPERTY, "STDOUT");
Log log = LogFactory.getLog(HelloWorld.class);
log.info("Hello commons-logging日志门面");
System.out.println(log.getClass().getName());
}
}
打印结果如下:
js
三月 19, 2024 7:46:12 下午 com.matio.jcl.helloworld.HelloWorld main
信息: Hello commons-logging日志门面
org.apache.commons.logging.impl.Jdk14Logger
jcl实现原理
具体看看这一行代码里面做了那些事情,分析它是根据什么依据来创建哪种log实例
Log log = LogFactory.getLog(HelloWorld.class);
LogFactory
负责创建log对象的工厂
来看看它的静态代码块
java
static {
// 省略...
diagnosticsStream = initDiagnostics();
// 省略...
}
其实就干了一件事情,就是根据读取系统属性org.apache.commons.logging.diagnostics.dest
值看看用户想把jcl内部日志打印在哪,可选有:System.out、System.err和文件。
继续深入getLog()方法内部:
java
public static Log getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}
分两步:
- getFactory() 获取LogFactory实现类
- getInstance(clazz) 获取Log实现类
getFactory()获取LogFactory实例主要流程分四步,只要LogFactory实例化成功就会立刻返回:
- 读取系统属性
org.apache.commons.logging.LogFactory
值 - 利用SPI读取
META-INF/services/org.apache.commons.logging.LogFactory
文件内容 - 读取
commons-logging.properties
配置文件,获取key=org.apache.commons.logging.LogFactory
对应的value值 - 默认为:
org.apache.commons.logging.impl.LogFactoryImpl
LogFactory现在已经拿到了,因为我们没有做任何配置,所以使用默认LogFactoryImpl实现类。
Log
接口 Log
默认只有4个实现类:JDK14Logger
、Log4jLogger
、SimpleLog
和Jdk13LumberjackLogger
通过 LogFactory
动态地获取 Log
实现类
然后来看看getInstance(clazz) 方法是如何获取Log实现类的。这个方法里面调用链很深,抛开一些细枝末节的,大体流程如下:
Log instance = newInstance(name);
instance = discoverLogImplementation(name);
String specifiedLogClassName = findUserSpecifiedLogClassName();
先来看看String specifiedLogClassName = findUserSpecifiedLogClassName();
这句代码是来干嘛的吧!
其实就是查看用户是否指定了要使用哪种日志,返回Log实现类class,主要分两步(1比2优先级高):
- 读取
commons-logging.properties
配置文件,获取key=org.apache.commons.logging.Log
对应的value值 - 读取系统属性
org.apache.commons.logging.Log
值
如果用户指定了使用何种日志实现,就优先使用用户配置的。否则继续往下走:
java
// jcl日志门面支持的日志实现数组
private static final String[] classesToDiscover = {
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
Log result = null;
for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
return result;
循环 jcl
中已经实现了的 4 个类,然后调用方法 createLogFromClass()
,如果返回result非空则直接返回,说明classesToDiscover
优先级是从高到低。然后我们来看看createLogFromClass()
具体实现:
java
private Log createLogFromClass(String logAdapterClassName,
String logCategory,
boolean affectState)
throws LogConfigurationException {
Object[] params = {logCategory};
Log logAdapter = null;
Constructor constructor = null;
Class logAdapterClass = null;
ClassLoader currentCL = getBaseClassLoader();
for (; ; ) {
try {
Class c;
try {
c = Class.forName(logAdapterClassName, true, currentCL);
} catch (ClassNotFoundException originalClassNotFoundException) {
try {
c = Class.forName(logAdapterClassName);
} catch (ClassNotFoundException secondaryClassNotFoundException) {
break;
}
}
constructor = c.getConstructor(logConstructorSignature);
Object o = constructor.newInstance(params);
if (o instanceof Log) {
logAdapterClass = c;
logAdapter = (Log) o;
break;
}
handleFlawedHierarchy(currentCL, c);
} catch (NoClassDefFoundError e) {
break;
} catch (ExceptionInInitializerError e) {
break;
} catch (LogConfigurationException e) {
throw e;
} catch (Throwable t) {
handleThrowable(t); // may re-throw t
handleFlawedDiscovery(logAdapterClassName, currentCL, t);
}
if (currentCL == null) {
break;
}
currentCL = getParentClassLoader(currentCL);
}
if (logAdapterClass != null && affectState) {
this.logClassName = logAdapterClassName;
this.logConstructor = constructor;
try {
this.logMethod = logAdapterClass.getMethod("setLogFactory", logMethodSignature);
} catch (Throwable t) {
handleThrowable(t); // may re-throw t
this.logMethod = null;
}
}
return logAdapter;
}
这个方法其实就甘霖一件事:通过反射 Class.forName(String className)
来加载 Log
的实现类(来自于 classesToDiscover
数组),如果此类存在,则通过反射创建其实例并返回;如果不存在,则直接返回。
那么,当我们没有引入 log4j 的依赖时,尝试去加载类 Log4JLogger(jcl 的实现类)。但是它并不会加载成功。因为 Log4JLogger 类中引入了 log4j 的依赖,依赖于 log4j。如:import org.apache.log4j.Logger; 所以,在调用 Class.forName(String className) 时会失败。然后继续循环,尝试加载第二个元素 Jdk14Logger,这个依赖就存在 jcl 依赖中,所以,它就会加载成功。
如果没有引用log4j依赖:
那么,当我们引入 log4j 依赖时,org.apache.commons.logging.impl.Log4JLogger 类就显得可用了,所以,它会一次加载成功,跳出循环数组
jcl + log4j
引入如下依赖:
xml
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
再次运行Hello world!运行结果如下:
如果对log4j不清楚的可以参考我之前写的两篇文章:
JCL淘汰及解决办法
commons-logging
是一个日志门面(facade),它为各种日志实现(如 Log4j, SLF4J, JUL 等)提供一个统一的接口。随着时间的发展,日志实现和日志门面的概念已经变得非常普遍,并且出现了一些新的技术选择。
由于以下原因,可能需要淘汰 commons-logging
:
- JCL 只支持以上4种实现类,如果还想支持其它三方日志框架,就得修改 jcl 源码了
- 依赖性冲突:如果项目中还在使用其他日志框架,比如 Log4j 或 SLF4J,可能会出现冲突。
- 性能问题:commons-logging 可能会带来性能上的损失,因为它是一个日志门面,底层实现可能会通过反射等方式查找并使用真正的日志实现。
- 不再维护:commons-logging 自 2014 年以后没有再更新,可能不再建议在新项目中使用。
- 配置不便:commons-logging 需要配置,而且配置不当可能导致日志无法正常工作。
解决方法:
- 使用 SLF4J 作为日志门面,并选择一个具体的日志实现,如 Logback 或 Log4j2。
- 移除 commons-logging 依赖,替换为 SLF4J 的绑定。
- 修改代码中的日志调用,使用 SLF4J 的 API。
- 配置 SLF4J 使用指定的日志实现。