使用 commons-logging 日志实现解耦

解耦需要

日志是实际应用中的一个重要部分,日志系统也有许多开源的实现,如 java.util.logging, logback, log4j系列等。

在使用日志系统时,如果与具体的日志实现耦合太深,如使用 log4j 作为日志的实现,在每一处需要打印日志的地方都会创建日志实例:

logger = LogManager.getLogger("instanceName");

当由于性能或者其他方面的需求需要更换日志实现时,如 log4j 升级到 log4j2,就不得不替换每一处日志实例得创建,将会是一个噩梦般的工作。

因此需要将应用与具体的日志实现解耦。commons-logging 提供了日志实现解耦方案,当然 commons-logging 也提供了简单的日志实现。

commons-logging 的官网地址为:http://commons.apache.org/proper/commons-logging/

其中对 commons-logging 的介绍:

The Logging package is an ultra-thin bridge between different logging implementations. A library that uses the commons-logging API can be used with any logging implementation at runtime. Commons-logging comes with support for a number of popular logging implementations, and writing adapters for others is a reasonably simple task.

commons-logging 简单日志实现

commons-logging 除了提供解耦合方案外,也提供了 org.apache.commons.logging.impl.SimpleLog 作为简单的日志实现。

使用 SimpleLog,需要做相应配置:

  1. 配置 commons-logging.properties

在 classpath 根路径放置名为 commons-logging.properties 的文件(该路径不可改变),内容为:

org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
  1. 创建 Log 实例

名为配置好了之后就可以创建 Log 实例并直接使用:

private static final Log logger = LogFactory.getLog(CommonsLogTest.class);

输出为,默认日志级别为 INFO。

  1. 输出配置

当然,SimpleLog 也可以做一些简单的输出配置,打开 SimpleLog 的源码,可以看到在初始化时:

static {
    // Add props from the resource simplelog.properties
    InputStream in = getResourceAsStream("simplelog.properties");
    if(null != in) {
        try {
            simpleLogProps.load(in);
            in.close();
        } catch(java.io.IOException e) {
            // ignored
        }
    }

    showLogName = getBooleanProperty(systemPrefix + "showlogname", showLogName);
    showShortName = getBooleanProperty(systemPrefix + "showShortLogname", showShortName);
    showDateTime = getBooleanProperty(systemPrefix + "showdatetime", showDateTime);

    if(showDateTime) {
        dateTimeFormat = getStringProperty(systemPrefix + "dateTimeFormat",
                                           dateTimeFormat);
        try {
            dateFormatter = new SimpleDateFormat(dateTimeFormat);
        } catch(IllegalArgumentException e) {
            // If the format pattern is invalid - use the default format
            dateTimeFormat = DEFAULT_DATE_TIME_FORMAT;
            dateFormatter = new SimpleDateFormat(dateTimeFormat);
        }
    }
}

可以看到最开始试图从 simplelog.properties 读取配置,若不存在后面指定了默认配置,因此只要在 classpath 根路径放置 simplelog.properties 文件即可控制日志输出行为。

可以从 http://commons.apache.org/proper/commons-logging/apidocs/org/apache/commons/logging/impl/SimpleLog.html 的关于 SimpleLog 的类介绍中了解配置详情:

Simple implementation of Log that sends all enabled log messages, for all defined loggers, to System.err. The following system properties are supported to configure the behavior of this logger:

  • org.apache.commons.logging.simplelog.defaultlog - Default logging detail level for all instances of SimpleLog. Must be one of ("trace", "debug", "info", "warn", "error", or "fatal"). If not specified, defaults to "info".
  • org.apache.commons.logging.simplelog.log.xxxxx - Logging detail level for a SimpleLog instance named "xxxxx". Must be one of ("trace", "debug", "info", "warn", "error", or "fatal"). If not specified, the default logging detail level is used.
  • org.apache.commons.logging.simplelog.showlogname - Set to true if you want the Log instance name to be included in output messages. Defaults to false.
  • org.apache.commons.logging.simplelog.showShortLogname - Set to true if you want the last component of the name to be included in output messages. Defaults to true.
  • org.apache.commons.logging.simplelog.showdatetime - Set to true if you want the current date and time to be included in output messages. Default is false.
  • org.apache.commons.logging.simplelog.dateTimeFormat - The date and time format to be used in the output messages. The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. If the format is not specified or is invalid, the default format is used. The default format is yyyy/MM/dd HH:mm:ss:SSS zzz.

