关于java:Mybatis的log机制

51次阅读

共计 10292 个字符,预计需要花费 26 分钟才能阅读完成。

Mybatis 的一个十分有用的附加性能就是能帮忙咱们打印执行过程中的 log:sql 语句、参数、执行后果等。这一个性在开发过程中十分有用,能够帮忙咱们疾速高效定位开发过程中的问题。

明天咱们从源码的角度钻研一下 Mybatis 的 log 机制,次要包含两局部:

  1. Mybatis 的日志框架
  2. 日志打印源码剖析,也就是找到 Mybatis 源码中打印日志的中央

日志框架 #Log 接口及其实现

目前 java 世界曾经有如下十分优良的日志框架:

  1. Slf4j
  2. CommonsLog
  3. Log4J2
  4. Log4J
  5. JdkLog

Mybatis 当然不会本人再实现一套日志框架,所谓 Mybatis 的日志框架,指的是 Mybatis 针对日志解决的外部实现,从而非常简单不便的反对上述各种支流日志框架。

首先,Mybatis 定义了一个 Log 接口:

public interface Log {boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

Log 接口定义了 4 种 log 级别,和下面提到的 Java 支流 log 框架的日志级别对应。

Log 接口有以下实现:

咱们能够看到,除了下面提到的针对 java 支流 log 框架的实现之外,Mybatis 还提供了其余的 Log 接口的实现,都非常简单,比方 StdOutImpl 就是间接 System.out.println 或者 System.err.println,NoLoggingImpl 就是没有 log 的意思。

java 支流日志框架的实现类其实也非常简单:创立相应 java 日志框架的 log 对象作为 Mybatis 日志对象的代理对象,Mybatis 须要记录日志的时候,委派给代理对象执行。

日志框架 #Log 对象初始化

既然 Mybatis 反对各种 java 支流日志框架,那他到底是怎么决定具体应用哪个日志框架呢?其实就是 Mybatis 的 log 对象的初始化过程。

这个工作是 LogFactory 实现的。

