Mybatis日志打印原理

1. Mybatis日志开启和源码分析

日志开启

一般常见的都是这种开启方式,这种日志打印是最简单的,采用的标准输出和错误输出的方式,如果采用的别的日志框架的话,name和value和这里不一样。这里就先作为开始。在下面会有介绍的

<settings>
     <setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
源码分析

Mybatis一切的开始都要从XML文件解析开始。

只要debug从创建SqlSessionFactory开始,就能看到解析操作

sqlMapper = new SqlSessionFactoryBuilder().build(reader);

//debug进去
private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties")); //解析properties标签,并把他放在 parser和config的 Variables 里面
      
       //解析settings标签。并且将里面的属性和值,变为Properties,并且验证是否有属性。
      Properties settings = settingsAsProperties(root.evalNode("settings"));//加载setting标签
      
      loadCustomVfs(settings); //lcnote 这里的vfs是啥?怎么用 我知道这个
      //我现在知道他的vfs是什么了,vfs(virtual file system)他抽象出了几个api,通过这些api就可以访问文件系统上的资源;比如在
      // 在解析   mapperElement(root.evalNode("mappers"));的时候,如果指定package,就可以通过VFS来获取包路径下面所有的class文件。
      // 并且会将他添加到mappe里面,和spring中的classPathSacnner一样差不多,可以指定过滤器。

    
      
      //自定义LogImpl
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));

      //从这里开始,都是解析具体的标签,new出对象,将标签下面的属性设置进去,
      // 从解析的这里基本也能看出mybatis里面重要的几个点,首先是objectFactory,objectFactory。objectFactory,plugins
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectFactory"));
      reflectorFactoryElement(root.evalNode("objectFactory"));

      //这里就具体设置setting标签了
      settingsElement(settings);

      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));

      //lcnote 这里是重点,解析mapper文件,
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
loadCustomLogImpl
private void loadCustomLogImpl(Properties props) {
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
  }

//resolveClass方法会通过调用typeAliasRegistry的resolveAlias来判断
//TypeAliasRegistry是类型别名注册器
//初始化的时候默认注入了好多类型,具体的可以看TypeAliasRegistry和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);

在解析到对应的实体类之后,就会通过LogFactory创建出实例。并且将class对象赋值给Configuration的logImpl属性

LogFactory.useCustomLogging
public void setLogImpl(Class<? extends Log> logImpl) {
    if (logImpl != null) {
      this.logImpl = logImpl;
      LogFactory.useCustomLogging(this.logImpl);
    }
  }

// useCustomLogging里面调用的方法
 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.");
      }
      
      //注意,这里将找到的Constructor赋值给了LogFactory的logConstructor属性
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

还有一个小tips,Mybatis里面还支持log的前缀

configuration.setLogPrefix(props.getProperty("logPrefix")); //这个是比较意思的,居然是可以设置log的前缀,很奇妙
// 可以在setting里面设置。
 // 对应的属性是configuration的logPrefix的属性

在这里设置好日志的实现类,并且也赋值给configuration了。那他是怎么使用的呢?在上面的代码中从LogFactory new出实例之后,也没有赋值,就答应了一下日志,然后就结束了,只有在LogFactory中保留了构造方法,用于创建实例。别的都没有了,可是在Mybatis中日志都长这个样子的。

springboot中mybatis输出日志 mybatis日志打印_bc

那么,下面就看看他是如果做这样的事情的?

抛开Mybatis实现不谈,要实现这样的功能,第一时间想到的就是切面。然后就想到了代理的创建方式。在Mybatis里面肯定用的不是Spring的切面。他用的JDK的动态代理。

想想,如果生成了代理对象给Statement,和Result的Next方法。那么在实际执行方法之前就可以将日志打印出来。事实Mybatis的确是这样做的。

在说一个预备知识,平常在使用日志的时候,每个类都有一个Logger对象。注意,Logger对象是每一个类都有的。

那么下面就具体看看。

// SimpleExecutor##doQuery方法。 

@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 重点就会这个。在这里面创建Connection对象。
      //会将ms中的StatementLog传递进去。
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);//查询并且映射结果集
    } finally {
      closeStatement(stmt);
    }
  }

MappedStatement这个就是一个Mapper中一个方法对应的实体类。这个对象是在解析XMl的时候创建的。StatementLog的属性也肯定是在这个时候设置进去的。

顺着prepareStatement方法点进去,就会看到创建Connection对象的方法。写过JDBC的都知道,这个对象是查询的开始。

