日志框架体系及部分框架实现

一直有一个疑惑,日志为什么会有那么多的框架,到今天为止,心里终于有个比较明确的答案了。在这里我把我所理解的日志体系整理描述一下,自己总结一下,也希望对大家学习能有所帮助。

日志框架的分类

个人理解,日志框架可分为两类:

  • 门面型日志框架:
  1. jakartaCommonsLoggingImpl(jcl),
  2. Simple Logging Facade for Java (SLF4J)
  • 记录型日志框架:
  1. Log4j–Log4JLogger
  2. jul–Jdk14Logger
  3. Log4j 2
  4. logback

还有一些其他的框架,有些是比较老了如Jdk13LumberjackLogger,有些用的较少,这里不做介绍。记录型日志框架不做具体介绍,这里详细说明门面型日志框架.

jcl

jcl=Jakarta commons-logging ,是apache公司开发的一个抽象日志通用框架,本身不实现日志记录,但是提供了记录日志的抽象方法即接口(info,debug,error…),底层通过一个数组存放具体的日志框架的类名,然后循环数组依次去匹配这些类名是否在app中被依赖了,如果找到被依赖的则直接使用,所以他有先后顺序。我们通过代码看一下:

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

public class JCL {
    public static void main(String[] args) {
        Log log = LogFactory.getLog("jcl");
        log.info("jcl");
    }
}

上面是一个例子,可以看到我们可以通过jcl提供的一个工厂LogFactory获取到一个日志对象,那么这个日志对象是怎么获取到的呢,门面型究竟是什么意思?接下来我们对源码进行分析,先将源码贴出来:
//org.apache.commons.logging.LogFactory 669行

/**
     * Convenience method to return a named logger, without the application
     * having to care about factories.
     *
     * @param name Logical name of the <code>Log</code> instance to be
     *  returned (the meaning of this name is only known to the underlying
     *  logging implementation that is being wrapped)
     * @throws LogConfigurationException if a suitable <code>Log</code>
     *  instance cannot be returned
     */
    public static Log getLog(String name) throws LogConfigurationException {
        return getFactory().getInstance(name);
    }

通过方法获取一个工厂
//org.apache.commons.logging.LogFactory 417行

public static LogFactory getFactory() throws LogConfigurationException {
    ...部分代码省略
    return factory;
}

获取到工厂之后,通过工厂获取一个日志对象
//org.apache.commons.logging.impl.LogFactoryImpl 289行

/**
 * <p>Construct (if necessary) and return a <code>Log</code> instance,
 * using the factory's current set of configuration attributes.</p>
 *
 * <p><strong>NOTE</strong> - Depending upon the implementation of
 * the <code>LogFactory</code> you are using, the <code>Log</code>
 * instance you are returned may or may not be local to the current
 * application, and may or may not be returned again on a subsequent
 * call with the same name argument.</p>
 *
 * @param name Logical name of the <code>Log</code> instance to be
 *  returned (the meaning of this name is only known to the underlying
 *  logging implementation that is being wrapped)
 *
 * @exception LogConfigurationException if a suitable <code>Log</code>
 *  instance cannot be returned
 */
public Log getInstance(String name) throws LogConfigurationException {
    Log instance = (Log) instances.get(name);
    if (instance == null) {
        instance = newInstance(name);
        instances.put(name, instance);
    }
    return instance;
}

进入newInstance方法
//org.apache.commons.logging.impl.LogFactoryImpl 537行

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方法
//org.apache.commons.logging.impl.LogFactoryImpl 771行

private Log discoverLogImplementation(String logCategory)
    throws LogConfigurationException {
    ...部分代码省略

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

这里最重要的就是其中的for循环,有一个常量

/**
 * The names of classes that will be tried (in order) as logging
 * adapters. Each class is expected to implement the Log interface,
 * and to throw NoClassDefFound or ExceptionInInitializerError when
 * loaded if the underlying logging library is not available. Any
 * other error indicates that the underlying logging library is available
 * but broken/unusable for some reason.
 */
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"
};

由此可以看出,循环其中的常量调用createLogFromClass方法
//org.apache.commons.logging.impl.LogFactoryImpl 960行