commons-logging 解耦原理

commons-logging 最核心有用的功能是解耦,它的 SimpleLog 实现性能比不上其他实现,如 log4j 等。

首先,日志实例是通过 LogFactorygetLog(String) 方法创建的:

LogFatory 是一个抽象类,它负责加载具体的日志实现,分析其 Factory getFactory() 方法:

public static org.apache.commons.logging.LogFactory getFactory() throws LogConfigurationException {
    // Identify the class loader we will be using
    ClassLoader contextClassLoader = getContextClassLoaderInternal();

    if (contextClassLoader == null) {
        // This is an odd enough situation to report about. This
        // output will be a nuisance on JDK1.1, as the system
        // classloader is null in that environment.
        if (isDiagnosticsEnabled()) {
            logDiagnostic("Context classloader is null.");
        }
    }

    // Return any previously registered factory for this class loader
    org.apache.commons.logging.LogFactory factory = getCachedFactory(contextClassLoader);
    if (factory != null) {
        return factory;
    }

    if (isDiagnosticsEnabled()) {
        logDiagnostic(
                "[LOOKUP] LogFactory implementation requested for the first time for context classloader " +
                        objectId(contextClassLoader));
        logHierarchy("[LOOKUP] ", contextClassLoader);
    }

    // Load properties file.
    //
    // If the properties file exists, then its contents are used as
    // "attributes" on the LogFactory implementation class. One particular
    // property may also control which LogFactory concrete subclass is
    // used, but only if other discovery mechanisms fail..
    //
    // As the properties file (if it exists) will be used one way or
    // another in the end we may as well look for it first.
    // classpath根目录下寻找commons-logging.properties
    Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);

    // Determine whether we will be using the thread context class loader to
    // load logging classes or not by checking the loaded properties file (if any).
    // classpath根目录下commons-logging.properties是否配置use_tccl
    ClassLoader baseClassLoader = contextClassLoader;
    if (props != null) {
        String useTCCLStr = props.getProperty(TCCL_KEY);
        if (useTCCLStr != null) {
            // The Boolean.valueOf(useTCCLStr).booleanValue() formulation
            // is required for Java 1.2 compatibility.
            if (Boolean.valueOf(useTCCLStr).booleanValue() == false) {
                // Don't use current context classloader when locating any
                // LogFactory or Log classes, just use the class that loaded
                // this abstract class. When this class is deployed in a shared
                // classpath of a container, it means webapps cannot deploy their
                // own logging implementations. It also means that it is up to the
                // implementation whether to load library-specific config files
                // from the TCCL or not.
                baseClassLoader = thisClassLoader;
            }
        }
    }

    // 这里真正开始决定使用哪个factory
    // 首先,尝试查找vm系统属性org.apache.commons.logging.LogFactory,其是否指定factory
    // Determine which concrete LogFactory subclass to use.
    // First, try a global system property
    if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] Looking for system property [" + FACTORY_PROPERTY +
                "] to define the LogFactory subclass to use...");
    }

    try {
        String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
        if (factoryClass != null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass +
                        "' as specified by system property " + FACTORY_PROPERTY);
            }
            factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
        } else {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] No system property [" + FACTORY_PROPERTY + "] defined.");
            }
        }
    } catch (SecurityException e) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] A security exception occurred while trying to create an" +
                    " instance of the custom factory class" + ": [" + trim(e.getMessage()) +
                    "]. Trying alternative implementations...");
        }
        // ignore
    } catch (RuntimeException e) {
        // This is not consistent with the behaviour when a bad LogFactory class is
        // specified in a services file.
        //
        // One possible exception that can occur here is a ClassCastException when
        // the specified class wasn't castable to this LogFactory type.
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] An exception occurred while trying to create an" +
                    " instance of the custom factory class" + ": [" +
                    trim(e.getMessage()) +
                    "] as specified by a system property.");
        }
        throw e;
    }

    // 第二,尝试使用java spi服务发现机制,载META-INF/services下寻找org.apache.commons.logging.LogFactory实现
    // Second, try to find a service by using the JDK1.3 class
    // discovery mechanism, which involves putting a file with the name
    // of an interface class in the META-INF/services directory, where the
    // contents of the file is a single line specifying a concrete class
    // that implements the desired interface.

    if (factory == null) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
                    "] to define the LogFactory subclass to use...");
        }
        try {
            // META-INF/services/org.apache.commons.logging.LogFactory, SERVICE_ID
            final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);

            if (is != null) {
                // This code is needed by EBCDIC and other strange systems.
                // It's a fix for bugs reported in xerces
                BufferedReader rd;
                try {
                    rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                } catch (java.io.UnsupportedEncodingException e) {
                    rd = new BufferedReader(new InputStreamReader(is));
                }

                String factoryClassName = rd.readLine();
                rd.close();

                if (factoryClassName != null && !"".equals(factoryClassName)) {
                    if (isDiagnosticsEnabled()) {
                        logDiagnostic("[LOOKUP]  Creating an instance of LogFactory class " +
                                factoryClassName +
                                " as specified by file '" + SERVICE_ID +
                                "' which was present in the path of the context classloader.");
                    }
                    factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader);
                }
            } else {
                // is == null
                if (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] No resource file with name '" + SERVICE_ID + "' found.");
                }
            }
        } catch (Exception ex) {
            // note: if the specified LogFactory class wasn't compatible with LogFactory
            // for some reason, a ClassCastException will be caught here, and attempts will
            // continue to find a compatible class.
            if (isDiagnosticsEnabled()) {
                logDiagnostic(
                        "[LOOKUP] A security exception occurred while trying to create an" +
                                " instance of the custom factory class" +
                                ": [" + trim(ex.getMessage()) +
                                "]. Trying alternative implementations...");
            }
            // ignore
        }
    }

    // 第三,尝试从classpath根目录下的commons-logging.properties中查找org.apache.commons.logging.LogFactory属性指定的factory
    // Third try looking into the properties file read earlier (if found)

    if (factory == null) {
        if (props != null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic(
                        "[LOOKUP] Looking in properties file for entry with key '" + FACTORY_PROPERTY +
                                "' to define the LogFactory subclass to use...");
            }
            String factoryClass = props.getProperty(FACTORY_PROPERTY);
            if (factoryClass != null) {
                if (isDiagnosticsEnabled()) {
                    logDiagnostic(
                            "[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
                }
                factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);

                // TODO: think about whether we need to handle exceptions from newFactory
            } else {
                if (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
                }
            }
        } else {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] No properties file available to determine" + " LogFactory subclass from..");
            }
        }
    }

    // 最后,使用后备factory实现,org.apache.commons.logging.impl.LogFactoryImpl
    // Fourth, try the fallback implementation class

    if (factory == null) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic(
                    "[LOOKUP] Loading the default LogFactory implementation '" + FACTORY_DEFAULT +
                            "' via the same classloader that loaded this LogFactory" +
                            " class (ie not looking in the context classloader).");
        }

        // Note: unlike the above code which can try to load custom LogFactory
        // implementations via the TCCL, we don't try to load the default LogFactory
        // implementation via the context classloader because:
        // * that can cause problems (see comments in newFactory method)
        // * no-one should be customising the code of the default class
        // Yes, we do give up the ability for the child to ship a newer
        // version of the LogFactoryImpl class and have it used dynamically
        // by an old LogFactory class in the parent, but that isn't
        // necessarily a good idea anyway.
        factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
    }

    if (factory != null) {
        /**
         * Always cache using context class loader.
         */
        cacheFactory(contextClassLoader, factory);

        if (props != null) {
            Enumeration names = props.propertyNames();
            while (names.hasMoreElements()) {
                String name = (String) names.nextElement();
                String value = props.getProperty(name);
                factory.setAttribute(name, value);
            }
        }
    }

    return factory;
}