给Connection创建代理对象

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    //代码很简单。如果为true就会创建一个Connection的代理对象。
    // StdOutImpl的debugEnabled默认是true。
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }
ConnectionLogger.newInstance
/**
   * Creates a logging version of a connection.
   *
   * @param conn
   *          the original connection
   * @param statementLog
   *          the statement log
   * @param queryStack
   *          the query stack
   * @return the connection with logging
   */
  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的代理来实现日志的,既然是JDK,那么肯定是要看InvocationHandler了。在这里就是ConnectionLogger
ConnectionLogger类详情
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  private final Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // 如果方法名字是prepareStatement或者prepareCall,打印日志。
       // 具体的打印就很简单了,
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          // 这里的debug方法会调用到BaseJdbcLogger里面的statementLog属性,这属性就是在MapperStatement里面的。
          debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
        }
        //当利用Connection调用prepareStatement的时候,就会生成一个PreparedStatement。
         // 这里的PreparedStatement也是BaseJdbcLogger的实现类。
        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);
    }
  }
}

首先可以看到他是继承与BaseJdbcLogger,BaseJdbcLogger的实现类表示,能在那些地方打印日志。

springboot中mybatis输出日志 mybatis日志打印_mybatis_02

在BaseJdbcLogger的类里面有两个集合,一个是SET_METHODS(保存着PreparedStatement里面set方法,并且参数大于1的方法)

EXECUTE_METHODS保存着名字为execute,executeUpdate,executeQuery,addBatch的方法

那么在看看ConnectionLoggerinvoke方法干了什么事情。

在上面的代码里面已经写了注释了。

给Statement创建代理对象

可以看到,他也是继承于BaseJdbcLogger并且也是一个InvocationHandler,那肯定看invoke方法,

public final class StatementLogger extends BaseJdbcLogger implements InvocationHandler {

  private final Statement statement;

  private StatementLogger(Statement stmt, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.statement = stmt;
  }
  
  /**
  1. 这里的方法很明显,
  2. 还是通过方法名字来判断,如果方法名字是executeQuery,在调用方法之后,生成ResultSet之后,还会使用ResultSetLogger包装
  3. 如果方法名字是ResultSet,再到调用方法之后,对生成的ResultSet包装
  **/
  @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(" Executing: " + removeExtraWhitespace((String) params[0]), true);
        }
        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 ("getResultSet".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);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
}

这里创建对象的方法代理对象的方法都是一致的。步骤都是一致的。

给Result创建代理对象

也是继承BaseJdbcLogger并且实现了InvocationHandler,同样看invoke方法。

/**
1. 注意这里的static里面的东西
2. BLOB_TYPES包含了几种类型,这里面包含的是Blob类型的,遇到这种类型是不打印日志了,不能打印就用<<BLOB>>代替
3. 如果能打印就直接打印。
**/
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {

  private static final Set<Integer> BLOB_TYPES = new HashSet<>();
  private boolean first = true;
  private int rows;
  private final ResultSet rs;
  private final Set<Integer> blobColumns = new HashSet<>();

  static {
    BLOB_TYPES.add(Types.BINARY);
    BLOB_TYPES.add(Types.BLOB);
    BLOB_TYPES.add(Types.CLOB);
    BLOB_TYPES.add(Types.LONGNVARCHAR);
    BLOB_TYPES.add(Types.LONGVARBINARY);
    BLOB_TYPES.add(Types.LONGVARCHAR);
    BLOB_TYPES.add(Types.NCLOB);
    BLOB_TYPES.add(Types.VARBINARY);
  }

  private ResultSetLogger(ResultSet rs, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.rs = rs;
  }

  @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()) {
            // 拿到ResultSet打印结果
            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);
    }
  }

  private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
    StringJoiner row = new StringJoiner(", ", "   Columns: ", "");
    for (int i = 1; i <= columnCount; i++) {
      //如果类型是之前规定的BLOB类型,就添加到blobColumns里面,注意,这添加的可是列的id
      if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
        blobColumns.add(i);
      }
      row.add(rsmd.getColumnLabel(i));
    }
    trace(row.toString(), false);
  }
  //这里就也一样,判断一下是不是blobColumns里面包含的,用<<BLOB>>来代替
  private void printColumnValues(int columnCount) {
    StringJoiner row = new StringJoiner(", ", "       Row: ", "");
    for (int i = 1; i <= columnCount; i++) {
      try {
        if (blobColumns.contains(i)) {
          row.add("<<BLOB>>");
        } else {
          row.add(rs.getString(i));
        }
      } catch (SQLException e) {
        // generally can't call getString() on a BLOB column
        row.add("<<Cannot Display>>");
      }
    }
    trace(row.toString(), false);
  }
}