private Log createLogFromClass(String logAdapterClassName,
                               String logCategory,
                               boolean affectState)
    throws LogConfigurationException {

    for(;;) {
        ...部分代码省略
        
            Class c;
            try {
                c = Class.forName(logAdapterClassName, true, currentCL);
            } catch (ClassNotFoundException originalClassNotFoundException) {
                String msg = originalClassNotFoundException.getMessage();
                logDiagnostic("The log adapter '" + logAdapterClassName + "' is not available via classloader " +
                              objectId(currentCL) + ": " + msg.trim());
                try {
                    c = Class.forName(logAdapterClassName);
                } catch (ClassNotFoundException secondaryClassNotFoundException) {
                    // no point continuing: this adapter isn't available
                    msg = secondaryClassNotFoundException.getMessage();
                    logDiagnostic("The log adapter '" + logAdapterClassName +
                                  "' is not available via the LogFactoryImpl class classloader: " + msg.trim());
                    break;
                }
            }

				...部分代码省略
        

    return logAdapter;
}

上面这个方法会对前面的那个常量的每一个类进行加载,如果加载到则返回,否则会继续进入下一个值进行加载。由此可以知道,jcl可以通过forName将按顺序将存在的log创建出来以供APP应用。
**jcl总结:**通过源码分析,我们知道jcl是固定的加载四种log,当然四种足够用了,因为其中有两个是JDK自带的,如Jdk14Logger是JDK自带的,所以肯定可以获取到。但是,jcl无法继续扩展了,对于新的日志框架如Log4j 2,logback等都无法满足了。
因此出现了一个新的门面型日志框架slf4j。

slf4j

不同与jcl的固定四种,slf4j有两个概念:

  • binding(绑定):通过绑定器可以绑定到具体的记录日志框架
  • bridging(桥接):通过桥接器可以将记录日志框架桥接到slf4j上

slf4j是通过通过绑定器绑定一个具体的日志记录来完成日志记录,如

  • slf4j-log4j12-1.8.0-beta4.jar可以绑定到log4j 2
  • slf4j-jdk14-1.8.0-beta4.jar可以绑定到jul
  • slf4j-jcl-1.8.0-beta4.jar可以绑定到jcl

绑定详细可以参考下图:

java 集成logstash 管理 java log 框架_mybatis

那么,桥接器的作用是为了将一个记录日志框架桥接到slf4j上,以便绑定到一个新的需要的记录日志框架,这个一帮是在项目整合或者使用新的日志框架会使用,如:

  • jcl可以通过jcl-over-slf4j.jar桥接到slf4j
  • jul可以通过jul-to-slf4j桥接到slf4j
  • log4j可以通过log4j-over-slf4j桥接到slf4j

桥接详细请参考下图

java 集成logstash 管理 java log 框架_spring_02

由此可知,通过绑定器以及桥接器,我们可以灵活的整合多种日志框架。

比如,有一个场景,旧的应用是用的log4j记录日志,现在要与一个新的应用整合在一起,而新的应用是通过slf4j绑定的jul记录的日志,如下图。

java 集成logstash 管理 java log 框架_java 集成logstash 管理_03

现在想将旧应用也修改成jul记录日志,那么就可以通过slf4j直接将log4j的桥接到slf4j,这样就可以绑定到jul记录日志,完美的解决问题。如下图:

java 集成logstash 管理 java log 框架_mybatis_04

spring4与spring5日志技术实现区别

  • spring4是依赖原生的jcl技术;
  • spring5使用的是spring-jcl(spring改了jcl的代码)来记录日志的

两者的区别:
spring4是原生的jcl,通过上面的介绍知道,原生jcl固定配置了四种日志框架,分别是:

  1. log4j
  2. jul-Jdk14Logger
  3. jul-Jdk13LumberjackLogger
  4. simple-log

而spring5使用的是spring通过改造jcl形成的spring-jcl,类似于原生jcl,它也是固定配置了几种日志框架,只是种类和顺序不同,看下源码:

//spring-5.1.x org.apache.commons.logging.LogAdapter 37行

final class LogAdapter {

	private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";
	private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";
	private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";
	private static final String SLF4J_API = "org.slf4j.Logger";
	private static final LogApi logApi;

	static {
		if (isPresent(LOG4J_SPI)) {
			if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
				// log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
				// however, we still prefer Log4j over the plain SLF4J API since
				// the latter does not have location awareness support.
				logApi = LogApi.SLF4J_LAL;
			}
			else {
				// Use Log4j 2.x directly, including location awareness support
				logApi = LogApi.LOG4J;
			}
		}
		else if (isPresent(SLF4J_SPI)) {
			// Full SLF4J SPI including location awareness support
			logApi = LogApi.SLF4J_LAL;
		}
		else if (isPresent(SLF4J_API)) {
			// Minimal SLF4J API without location awareness support
			logApi = LogApi.SLF4J;
		}
		else {
			// java.util.logging as default
			logApi = LogApi.JUL;
		}
	}
	...
}

通过代码可以看到,spring通过一段静态域按顺序加载对应的日志框架,最后默认的是jul,其顺序是:

  1. log4j 2
  2. slf4j
  3. jul(默认)

##mybatis日志技术实现
//org.apache.ibatis.logging.LogFactory 24行

public final class LogFactory {
  /**
   * Marker to be used by logging implementations that support markers
   */
  public static final String MARKER = "MYBATIS";
  private static Constructor<? extends Log> logConstructor;

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4J2Logging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useJdkLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useNoLogging();
      }
    });
  }
  ...
}

类似spring-jcl,mybatis也是通过一段静态域去加载,其主要方法源码如下:
//org.apache.ibatis.logging.LogFactory 120行

private static void tryImplementation(Runnable runnable) {
  if (logConstructor == null) {
    try {
      runnable.run();
    } catch (Throwable t) {
      // ignore
    }
  }
}

由源码可以看到,mybatis日志框架的加载顺序是:

  1. slf4j
  2. jcl
  3. log4j 2
  4. log4j
  5. jul
  6. nolog

mybatis提供很多日志的实现类,用来记录日志,取决于初始化的时候load到的class,如下图:

java 集成logstash 管理 java log 框架_mybatis_05


上图红色箭头可以看到JakartaCommonsLoggingImpl中引用了jcl 的类,如果在初始化的时候load到类为JakartaCommonsLoggingImpl那么则使用jcl 去实现日志记录,但是也是顺序的,顺序参考源码

这里留个问题,大家可以思考一下:

spring4+log4j 可以正常打印log4j日志,但是spring5+log4j 是无法打印log4j日志,通过以上的各个技术的是实现分析,大家能得出答案吗?