mybatis源码_Mybatis 源码分析之 Log

源码

如果是你设计一个框架,你的日志系统会怎么设计?是自己实现还是依赖日志门面 slf4j commons-logging jul 或者是直接依赖实现 log4j log4j2 logback stdout 或者是干脆不输出日志。其实还是挺令人头疼的一件事,接下来,我们来看看 Mybatis 这款优秀的 ORM 框架是如何做的。

5c72dae95f7f52670252a9f84d6c1514.png
Mybatis-Log-Uml

通过上图可以看到,Log 一共有 7 大实现类:

  1. StdOutImpl

  2. Log4jImpl

  3. NoLoggingImpl

  4. Jdk14LoggingImpl

  5. JakartaCommonsLoggingImpl

  6. Slf4jImpl

  • Slf4jLocationAwareLoggerImpl

  • Slf4jLoggerImpl

Log4j2Impl

  • Log4j2AbstractLoggerImpl

  • Log4j2LoggerImpl

接下来我们就逐个分析每一种实现

StdOutImpl

从名字来看,很容易能够看出这个是通过 JDK  原生的 API System.out.println 来实现的,接下来我们看看源码。

package org.apache.ibatis.logging.stdout;

import org.apache.ibatis.logging.Log;

/**
 * @author Clinton Begin
 */
public class StdOutImpl implements Log {

  public StdOutImpl(String clazz) {
    // Do Nothing
  }

  @Override
  public boolean isDebugEnabled() {
    return true;
  }

  @Override
  public boolean isTraceEnabled() {
    return true;
  }

  @Override
  public void error(String s, Throwable e) {
    System.err.println(s);
    e.printStackTrace(System.err);
  }

  @Override
  public void error(String s) {
    System.err.println(s);
  }

  //省略下面部分代码
}

这种实现方式较为简单,就是通过标准的输出流输出到指定的目标位置,默认情况下应该是输出到控制台,当然也可以输出到文件,相信很少有人用这种方式。

Log4jImpl

Log4jImpl 是 Apache 的一个开源项目,它可以通过配置文件灵活的配置日志的输出格式和目的地,也是比较主流的实现方式。同时分为 1.x 和 2.x 版本,但是从 2015年8月5日开始,官方建议使用 2.x  传送门:https://logging.apache.org/log4j/1.2/

On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life. For complete text of the announcement please see the Apache Blog. Users of Log4j 1 are recommended to upgrade to Apache Log4j 2

我们先看 1.x 的实现。

package org.apache.ibatis.logging.log4j;

import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
 * @author Eduardo Macarron
 */
public class Log4jImpl implements Log {

  private static final String FQCN = Log4jImpl.class.getName();

  private final Logger log;

  public Log4jImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.log(FQCN, Level.ERROR, s, e);
  }

  //省略下面部分代码
}

由于 Mybatis 本身依赖了 Log4j 所以可以直接使用其 API Logger.getLogger  初始化 Logger 对象。

NoLoggingImpl

顾名思义,这种实现方式就是不使用任何日志输出。

package org.apache.ibatis.logging.nologging;

import org.apache.ibatis.logging.Log;

/**
 * @author Clinton Begin
 */
public class NoLoggingImpl implements Log {

  public NoLoggingImpl(String clazz) {
    // Do Nothing
  }

  @Override
  public boolean isDebugEnabled() {
    return false;
  }

  @Override
  public boolean isTraceEnabled() {
    return false;
  }

  @Override
  public void error(String s, Throwable e) {
    // Do Nothing
  }
  //省略下面部分代码
}

可以看到,方法里面都是空实现。没有任何日志输出操作。

Jdk14LoggingImpl

这种实现方式也是使用了 JDK 原生的 API 还有另外一种叫法 JUL 它使用了 java.util.logging 包下面的 Logger 对象输出日志。

package org.apache.ibatis.logging.jdk14;

import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.ibatis.logging.Log;

/**
 * @author Clinton Begin
 */