到这里就分析完了,Mybatis里面日志是怎么打印的,并且是怎么起作用的。

下面就整体的看看Mybatis是如果将大多数的日志整合在一起的,并且它还支持自定义的日志实现类。

3. Mybatis日志分析接口和实现类分析(源码分析)

MapperStatement是怎么创建日志的,在哪里创建的?

  1. 在解析Xml文件中创建MapperStatement的时候创建的
//MapperStatement内部类Builder方法
    public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
      mappedStatement.configuration = configuration;
      mappedStatement.id = id;
      mappedStatement.sqlSource = sqlSource;
      mappedStatement.statementType = StatementType.PREPARED;
      mappedStatement.resultSetType = ResultSetType.DEFAULT;
      mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<>()).build();
      mappedStatement.resultMaps = new ArrayList<>();
      mappedStatement.sqlCommandType = sqlCommandType;
      mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      String logId = id;
      if (configuration.getLogPrefix() != null) {
        logId = configuration.getLogPrefix() + id;
      }
      //重点是这里
      mappedStatement.statementLog = LogFactory.getLog(logId);
      mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
    }

在上面分析过,在解析Setting标签的时候,会在LogFactory保留所选构造日志的构造函数,之后凡是创建日志,就可以通过这个构造函数来创建Log对象。

LogFactory是怎么创建Log对象的

//LogFactory的getLog方法,通过之前保留的构造函数来创建对象
  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);
    }
  }

Mybatis日志Log接口和实现类(Log是怎么将常见的日志都融合进的)

Log接口

定义Log接口,并且里面包含了一些打日志常见的方法

/**
 * @author Clinton Begin
 */
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实现类

自定了一个Log接口,然后再不同的实现类里面采用不同的日志框架。主要是在构造方法里面先创建出具体的Logger对象,将这个对象用自定义的实现了Logger接口包装,这样就实现了统一。适配器模式

springboot中mybatis输出日志 mybatis日志打印_java_03

在xml中的Setting中怎么配置能使用到上面的实现类(怎么用别的日志框架)

Configuration构造方法里面通过typeAliasRegistry来注册别名和类型的映射关系

springboot中mybatis输出日志 mybatis日志打印_方法名_04

看两个个具体的Log接口实现的例子

这两具体的例子都很简单。

StdOutImpl

这个实现就很简单了,都是调用System正常和错误的输出来打印的。

/*
 *    Copyright 2009-2021 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
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);
  }

  @Override
  public void debug(String s) {
    System.out.println(s);
  }

  @Override
  public void trace(String s) {
    System.out.println(s);
  }

  @Override
  public void warn(String s) {
    System.out.println(s);
  }
}
Log4jImpl
/*
 *    Copyright 2009-2021 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
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();
  // 这个Logger是log4j的,适配器模式,真正打日志的还是Log4j
  private final Logger log;

  public Log4jImpl(String clazz) {
    // 传递进来的class就是MapperStatement的id
    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);
  }

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

  @Override
  public void debug(String s) {
    log.log(FQCN, Level.DEBUG, s, null);
  }

  @Override
  public void trace(String s) {
    log.log(FQCN, Level.TRACE, s, null);
  }

  @Override
  public void warn(String s) {
    log.log(FQCN, Level.WARN, s, null);
  }

}

2. 自定义日志

1. 实现Log接口

package org.apache.ibatis.session.mybatis;

import org.apache.ibatis.logging.Log;

public class MyLog implements Log {
  public MyLog(String id){
    System.out.println("MyLog:"+id);
  }
  @Override
  public boolean isDebugEnabled() {
    return true;
  }

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

  @Override
  public void error(String s, Throwable e) {
    System.out.println("MyLog:"+s);
    e.printStackTrace();
  }

  @Override
  public void error(String s) {
    System.out.println("MyLog:"+s);
  }

  @Override
  public void debug(String s) {
    System.out.println("MyLog:"+s);
  }

  @Override
  public void trace(String s) {
    System.out.println("MyLog:"+s);
  }

  @Override
  public void warn(String s) {
    System.out.println("MyLog:"+s);
  }
}

2. 配置文件配置

springboot中mybatis输出日志 mybatis日志打印_bc_05

注意自定义的value必须是全限定类名。不能配置别名。因为上面章节的代码里面,解析Setting在前面,解析typeAliases在后面,故不能通过别名来配置。

3. 结果展示

可以看到日志都以MyLog开头了。

springboot中mybatis输出日志 mybatis日志打印_java_06

Mybatis日志打印就分析到这里了。如有不正确的地方,欢迎指出。谢谢。