可以看出,抽象类 LogFactory 加载具体实现的步骤如下:

  1. 从 vm 系统属性 org.apache.commons.logging.LogFactory
  2. 使用 SPI 服务发现机制,发现 org.apache.commons.logging.LogFactory 的实现
  3. 查找 classpath 根目录 commons-logging.properties的org.apache.commons.logging.LogFactory 属性是否指定 factory 实现
  4. 使用默认 factory 实现,org.apache.commons.logging.impl.LogFactoryImpl

LogFactory 的 getLog() 方法返回类型是 org.apache.commons.logging.Log 接口,提供了从 trace 到 fatal 方法。可以确定,如果日志实现提供者只要实现该接口,并且使用继承自 org.apache.commons.logging.LogFactory 的子类创建 Log,必然可以构建一个松耦合的日志系统。

log4j+commons-logging 解耦

使用 commons-logging 解耦 log4j,可按照下列步骤:

  1. 将 log4j 和 commons-logging 依赖放入 classpath:
<!--commons-logging-->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<!-- log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
  1. 配置 log4j.xml 或者 log4j.properties,放入类路径根目录;

  2. 使用 commons-logging 得 LogFactory 获取日志实例

log4j 提供了 LogManager.getLogger(String) 方法创建日志实例,但是为了日志实现的解耦,必须使用 commons-logging 提供的抽象工厂 LogFactory 创建日志实例:

Log logger = LogFactory.getLog(String)

接下来就可以使用 logger 输出日志信息了。

log4j 如何被 commons-logging 加载?

根据上述 commons-logging 解耦的原理,如果 log4j 是通过继承抽象工厂 org.apache.commons.logging.LogFactory 来创建实现 org.apache.commons.logging.Log 接口的日志实例的,那么就可以通过 3 种方法让 commons-logging 加载 log4j,分别是:

  • 从 vm 系统属性 org.apache.commons.logging.LogFactory
  • 使用 SPI 服务发现机制,发现 org.apache.commons.logging.LogFactory 的实现
  • 查找 classpath 根目录 commons-logging.properties 的 org.apache.commons.logging.LogFactory 属性是否指定 factory 实现

但是,查看 log4j 的 jar 包,发现在 META-INF 下面并没有名为 services 得文件夹,更别说 org.apache.commons.logging.LogFactory 文件了。而且,Logger 类也没有实现 commons-logging 提供的 Log 接口。log4j 中也没有找到继承 org.apache.commons.logging.LogFactory 的类。

那么,log4j 是如何被 commons-logging 加载的呢?

按照上述 LogFactory 寻找 factory 实现的流程,LogFactory 在找不到 org.apache.commons.logging.LogFactory 实现时,会使用默认实现 org.apache.commons.logging.impl.LogFactoryImpl。

通过分析 LogFactoryImpl 的 getInstance() 方法,其调用以下方法获得 logger 实例:

protected Log newInstance(String name) throws LogConfigurationException {
    Log instance;
    try {
        if (logConstructor == null) {
            instance = discoverLogImplementation(name);
        }
        else {
            Object params[] = { name };
            instance = (Log) logConstructor.newInstance(params);
        }

        if (logMethod != null) {
            Object params[] = { this };
            logMethod.invoke(instance, params);
        }

        return instance;

    } catch (LogConfigurationException lce) {

        // this type of exception means there was a problem in discovery
        // and we've already output diagnostics about the issue, etc.;
        // just pass it on
        throw lce;

    } catch (InvocationTargetException e) {
        // A problem occurred invoking the Constructor or Method
        // previously discovered
        Throwable c = e.getTargetException();
        throw new LogConfigurationException(c == null ? e : c);
    } catch (Throwable t) {
        handleThrowable(t); // may re-throw t
        // A problem occurred invoking the Constructor or Method
        // previously discovered
        throw new LogConfigurationException(t);
    }
}

discoverLogImplementation 方法如下:

  • 该方法首先查找用户是否指定了日志实现
  • classesToDiscover 定义如下:
private static final String[] classesToDiscover = {
    LOGGING_IMPL_LOG4J_LOGGER, // org.apache.commons.logging.impl.Log4JLogger
    "org.apache.commons.logging.impl.Jdk14Logger",
    "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
    "org.apache.commons.logging.impl.SimpleLog"
};

用户没有指定日志实现的情况下,按照 classesToDiscover 数组元素的顺序,依次创建对应 Log 实例,直到返回成功创建的 Log 实例:

