Log printing principle of Mybatis

1. Mybatis log start and source code analysis

Log on

This type of log printing is the simplest, using standard output and error output. If you use other logging frameworks, the name and value are different from here. So let’s start with that. This will be explained below

<settings>
     <setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
Copy the code

Source code analysis

Mybatis starts with XML file parsing.

As long as DEBUG starts from creating the SqlSessionFactory, you can see the parse action

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

/ / the debug
private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties")); // Parse the properties tag and place it in the Variables of Parser and config
      
       // Parse the Settings tag. And change the Properties and values inside to Properties and verify that there are Properties.
      Properties settings = settingsAsProperties(root.evalNode("settings"));// Load the setting TAB
      
      loadCustomVfs(settings); // what is the VFS in lcnote? How do I know this
      VFS (virtual File System) abstracts several apis to access resources on the file system; For example, in
      // Parse mapperElement(root.evalNode("mappers")); If you specify package, you can get all the class files under the package path through the VFS.
      // It will be added to mappe, similar to the classPathSacnner in Spring, which can specify filters.

    
      
      // Customize LogImpl
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));

      // From here on, we parse the specific tag, new out the object, set the attribute under the tag,
      Mybatis (mybatis) objectFactory (mybatis) objectFactory (mybatis) The objectFactory, plugins
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectFactory"));
      reflectorFactoryElement(root.evalNode("objectFactory"));

      // Set the setting tag
      settingsElement(settings);

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

      //lcnote
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: "+ e, e); }}Copy the code
loadCustomLogImpl
 private void loadCustomLogImpl(Properties props) {
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
  }

The resolveClass method is determined by calling the resolveAlias of typeAliasRegistry
//TypeAliasRegistry is the TypeAliasRegistry
// Many types are injected by default during initialization. See the constructor of TypeAliasRegistry and Configuration for details.


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

Copy the code

After the corresponding entity class is parsed, the instance is created using LogFactory. The class object is assigned to the logImpl property of Configuration

LogFactory.useCustomLogging
 public void setLogImpl(Class<? extends Log> logImpl) {
    if(logImpl ! =null) {
      this.logImpl = logImpl;
      LogFactory.useCustomLogging(this.logImpl); }}// the method called in 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.");
      }
      
      // Notice that the Constructor found here is assigned to the logConstructor property of the LogFactory
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation. Cause: "+ t, t); }}Copy the code

Mybatis also supports the log prefix

    configuration.setLogPrefix(props.getProperty("logPrefix")); // The prefix can be set to log
// Can be set in setting.
 // The corresponding property is the logPrefix property of the Configuration
Copy the code

The logging implementation class is set here and also assigned to Configuration. So how does he use it? In the above code, after the instance is generated from LogFactory New, there is no assignment, it is allowed to log, and then it is finished, leaving only the constructor in LogFactory to create the instance. Nothing else, but in Mybatis logs look like this.

So, how does he do this?

Regardless of Mybatis implementation, to achieve such a function, the first thought is the section. Then came the creation of the proxy. In Mybatis, it is definitely not Spring. He used the JDK’s dynamic proxy.

Imagine generating proxy objects for the Statement and Result Next methods. The log can then be printed before the method is actually executed. That’s exactly what Mybatis does.

As a caveat, each class usually has a Logger object when logging. Notice that the Logger object is owned by every class.

So let’s see.

// SimpleExecutor##doQuery method.

@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);
      // This is the point. Create a Connection object inside it.
      // The ms StatementLog will be passed in.
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);// Query and map result sets
    } finally{ closeStatement(stmt); }}Copy the code

MappedStatement is an entity class that corresponds to a method in a Mapper. This object is created when parsing XMl. The StatementLog attribute must also be set at this time.

Click through the prepareStatement method to see how to create a Connection object. As anyone who has written JDBC knows, this object is the start of a query.

Create a proxy object for Connection

  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    // The code is simple. If true, a proxy object for Connection is created.
    // The StdOutImpl debugEnabled defaults to true.
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      returnconnection; }}Copy the code

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

// The JDK proxy is used to implement logging, and the JDK proxy is InvocationHandler. In this case, the ConnectionLogger
Copy the code
ConnectionLogger class details

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);
      }
      // If the method name is prepareStatement or prepareCall, the log is printed.
       // It is easy to print,
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          // The debug method calls the BaseJdbcLogger statementLog attribute, which is in MapperStatement.
          debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
        }
        // When a prepareStatement is called with Connection, a PreparedStatement is generated.
         // PreparedStatement is the BaseJdbcLogger implementation class.
        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 {
        returnmethod.invoke(connection, params); }}catch (Throwable t) {
      throwExceptionUtil.unwrapThrowable(t); }}}Copy the code

The first thing you can see is that it inherits BaseJdbcLogger, where the BaseJdbcLogger implementation class says it can print logs.