public class Jdk14LoggingImpl implements Log {

  private final Logger log;

  public Jdk14LoggingImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isLoggable(Level.FINE);
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isLoggable(Level.FINER);
  }

  @Override
  public void error(String s, Throwable e) {
    log.log(Level.SEVERE, s, e);
  }

  @Override
  public void error(String s) {
    log.log(Level.SEVERE, s);
  }
  //省略下面部分代码

}

可以看到 Jdk14LoggingImpl 只是对 JUL 做了一层封装,使用 JUL 的最大好处估计也就是因为他是 JDK 内置的 API 我们不用引入任何第三方依赖库,就能直接使用。

虽然它也提供了配置文件的方式来配置日志,但是目前应该只有很少一部人使用它吧,大多数人还是愿意使用 Log4j Slf4j 等日志,究其历史原因,JUL 是 jdk1.4 中引入的记录日志的。

但是在此之前就有记录日志的 API 当然这可能只是一部分原因,还有就是配置方式可能没有其他日志那么灵活。传送门:https://stackoverflow.com/questions/11359187/why-not-use-java-util-logging 有讨论 JUl 和其他日志的对比,其中有 Log4j Slf4j Logback 项目创始人的回答。

JakartaCommonsLoggingImpl

这种实现方式是对 commons-logging 的封装,commons-logging 也叫 JCL 它是一种日志的门面,所谓门面就是它提供了统一的接口,底层适配了各个日志接口的实现,然后根据规则调用各个日志实现,返回 Log 对象。

package org.apache.ibatis.logging.commons;

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

/**
 * @author Clinton Begin
 */
public class JakartaCommonsLoggingImpl implements org.apache.ibatis.logging.Log {

  private final Log log;

  public JakartaCommonsLoggingImpl(String clazz) {
    log = LogFactory.getLog(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.error(s, e);
  }

  //省略下面部分代码

}

commons-logging 的适配规则是:

  1. org.apache.commons.logging.impl.Log4JLogger(Log4j)

  2. org.apache.commons.logging.impl.Jdk14Logger(Jul,Jdk1.4之后)

  3. org.apache.commons.logging.impl.Jdk13LumberjackLogger(Jul,Jdk1.4之前)

  4. org.apache.commons.logging.impl.SimpleLog(System.err.println)

我们可以看到虽然 commons-logging 是一种日志门面,但是当所有规则都不能匹配的情况下,它会自己实现一个简单的日志 SimpleLog 但是从匹配规则来看,如果项目依赖中没有 Log4j 那么应该匹配 Jul 这个肯定是存在的,毕竟它是在 JDK 自带的包中。

Slf4jImpl

Slf4j 也是一种日志门面,它提供了和很多日志关联的适配器。

4d4098a1ce9617ef72520851d507a576.png
concrete-bindings

通过这幅图我们可以看到,它可以和很多第三方依赖配合完成日志的输出。

package org.apache.ibatis.logging.slf4j;

import org.apache.ibatis.logging.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.spi.LocationAwareLogger;

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class Slf4jImpl implements Log {

  private Log log;

  public Slf4jImpl(String clazz) {
    Logger logger = LoggerFactory.getLogger(clazz);

    if (logger instanceof LocationAwareLogger) {
      try {
        // check for slf4j >= 1.6 method signature
        logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
        log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
        return;
      } catch (SecurityException e) {
        // fail-back to Slf4jLoggerImpl
      } catch (NoSuchMethodException e) {
        // fail-back to Slf4jLoggerImpl
      }
    }

    // Logger is not LocationAwareLogger or slf4j version 
    log = new Slf4jLoggerImpl(logger);
  }//省略下面部分代码
}

Slf4jImpl 的构造方法里面有一个判断条件,源码里面的注释说的也清楚,也就是说如果得到的 Logger 对象是

LocationAwareLogger 接口的实例,则说明当前 Slf4j 的版本是大于等于 1.6 的,下面的代码会通过反射检查是否包含指定的方法,检查通过则实例化 Slf4jLocationAwareLoggerImpl 对象

package org.apache.ibatis.logging.slf4j;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.slf4j.spi.LocationAwareLogger;

/**
 * @author Eduardo Macarron
 */
class Slf4jLocationAwareLoggerImpl implements Log {