private Log discoverLogImplementation(String logCategory) throws LogConfigurationException {
    if (isDiagnosticsEnabled()) {
        logDiagnostic("Discovering a Log implementation...");
    }

    initConfiguration();

    Log result = null;

    // See if the user specified the Log implementation to use
    // 查找用户是否指定了日志实现?
    String specifiedLogClassName = findUserSpecifiedLogClassName();

    if (specifiedLogClassName != null) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("Attempting to load user-specified log class '" +
                specifiedLogClassName + "'...");
        }

        result = createLogFromClass(specifiedLogClassName,
                                    logCategory,
                                    true);
        if (result == null) {
            StringBuffer messageBuffer =  new StringBuffer("User-specified log class '");
            messageBuffer.append(specifiedLogClassName);
            messageBuffer.append("' cannot be found or is not useable.");

            // Mistyping or misspelling names is a common fault.
            // Construct a good error message, if we can
            informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LOG4J_LOGGER);
            informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_JDK14_LOGGER);
            informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LUMBERJACK_LOGGER);
            informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_SIMPLE_LOGGER);
            throw new LogConfigurationException(messageBuffer.toString());
        }

        return result;
    }

    // No user specified log; try to discover what's on the classpath
    //
    // Note that we deliberately loop here over classesToDiscover and
    // expect method createLogFromClass to loop over the possible source
    // classloaders. The effect is:
    //   for each discoverable log adapter
    //      for each possible classloader
    //          see if it works
    //
    // It appears reasonable at first glance to do the opposite:
    //   for each possible classloader
    //     for each discoverable log adapter
    //        see if it works
    //
    // The latter certainly has advantages for user-installable logging
    // libraries such as log4j; in a webapp for example this code should
    // first check whether the user has provided any of the possible
    // logging libraries before looking in the parent classloader.
    // Unfortunately, however, Jdk14Logger will always work in jvm>=1.4,
    // and SimpleLog will always work in any JVM. So the loop would never
    // ever look for logging libraries in the parent classpath. Yet many
    // users would expect that putting log4j there would cause it to be
    // detected (and this is the historical JCL behaviour). So we go with
    // the first approach. A user that has bundled a specific logging lib
    // in a webapp should use a commons-logging.properties file or a
    // service file in META-INF to force use of that logging lib anyway,
    // rather than relying on discovery.

    if (isDiagnosticsEnabled()) {
        logDiagnostic(
            "No user-specified Log implementation; performing discovery" +
            " using the standard supported logging implementations...");
    }
    // 用户没有指定日志实现的情况下,按照classesToDiscover数组元素的顺序,依次创建对应Log实例,直到返回成功创建的Log实例
    for(int i=0; i<classesToDiscover.length && result == null; ++i) {
        result = createLogFromClass(classesToDiscover[i], logCategory, true);
    }

    if (result == null) {
        throw new LogConfigurationException
                    ("No suitable Log implementation");
    }

    return result;
}

findUserSpecifiedLogClassName 方法如下:

  • 该方法首先试图获取 commons-logging.properties 中的 org.apache.commons.logging.Log 指定的日志实现
  • 试图获取 commons-logging.properties 中的 org.apache.commons.logging.log 指定的日志实现
  • 试图获取 vm 系统属性 org.apache.commons.logging.Log 指定的日志实现
  • 试图获取 vm 系统属性 org.apache.commons.logging.log 指定的日志实现
  • 都没有返回 null
private String findUserSpecifiedLogClassName() {
    if (isDiagnosticsEnabled()) {
        logDiagnostic("Trying to get log class from attribute '" + LOG_PROPERTY + "'");
    }
    // 试图获取commons-logging.properties 中的org.apache.commons.logging.Log指定的日志实现
    String specifiedClass = (String) getAttribute(LOG_PROPERTY);

    if (specifiedClass == null) { // @deprecated
        if (isDiagnosticsEnabled()) {
            logDiagnostic("Trying to get log class from attribute '" +
                          LOG_PROPERTY_OLD + "'");
        }
        // 试图获取commons-logging.properties 中的org.apache.commons.logging.log指定的日志实现
        specifiedClass = (String) getAttribute(LOG_PROPERTY_OLD);
    }

    if (specifiedClass == null) {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("Trying to get log class from system property '" +
                      LOG_PROPERTY + "'");
        }
        try {
            // 试图获取vm系统属性org.apache.commons.logging.Log指定的日志实现
            specifiedClass = getSystemProperty(LOG_PROPERTY, null);
        } catch (SecurityException e) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("No access allowed to system property '" +
                    LOG_PROPERTY + "' - " + e.getMessage());
            }
        }
    }

    if (specifiedClass == null) { // @deprecated
        if (isDiagnosticsEnabled()) {
            logDiagnostic("Trying to get log class from system property '" +
                      LOG_PROPERTY_OLD + "'");
        }
        try {
            // 试图获取vm系统属性org.apache.commons.logging.log指定的日志实现
            specifiedClass = getSystemProperty(LOG_PROPERTY_OLD, null);
        } catch (SecurityException e) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("No access allowed to system property '" +
                    LOG_PROPERTY_OLD + "' - " + e.getMessage());
            }
        }
    }

    // Remove any whitespace; it's never valid in a classname so its
    // presence just means a user mistake. As we know what they meant,
    // we may as well strip the spaces.
    if (specifiedClass != null) {
        specifiedClass = specifiedClass.trim();
    }

    return specifiedClass;
}