  private static Constructor<? extends Log> logConstructor;
    //...
  private LogFactory() {// disable construction}

logFactory 定义了一个动态 Constructor 变量 logConstructor,并且把构造方法定义为 private 来阻止通过构造方法初始化,对外通过 getLog 办法提供 Log 对象:

  public static Log getLog(String logger) {
    try {return logConstructor.newInstance(logger);
    } catch (Throwable t) {throw new LogException("Error creating logger for logger" + logger + ".  Cause:" + t, t);
    }
  }

getLog 办法也非常简单,间接通过结构器 logConstructor 调用 newInstance 办法获取 Log 对象。所以,LogFactory 的外围其实就应该是这个 logConstructor 的初始化过程了。

持续看源码:

  static {tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }



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

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

  public static synchronized void useCommonsLogging() {setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

LogFactory 定义了一个尝试创立日志实现对象的动态区,通过调用静态方法 tryImplementation 尝试创立日志对象,尝试的先后顺序就是咱们文章开篇列出的日志框架的程序,首先会尝试 Slf4j、而后是 CommonsLogging… 最初应用 NoLogging,不记录日志。

tryImplementation 办法接管 Runnable 参数,调用的时候通过 Lambda 形式创立,别离指定为 useSlf4jLogging()、useCommonsLogging()…… 等。通过调用 setImplementation、传递相应的日志实现类参数,来创立对应的日志实现类。

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 创立日志实现类的结构器,只有该日志框架 (比方 Slf4j)的包引入到我的项目、创立该日志框架的日志对象胜利的话,就肯定可能获取到该日志对象的结构器,则将该结构器赋值给 logConstructor。

这样,动态区 6 个 tryImplementation 办法的逻辑就分明了:依照调用程序,首先尝试初始化 logConstructor 为 Slf4j 的实现类,创立胜利的话,前面的调用就会被间接跳过、不会失效了。创立不胜利的话(比方 Slf4j 的包没有导入我的项目中)则尝试下一个 …… 以此类推,最初肯定会创立进去适合的 logConstructor。

除了动态区的 tryImplementation 办法的调用先后顺序来决定日志框架的优先级(到底是应用哪一个日志框架)之外,其实 Mybatis 还提供了另外两种手动设置日志框架的形式:

  1. 通过配置文件
  2. 代码中间接调用 LocFactory.useCustomLogging

配置文件:

    <settings>
        <setting name="cacheEnabled"             value="true" />  <!-- 全局映射器启用缓存 -->
        <setting name="useGeneratedKeys"         value="true" />  <!-- 容许 JDBC 反对主动生成主键 -->
        <setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
        <setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
        <!-- <setting name="mapUnderscoreToCamelCase" value="true"/>  驼峰式命名 -->
    </settings>

配置文件指定的日志实现类是和 typeAlias 配合失效的,Configuration 中实例化的时候曾经实现了注册:

typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

配置文件在 Mybatis 初始化的过程中被读入,通过 XMLConfigBuilder 调用 loadCustomLogImpl->Configuration.setLogImpl->LogFactory.useCustomLogging 实现日志实现类的初始化。useCustomLogging 办法代码咱们下面曾经看过了,间接调用 setImplementation(clazz); 办法无条件笼罩掉 logConstructor,从而实现配置文件的最高优先级。

所以,决定 Mybatis 日志实现类(最终到底应用哪一个日志框架)的优先程序由低到高是:

  1. LogFactory 动态区 tryImplementation 调用程序指定
  2. 通过配置文件指定
  3. 用户代码中通过调用 useCustomLogging 随时指定,立刻失效

Mybatis 的日志框架剖析实现, 小结一下:Mybatis 反对 Java 支流框架,包含 Slf4j/CommonLogging/log4J2/log4J/JDKLoging/NoLogging/ 或者用户自定义 Logging(应该没有人这么干吧)。用户能够通过配置文件或者在代码中指定具体应用哪一个日志框架,不指定的话 Mybatis 自行依照以上优先级一一尝试以后我的项目容许应用的日志框架(导入日志框架的包就能够应用)。

SQL 语句打印

容许 Mybatis 打印 SQL 语句的配置也比较简单:在日志配置文件中指定关上针对 mapper 接口类的 DEBUG 级别的日志。比方,咱们我的项目中配置 Mybatis 应用 Slf4j->logback 作为日志框架,则须要在 logback.xml 做如下配置:

<logger name="com.myproject.mapper" level="debug" />

其中 com.myproject.mapper 是我的项目的 mapper 接口文件所在的包名称。

或者,基于 Springboog 的我的项目,间接在 application.yml 文件中增加:

logging:
  level:
    com.myproject.mapper: debug

配置的是 mapper 接口类的日志,顺着这个线索来找找 Mybatis 管制日志的代码:从 Mapper 接口登程。

后面的文章曾经剖析过了 mapper.xml 文件中的 sql 语句,初始化过程中被加载到 Configuration 中的 mappedStatements 中,最终被 mapper 接口的动静代理类通过调用 MapperMethod 的 execute 办法执行。

那咱们就间接看 MapperMethod 的 execute 办法,咱们就以 INSERT 语句为代表:

 public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }

看 DefaultSqlSession 的 insert 办法,始终追下去,最初调用到 Executor 的 prepareStatement 办法:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

这里看到了十分相熟的获取数据库连贯的语句,调用了 getConnection(BaseExecutor 中)办法:

  protected Connection getConnection(Log statementLog) throws SQLException {Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {return connection;}
  }

看到这里会判断 statementLog.isDebugEnabled(),如果 log 关上了 debug level 的话,调用了 ConnectionLogger.newInstance 办法,去看一眼代码:

  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

又是十分相熟的 JDK 动静代理,阐明返回的理论是数据库连贯的动静代理对象。

假相慢慢浮出水面了,咱们当初就要去认真钻研一下这个 ConnectionLogger 对象了,因为咱们要追究 Mybatis 日志的假相,这个类的名字切实太可疑了。

BaseJdbcLogger

Mybatis 提供了一个叫 BaseJdbcLogger 的虚构类,专门用来提供数据库操作的日志记录能力,咱们先看一下类构造:

有 4 个实现类,别离对应 Connection、Statement、PrepareStatement 和 Resultset,4 个实现类都实现了 InvocationHandler 接口,充当了 JDK 动静代理的回调类,所有对被代理 Connection、Statement、PrepareStatement、ResultSet 对象的办法调用,都会回调到动静代理类的 invoke 办法上。

所以咱们间接看他们的 invoke 办法。

ConnectionLogger

间接看 invoke 办法:

@Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, params);
      }
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {if (isDebugEnabled()) {debug("Preparing:" + removeExtraWhitespace((String) params[0]), true);
        }
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {return method.invoke(connection, params);
      }
    } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
    }
  }