  private static final Marker MARKER = MarkerFactory.getMarker(LogFactory.MARKER);

  private static final String FQCN = Slf4jImpl.class.getName();

  private final LocationAwareLogger logger;

  Slf4jLocationAwareLoggerImpl(LocationAwareLogger logger) {
    this.logger = logger;
  }

  @Override
  public boolean isDebugEnabled() {
    return logger.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return logger.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    logger.log(MARKER, FQCN, LocationAwareLogger.ERROR_INT, s, null, e);
  }
  //省略下面部分代码
}

如果失败则说明当前 Slf4j 的版本小于 1.6 则实例化 Slf4jLoggerImpl 对象。

package org.apache.ibatis.logging.slf4j;

import org.apache.ibatis.logging.Log;
import org.slf4j.Logger;

/**
 * @author Eduardo Macarron
 */
class Slf4jLoggerImpl implements Log {

  private final Logger log;

  public Slf4jLoggerImpl(Logger logger) {
    log = logger;
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.error(s, e);
  }
  //省略下面部分代码
}

想验证也很简单,Slf4j 初始化时会查找 StaticLoggerBinder 类,如果发现多个,控制台就会报错,告诉你找到多个绑定。

package org.slf4j.impl;

import org.apache.log4j.Level;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.Util;
import org.slf4j.spi.LoggerFactoryBinder;

public class StaticLoggerBinder implements LoggerFactoryBinder {

    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

    public static final StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }

    public static String REQUESTED_API_VERSION = "1.6.99"; // !final

    private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();

    private final ILoggerFactory loggerFactory;

    private StaticLoggerBinder() {
        loggerFactory = new Log4jLoggerFactory();
        try {
            @SuppressWarnings("unused")
            Level level = Level.TRACE;
        } catch (NoSuchFieldError nsfe) {
            Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
        }
    }
    //忽略下面部分代码
}

Class path contains multiple SLF4J bindings 这个报错大家可能都不陌生吧。如果你引入的是 slf4j-log4j 在 StaticLoggerBinder 的构造方法中会初始化 Log4jLoggerFactory 对象,该对象的 getLogger 方法中会创建 Logger 对象。

public Logger getLogger(String name) {
    Logger slf4jLogger = loggerMap.get(name);
    if (slf4jLogger != null) {
        return slf4jLogger;
    } else {
        org.apache.log4j.Logger log4jLogger;
        if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
            log4jLogger = LogManager.getRootLogger();
        else
            log4jLogger = LogManager.getLogger(name);

        Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
        Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
        return oldInstance == null ? newInstance : oldInstance;
    }
}

打开 Log4jLoggerAdapter 你会发现它实现了 LocationAwareLogger 接口。

package org.slf4j.impl;

import static org.slf4j.event.EventConstants.NA_SUBST;

import java.io.Serializable;

import org.apache.log4j.Level;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.ThrowableInformation;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.event.LoggingEvent;
import org.slf4j.helpers.FormattingTuple;
import org.slf4j.helpers.MarkerIgnoringBase;
import org.slf4j.helpers.MessageFormatter;
import org.slf4j.spi.LocationAwareLogger;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements LocationAwareLogger, Serializable {
    //省略代码
}
Log4j2Impl

前面我们说过官方建议使用 Log4j2 下面我们来看看 Mybatis 是怎么适配的。

package org.apache.ibatis.logging.log4j2;

import org.apache.ibatis.logging.Log;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.spi.AbstractLogger;

/**
 * @author Eduardo Macarron
 */
public class Log4j2Impl implements Log {

  private final Log log;