如此一来,如果像上述一样没有指定任何 org.apache.commons.logging.Log 的实现,那么 commons-logging 首先使用的是 org.apache.commons.logging.impl.Log4JLogger 作为 Log 实现,可以看到 Log4jLogger 是实现了 Log 接口的:

public class Log4JLogger implements Log, Serializable

Log4jLogger 则是通过组合 log4j 提供的 org.apache.log4j.Logger,来提供 trace 到 fatal 功能。

在 log4j 的 jar 包没有导入到 classpath 之前,这个类是无法通过编译的。

到现在应该知道了 commons-logging 是如何与 log4j 实现解耦的了,不是通过 commons-logging 提供的抽象工厂,而是通过 org.apache.commons.logging.impl.LogFactoryImpl 加载具体的日志实现。不得不说,在我看来这种设计极其不好,完全可以用抽象工厂提供的日志实现加载!

log4j2+commons-logging 解耦

log4j2 是 Apache 推出的 log4j 的升级版本,在性能等各方面都做了提升。

要想使用 commons-logging 解耦 log4j2,可按照下列步骤:

  1. 导入log4j2, commons-logging, log4j-jcl 依赖

其中,log4j-jcl 是用来作为 log4j 和 commons-logging 的桥接的。

<!--log4j2-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.7</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.7</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-jcl</artifactId>
    <version>2.7</version>
</dependency>
  1. 配置好 log4j2 依赖的配置

log4j2 支持了更多的配置方式,包括 xml 和 json 格式;若不指定配置文件位置,将其放置在 classpath 根路径。

  1. 创建日志实例使用

log4j2 也提供了自己的日志实例创建的方法:

logger = LogManager.getLogger(Log4jTwoTest.class);

为了实现解耦,需要使用 commons-logging 提供的抽象工厂 LogFactory.getLog(String) 方法获取日志实例即可;

解耦原理:log4j-jcl 提供了解耦的实现,观察其 jar 包:

该文件内容为:org.apache.logging.log4j.jcl.LogFactoryImpl

根据 commons-logging 抽象日志工厂的第二步骤 spi 服务发现规则,抽象工厂使用 org.apache.logging.log4j.jcl.LogFactoryImpl 作为实际产生日志实现的工厂。

分析 LogFactoryImpl 的代码:

private final LoggerAdapter<Log> adapter = new LogAdapter();
 
private final ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
 
@Override
public Log getInstance(final String name) throws LogConfigurationException {
    return adapter.getLogger(name);
}

Log 实例是由 LogAdapter 类的 getLogger(String) 方法创建的,其继承了抽象适配器 AbstractLoggerAdapter<Log> 的 getLogger 方法:

public L getLogger(String name) {
    LoggerContext context = this.getContext();
    ConcurrentMap loggers = this.getLoggersInContext(context);
    Object logger = loggers.get(name);
    if(logger != null) {
        return logger;
    } else {
        loggers.putIfAbsent(name, this.newLogger(name, context));
        return loggers.get(name);
    }
}

最终调用 LogAdapter 的 newLogger() 方法产生日志实例:

@Override
protected Log newLogger(final String name, final LoggerContext context) {
    return new Log4jLog(context.getLogger(name));
}

Log4jLog 组合了 log4j2 中的 ExtendedLogger 提供日志打印功能。

可以看出,log4j2 和 commons-logging 的解耦,完全使用了 commons-logging 提供的 spi 服务发现机制。


参考:

  1. SimpleLog 类介绍
  2. commons.logging 1.1.1 源代码研究(2)-- 基本使用及配置文件
  3. 使用 commons.logging 中的 SimpleLog 显示调试和日志信息
  4. Apache Commons Logging 是如何决定使用哪个日志实现类的
如果觉得这对你有用,请随意赞赏,给与作者支持
评论 0
最新评论