likes
comments
collection
share

04.java日志之jcl门面

作者站长头像
站长
· 阅读数 3

本篇文章中涉及到的所有代码都已经上传到gitee中: gitee.com/sss123a/log…

JCL

JCL:全称 Jakarta Common Logging ,是 Apache 提供的一个通用日志 API。

它为 “所有的 Java 日志实现”提供了一个统一的接口,它自身也提供了一个日志的实现,但功能非常弱(SimpleLog)。所以,一般不会单独使用它。它允许开发者使用不同的具体日志实现工具,比如:Log4j、JUL。

JCL 有两个基本的抽象类:Log、LogFactory(负责创建 Log)

Hello world!

引入依赖

<dependency>  
    <groupId>commons-logging</groupId>  
    <artifactId>commons-logging</artifactId>  
    <version>1.2</version>  
</dependency>

commons-logging包结构如下: 04.java日志之jcl门面

开始编写Hello world!示例代码如下:

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());
    }  
}

打印结果如下:

三月 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对象的工厂

来看看它的静态代码块

static {  
    // 省略...
    diagnosticsStream = initDiagnostics();
    // 省略...
}

其实就干了一件事情,就是根据读取系统属性org.apache.commons.logging.diagnostics.dest值看看用户想把jcl内部日志打印在哪,可选有:System.out、System.err和文件。

继续深入getLog()方法内部:

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

分两步:

  1. getFactory() 获取LogFactory实现类
  2. getInstance(clazz) 获取Log实现类

getFactory()获取LogFactory实例主要流程分四步,只要LogFactory实例化成功就会立刻返回:

  1. 读取系统属性org.apache.commons.logging.LogFactory
  2. 利用SPI读取META-INF/services/org.apache.commons.logging.LogFactory文件内容
  3. 读取commons-logging.properties配置文件,获取key=org.apache.commons.logging.LogFactory对应的value值
  4. 默认为:org.apache.commons.logging.impl.LogFactoryImpl

LogFactory现在已经拿到了,因为我们没有做任何配置,所以使用默认LogFactoryImpl实现类。

Log

04.java日志之jcl门面 接口 Log 默认只有4个实现类:JDK14LoggerLog4jLoggerSimpleLogJdk13LumberjackLogger

通过 LogFactory 动态地获取 Log实现类

然后来看看getInstance(clazz) 方法是如何获取Log实现类的。这个方法里面调用链很深,抛开一些细枝末节的,大体流程如下:

Log instance = newInstance(name);

instance = discoverLogImplementation(name);

String specifiedLogClassName = findUserSpecifiedLogClassName();

先来看看String specifiedLogClassName = findUserSpecifiedLogClassName(); 这句代码是来干嘛的吧!

其实就是查看用户是否指定了要使用哪种日志,返回Log实现类class,主要分两步(1比2优先级高):

  1. 读取commons-logging.properties配置文件,获取key=org.apache.commons.logging.Log对应的value值
  2. 读取系统属性org.apache.commons.logging.Log

如果用户指定了使用何种日志实现,就优先使用用户配置的。否则继续往下走:

// 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()具体实现:

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依赖: 04.java日志之jcl门面

那么,当我们引入 log4j 依赖时,org.apache.commons.logging.impl.Log4JLogger 类就显得可用了,所以,它会一次加载成功,跳出循环数组

jcl + log4j

引入如下依赖:

<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!运行结果如下:

04.java日志之jcl门面 如果对log4j不清楚的可以参考我之前写的两篇文章:

JCL淘汰及解决办法

commons-logging 是一个日志门面(facade),它为各种日志实现(如 Log4j, SLF4J, JUL 等)提供一个统一的接口。随着时间的发展,日志实现和日志门面的概念已经变得非常普遍,并且出现了一些新的技术选择。

由于以下原因,可能需要淘汰 commons-logging

  1. JCL 只支持以上4种实现类,如果还想支持其它三方日志框架,就得修改 jcl 源码了
  2. 依赖性冲突:如果项目中还在使用其他日志框架,比如 Log4j 或 SLF4J,可能会出现冲突。
  3. 性能问题:commons-logging 可能会带来性能上的损失,因为它是一个日志门面,底层实现可能会通过反射等方式查找并使用真正的日志实现。
  4. 不再维护:commons-logging 自 2014 年以后没有再更新,可能不再建议在新项目中使用。
  5. 配置不便:commons-logging 需要配置,而且配置不当可能导致日志无法正常工作。

解决方法:

  1. 使用 SLF4J 作为日志门面,并选择一个具体的日志实现,如 Logback 或 Log4j2。
  2. 移除 commons-logging 依赖,替换为 SLF4J 的绑定。
  3. 修改代码中的日志调用,使用 SLF4J 的 API。
  4. 配置 SLF4J 使用指定的日志实现。