  public Log4j2Impl(String clazz) {
    Logger logger = LogManager.getLogger(clazz);

    if (logger instanceof AbstractLogger) {
      log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger);
    } else {
      log = new Log4j2LoggerImpl(logger);
    }
  }
  //省略下面部分代码
}

这个类和 Slf4j 一样构造方法里面都有条件判断,初始化不同的 Logger 实例。在 LogManager 的静态代码块里面会初始化 LoggerContextFactory 对象,如果你要使用 Log4j2 需要添加以下依赖。

<dependency>
    <groupId>org.apache.logging.log4jgroupId>
    <artifactId>log4j-apiartifactId>
    <version>xxxversion>
dependency>
<dependency>
    <groupId>org.apache.logging.log4jgroupId>
    <artifactId>log4j-coreartifactId>
    <version>xxxversion>
dependency>

那为什么要判断获取到的 Logger 对象呢,如果你只引入了 log4j-api 而没有引入 log4j-core 依赖,那还能记录日志吗?答案是当然可以,我们来看一下静态代码块。

e53d4d36a98934e984f2bb35558f797f.png
LogManager-static-1
d67b5ee82760538e9e70d95604e602c9.png
LogManager-static-2

由于静态代码块代码太长,粘贴代码的话可能看的不太清楚,所以这里用图片代替。

正如我代码里面的注释所描述,就算只引入了 log4j-api 也是可以打印日志的,只不过这个时候只能打印 error 级别的日志。如果使用默认的 SimpleLoggerContextFactory 的话接下来我们获取到的 LoggerContext 对象就是 SimpleLoggerContext 当调用 getLogger 的时候返回的就是 SimpleLogger

那么重点来了,在 Log4j2Impl 里面的判断是 logger instanceof AbstractLogger 那么去查看 SimpleLogger 类就会看到它其实是继承了 AbstractLogger 对象。

我们根据构造方法再来看一下

if (logger instanceof AbstractLogger) {
    log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger);
} else {
    log = new Log4j2LoggerImpl(logger);
}

创建 Log4j2AbstractLoggerImpl 对象。

package org.apache.ibatis.logging.log4j2;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.spi.AbstractLogger;
import org.apache.logging.log4j.spi.ExtendedLoggerWrapper;

/**
 * @author Eduardo Macarron
 */
public class Log4j2AbstractLoggerImpl implements Log {

  private static final Marker MARKER = MarkerManager.getMarker(LogFactory.MARKER);

  private static final String FQCN = Log4j2Impl.class.getName();

  private final ExtendedLoggerWrapper log;

  public Log4j2AbstractLoggerImpl(AbstractLogger abstractLogger) {
    log = new ExtendedLoggerWrapper(abstractLogger, abstractLogger.getName(), abstractLogger.getMessageFactory());
  }
  //省略下面部分代码
}

ExtendedLoggerWrapper 其实是对 AbstractLogger 又做了一次包装。底层还是调用 SimpleLogger 对象的方法。

那我们再来看一下,如果添加了 log4j-core 以后会有什么不同,前面代码里面说了,如果加了这个依赖,就会找到 log4j-provider.properties 这个文件,我们看下文件内容。

LoggerContextFactory = org.apache.logging.log4j.core.impl.Log4jContextFactory
Log4jAPIVersion = 2.1.0
FactoryPriority= 10

可以看到文件里面提供了 LoggerContextFactory 的实现,那么就会初始化 Log4jContextFactory 从而得到 LoggerContext 最后返回的是 Logger 对象。

我们根据构造方法再来看一下

if (logger instanceof AbstractLogger) {
    log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger);
} else {
    log = new Log4j2LoggerImpl(logger);
}

创建 Log4j2LoggerImpl 对象,进行包装。

package org.apache.ibatis.logging.log4j2;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;

/**
 * @author Eduardo Macarron
 */
public class Log4j2LoggerImpl implements Log {

  private static final Marker MARKER = MarkerManager.getMarker(LogFactory.MARKER);

  private final Logger log;

  public Log4j2LoggerImpl(Logger logger) {
    log = logger;
  }
  //省略下面部分代码
}