看到了十分相熟的、常常在日志中呈现的内容“Preparing:”,前面跟的就是要执行的 sql 语句。

而且,创立的 prepareStatement 之后,通过调用 PreparedStatementLogger.newInstance 创立代理对象,最终返回的是代理对象 PreparedStatementLogger。

顺便说一句,咱们当初我的项目中绝大多数状况应该都是应用 prepareStatement 了,Mybatis 反对在配置文件中指定 statementType,然而个别状况下倡议不指定。不指定的话,默认也是 PrepareStatement。所以绝大多数状况都是通过调用 Connection 的 prepareStatement 办法创立 PrepareStatement 的,日志里的 Sql 语句就是在这儿被打印的。

而且,下面咱们也从代码角度一路跟踪过去了,连贯创立以及 statement 的创立是 mapper 接口调用进来的,所以关上 debug level 的日志须要针对 mapper 包做配置。

PrepareStatementLogger

其实从日志追踪的角度看,代码就非常简单了:

 @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, params);
      }
      if (EXECUTE_METHODS.contains(method.getName())) {if (isDebugEnabled()) {debug("Parameters:" + getParameterValueString(), true);
        }
        clearColumnInfo();
        if ("executeQuery".equals(method.getName())) {ResultSet rs = (ResultSet) method.invoke(statement, params);
          return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
        } else {return method.invoke(statement, params);
        }
      } else if (SET_METHODS.contains(method.getName())) {if ("setNull".equals(method.getName())) {setColumn(params[0], null);
        } else {setColumn(params[0], params[1]);
        }
        return method.invoke(statement, params);
      } else if ("getResultSet".equals(method.getName())) {ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      } else if ("getUpdateCount".equals(method.getName())) {int updateCount = (Integer) method.invoke(statement, params);
        if (updateCount != -1) {debug("Updates:" + updateCount, false);
        }
        return updateCount;
      } else {return method.invoke(statement, params);
      }
    } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
    }
  }

同样,日志中十分相熟的“Parameters,Updates:”等内容呈现了。

如果是 executeQuery 办法调用、须要返回 ResultSet 的话,通过 ResultSetLogger.newInstance 办法创立并返回 ResultSet 的代理对象 ResultSetLogger。

ResultSetLogger

和 ConnectionLoger、PreparedStatementLogger 的 invoke 办法大同小异:

 @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, params);
      }
      Object o = method.invoke(rs, params);
      if ("next".equals(method.getName())) {if ((Boolean) o) {
          rows++;
          if (isTraceEnabled()) {ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false;
              printColumnHeaders(rsmd, columnCount);
            }
            printColumnValues(columnCount);
          }
        } else {debug("Total:" + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
    }
  }

调用 ResultSet 的 next 办法获取数据的过程中累加记录条数,获取数据完结后 log 打印记录数。

如果日志级别设置为 TRACE,则 ResultSetLogger 会打印获取到的数据后果,内容会很多,一般来讲也没有什么意义,倡议即便测试环境也不关上。

总结

通过对 Mybatis 的 log 框架源码的剖析,咱们晓得了 Mybatis 日志的前因后果,SQL 语句的打印是 DEBUG 级别的,正式环境下咱们个别不会开 DEBUG 级别的日志,非凡状况须要察看正式环境下的 SQL 语句执行状况的话,能够只开启 mapper 接口的 DEBUG 日志。

上一篇 连接池 Druid(补充)– removeAbandoned

正文完
 0