The BaseJdbcLogger class has two collections. One is SET_METHODS, which hold set methods in PreparedStatement with parameters greater than 1.

EXECUTE_METHODS holds the name for the execute executeUpdate, executeQuery, addBatch method

So let’s see what the ConnectionLoggerinvoke method does.

Comments are already written in the code above.

Create a proxy object for Statement

As you can see, it also inherits from BaseJdbcLogger and is an InvocationHandler, which must look at the Invoke method,

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;
  }
  
  If the method name is executeQuery, the ResultSetLogger will be wrapped after the method is called and the ResultSet is generated. If the method name is ResultSet, wrap the generated ResultSet **/ after calling the method
  @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 {
          returnmethod.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 {
        returnmethod.invoke(statement, params); }}catch (Throwable t) {
      throwExceptionUtil.unwrapThrowable(t); }}}Copy the code

Here the methods used to create objects are the same as the methods used to delegate objects. The steps are the same.

Create a proxy object for Result

It also inherits BaseJdbcLogger and implements InvocationHandler, again looking at the Invoke method.

BLOB_TYPES << Blob >> << Blob >> << Blob >> Print if you can. * * /
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()) {
            // Get the ResultSet print result
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false;
              // Prints header information
              printColumnHeaders(rsmd, columnCount);
            }
            // Prints a specific valueprintColumnValues(columnCount); }}else {
          / / a total
          debug(" Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throwExceptionUtil.unwrapThrowable(t); }}private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
    StringJoiner row = new StringJoiner(","." Columns: "."");
    for (int i = 1; i <= columnCount; i++) {
      // If the type is BLOB, add it to blobColumns. Note that this adds the id of the column
      if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
        blobColumns.add(i);
      }
      row.add(rsmd.getColumnLabel(i));
    }
    trace(row.toString(), false);
  }
  << blobColumns >> << blobColumns
  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); }}Copy the code

Here is the analysis of the end, Mybatis inside the log is how to print, and how to play a role.

Let’s take a look at how Mybatis integrates most logging together, and it also supports custom logging implementation classes.

3. Mybatis log analysis interface and implementation class analysis (source code analysis)

How and where is a MapperStatement created?

  1. Created when the MapperStatement is created in the parsing Xml file

    //MapperStatement inner class Builder method
        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;
          }
          // The point is here
          mappedStatement.statementLog = LogFactory.getLog(logId);
          mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
        }
    Copy the code

    As discussed above, when parsing the Setting tag, the selected constructor to build the Log is kept in the LogFactory, which can then be used to create the Log object whenever the Log is created.

    How does LogFactory create a Log object

    // The getLog method of LogFactory creates the object using the previously reserved constructor
      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); }}Copy the code

    Mybatis Log interface and implementation class (Log is how to merge common Log)

    The Log interface

    Define the Log interface, and it contains some common logging methods

    
    / * * *@author Clinton Begin
     */
    public interface Log {
     
      boolean isDebugEnabled(a);
    
      boolean isTraceEnabled(a);
    
      void error(String s, Throwable e);
    
      void error(String s);
    
      void debug(String s);
    
      void trace(String s);
    
      void warn(String s);
    
    }
    Copy the code

The Log implementation class

You customize a Log interface and then use different logging frameworks in different implementation classes. The main idea is to create a concrete Logger object in the constructor and wrap this object in a custom implementation Logger interface, thus achieving unity. Adapter mode

In the XMLSettingHow to configure to use the above implementation class (how to use other logging frameworks)

The alias and type mappings are registered through typeAliasRegistry in the Configuration constructor

Look at two examples of concrete Log interface implementations

Both of these concrete examples are very simple.

  1. StdOutImpl

    This implementation is very simple, all call System normal and error output to print.

    /* * 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(a) {
        return true;
      }
    
      @Override
      public boolean isTraceEnabled(a) {
        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); }}Copy the code
  2. 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();
      // This Logger is log4j's adaptor mode
      private final Logger log;
    
      public Log4jImpl(String clazz) {
        // The class passed in is the id of the MapperStatement
        log = Logger.getLogger(clazz);
      }
    
      @Override
      public boolean isDebugEnabled(a) {
        return log.isDebugEnabled();
      }
    
      @Override
      public boolean isTraceEnabled(a) {
        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); }}Copy the code

    2. Customize logs

    1. Implement the Log interface

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(a) {
    return true;
  }

  @Override
  public boolean isTraceEnabled(a) {
    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); }}Copy the code

2. Configure the configuration file

Note that the custom value must be a fully qualified class name. An alias cannot be configured. In the code in the above section, Setting is parsed in front of typeAliases, so it cannot be configured through aliases.

3. Results presentation

You can see that the log starts with MyLog.

Mybatis log print analysis so far. Please point out any inaccuracies. thank you