看到这里,Mybatis 对日志的封装适配就结束了。接下来我们来看看 Mybatis 是怎么让这些日志生效的。

分析

Mybatis 通过 LogFactory 获取 Log 对象,具体怎么使用,日志的优先级是什么都在这个类里面。

package org.apache.ibatis.logging;

import java.lang.reflect.Constructor;

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
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();
      }
    });
  }
  //省略下面部分代码
}

从静态代码块可以看出,日志的优先顺序。

  1. useSlf4jLogging

  2. useCommonsLogging

  3. useLog4J2Logging

  4. useLog4JLogging

  5. useJdkLogging

  6. useNoLogging

tryImplementation 方法里面做了什么呢,我们来看一看。

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

其实就是判断当前 logConstructor 属性是否为空,如果为空则执行线程的 run 方法,为什么是 run 而不是 start 呢,先卖个关子,我们稍后再说。

public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}

每个 userXXX 方法又调用了一个 setImplementation 方法,参数就是每个日志类的类对象。

private static void setImplementation(Class extends Log> implClass) {
    try {
        Constructor extends Log> candidate = implClass.getConstructor(String.class);
        Log log = candidate.newInstance(LogFactory.class.getName());
        if (log.isDebugEnabled()) {
            log.debug("Logging initialized using '" + implClass + "' adapter.");
        }
        logConstructor = candidate;
    } catch (Throwable t) {
        throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
}

setImplementation 方法就很简单了,就是根据参数类对象,获取指定的构造方法,然后创建实例,如果成功则赋值给全局属性 logConstructor 否则抛出异常。

回头看 tryImplementation 方法里面有捕捉异常的代码,就是处理这个地方抛出的异常,为什么要这样呢,很简单,Mybatis 依次加载日志对象,如果失败则顺序加载,如果成功,则赋值全局属性,那么下面的代码就不会执行了。

看到这里有的小伙伴可能会对 useXXX 方法和 tryImplementation 方法有疑问,一是 userXXX 方法加了 synchronized 线程同步关键字,但是从代码里面看好像没有线程同步的问题。二是 tryImplementation 方法的参数是 Runnable 它是一个接口,用来创建线程。但是代码里面却没有启动任何线程。

首先第一个问题,我们可以看到 useXXX 方法是用 public 关键字修饰的,也就意味着它可以在任何地方,被任何代码所调用,之所以加了线程同步关键字,应该也是为了更加严谨吧,毕竟写代码的人不知道这个代码究竟会被人怎么使用。

第二个问题,如果从我们自己的角度来编写的话,会怎么写这段代码,有顺序的进行判断,并忽略可能会发生的异常。如果之前没看过这个代码,我来写的话,可能就是直接调用多个 userXXX 方法,在每个方法里面处理异常。

那作者当时为什么这样写呢,菜鸡和大神的想法肯定是不一样的。所以我就去 Mybatis 的 Github 仓库里面问了这个问题,为什么要这样处理。大神给了回复。传送门:https://github.com/mybatis/mybatis-3/pull/2052

In some ways,tryImplementationparameter is an action function without parameters or results.The JDK only provides an action function without parameters or results, that is Runadble. In short,Just for convenience!

用我蹩脚的英文水平翻译了一下大致意思就是:tryImplementation 方法的参数是一个动作函数它不需要参数和结果,为了优雅的执行 userXXX 需要使用这样一个函数式的参数,而 JDK 没有提供别的方式,只有使用 Runnable 可以达到这样的效果,所以就这样用了,只是为了更方便而已。

那我在想如果是 Jdk1.8 其实我们就可以使用 lambada 来完成了。就不需要 Runnable 接口了。

到这里 Mybatis 对日志的处理和适配就分析完了,看了人家的源码才知道写代码的艺术。

一入源码深似海,对吗?


1a62464fcdef9569e64c1d987a531bf5.png

968bf7ca4e54ab748720bf38c2dca863.gif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值