Mybatis Source Code Learning-Logging Module II: About JDBC Dynamic Agent Implementing Logging Function

1. Overview

_Log module not only realizes the integration of regular third-party logs, but also realizes the log enhancement of JDBC operations through JDK dynamic proxy.

2. Dynamic Agent

_Subject is the business logic interface in the program. RealSubject is the real business class that implements the Subject interface. Proxy is the proxy class that implements the Subject interface, which encapsulates the RealSubject object. In the program, the method of RealSubject object is not directly mobilized, but the function is realized by Proxy object. The implementation of the Proxy.operation() method calls the operation () method of the RealSubject object to execute the real business logic, but after processing the business logic, Proxy.operation() preprocesses and postprocesses before and after the invocation of the RealSubjct. operation () method. This is the so-called "agency model".

3. Dynamic Enhancement Class (Proxy)

There are so many dynamic enhancement classes used in log module. BaseJdbcLogger is the base class, and then distributed to implement Connection, PreparedStatement, ResultSet, Statement and other classes. As shown in the following figure:

  • BaseJdbcLogger base class
/**
 * Base class for proxies to do logging
 * jdbc Log base class, proxy class, realize log function
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public abstract class BaseJdbcLogger {

  protected static final Set<String> SET_METHODS = new HashSet<String>();
  protected static final Set<String> EXECUTE_METHODS = new HashSet<String>();

  private final Map<Object, Object> columnMap = new HashMap<Object, Object>();

  private final List<Object> columnNames = new ArrayList<Object>();
  private final List<Object> columnValues = new ArrayList<Object>();

  protected Log statementLog;
  protected int queryStack;

  /*
   * Default constructor
   */
  public BaseJdbcLogger(Log log, int queryStack) {
    this.statementLog = log;
    if (queryStack == 0) {
      this.queryStack = 1;
    } else {
      this.queryStack = queryStack;
    }
  }

  static {
    SET_METHODS.add("setString");
    SET_METHODS.add("setNString");
    SET_METHODS.add("setInt");
    SET_METHODS.add("setByte");
    SET_METHODS.add("setShort");
    SET_METHODS.add("setLong");
    SET_METHODS.add("setDouble");
    SET_METHODS.add("setFloat");
    SET_METHODS.add("setTimestamp");
    SET_METHODS.add("setDate");
    SET_METHODS.add("setTime");
    SET_METHODS.add("setArray");
    SET_METHODS.add("setBigDecimal");
    SET_METHODS.add("setAsciiStream");
    SET_METHODS.add("setBinaryStream");
    SET_METHODS.add("setBlob");
    SET_METHODS.add("setBoolean");
    SET_METHODS.add("setBytes");
    SET_METHODS.add("setCharacterStream");
    SET_METHODS.add("setNCharacterStream");
    SET_METHODS.add("setClob");
    SET_METHODS.add("setNClob");
    SET_METHODS.add("setObject");
    SET_METHODS.add("setNull");

    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch");
  }

  protected void setColumn(Object key, Object value) {
    columnMap.put(key, value);
    columnNames.add(key);
    columnValues.add(value);
  }

  protected Object getColumn(Object key) {
    return columnMap.get(key);
  }
  /**
   * Convert the columnValues collection into String strings, and delete the middle brackets "[",]"before and after the strings.
   * @return
   */
  protected String getParameterValueString() {
    List<Object> typeList = new ArrayList<Object>(columnValues.size());
    for (Object value : columnValues) {
      if (value == null) {
        typeList.add("null");
      } else {
        typeList.add(objectValueString(value) + "(" + value.getClass().getSimpleName() + ")");
      }
    }
    final String parameters = typeList.toString();
    //Remove the brackets "["]"at the beginning and end.
    return parameters.substring(1, parameters.length() - 1);
  }
  /**
   * Object transformation
   * 	If value belongs to the Array class, it is implemented through Arrays.toString().
   * 	Otherwise, the toString() value of the object is returned directly.
   * @param value
   * @return
   */
  protected String objectValueString(Object value) {
    if (value instanceof Array) {
      try {
        return ArrayUtil.toString(((Array) value).getArray());
      } catch (SQLException e) {
        return value.toString();
      }
    }
    return value.toString();
  }

  protected String getColumnString() {
    return columnNames.toString();
  }

  protected void clearColumnInfo() {
    columnMap.clear();
    columnNames.clear();
    columnValues.clear();
  }
  /**
   * Remove string special characters
   * @param original
   * @return
   */
  protected String removeBreakingWhitespace(String original) {
    StringTokenizer whitespaceStripper = new StringTokenizer(original);
    StringBuilder builder = new StringBuilder();
    while (whitespaceStripper.hasMoreTokens()) {
      builder.append(whitespaceStripper.nextToken());
      builder.append(" ");
    }
    return builder.toString();
  }

  protected boolean isDebugEnabled() {
    return statementLog.isDebugEnabled();
  }

  protected boolean isTraceEnabled() {
    return statementLog.isTraceEnabled();
  }

  protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
      statementLog.debug(prefix(input) + text);
    }
  }

  protected void trace(String text, boolean input) {
    if (statementLog.isTraceEnabled()) {
      statementLog.trace(prefix(input) + text);
    }
  }
  /**
   * Generate "<=" or "=>" strings, the number of equal sign "=" is queryStack * 2 + 1
   * isInput=true When generating "==>" type string, otherwise generating "<==" type string
   * @param isInput
   * @return
   */
  private String prefix(boolean isInput) {
    char[] buffer = new char[queryStack * 2 + 2];
    Arrays.fill(buffer, '=');
    buffer[queryStack * 2 + 1] = ' ';
    if (isInput) {
      buffer[queryStack * 2] = '>';
    } else {
      buffer[0] = '<';
    }
    return new String(buffer);
  }

}
  • ConnectionLogger class
    Take the ConnectionLogger class as an example to learn how to improve object functionality. Where the property field is Connection, the instance object is an enhancement of Connection by Mybatis Connection Logger.
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;
  }
  /**
   * Enhance Connection Function and Add Logging Function
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
    	//method.getDeclaringClass(); the class in which the method definition resides
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }    
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((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);
    }
  }

  /**
   * Creates a logging version of a connection
   *	Create a connection instance object with log function, which is implemented by Proxy in java.
   * @param conn - the original connection
   * @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);
  }

  /**
   * return the wrapped connection
   *	
   * @return the connection
   */
  public Connection getConnection() {
    return connection;
  }

}

4. Application of JDBC Dynamic Proxy Log in Mobile

_The use of JDBC dynamic proxy log is analyzed by taking SimpleExecutor.doQuery() method as an example.

  1. 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);
     stmt = prepareStatement(handler, ms.getStatementLog());
     return handler.<E>query(stmt, resultHandler);
   } finally {
     closeStatement(stmt);
   }
 }

_This method is Mybatis's underlying method of querying list data, in which prepareStatement() method is the encapsulation of JDBC calling database.

  1. prepareStatement() method
    _Where getConnection() returns a Connection object, that is, an enhanced object with log function. This method is located in the base class BaseExecutor of SimpleExecutor.
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;
  }
  1. getConnection() method
    __In the getConnection() method of BaseExecutor class, the log enhancement function of Connection object is realized. First, get the real Connection object, and then decide whether the logging function is turned on. If it is turned on, create the enhancement class through the ConnectionLogger.newInstance() method, or return as it is.
 protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

3.ConnectionLogger.newInstance() method
This method creates an enhanced object through the dynamic proxy class Proxy of JDK and returns it. In the previous query, the Connection object that is called again is an enhanced object. In this way, the log function is implemented on the original Connection object.

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

Tags: JDBC JDK Mybatis Java

Posted on Fri, 06 Sep 2019 20:14:51 -0700 by Foregone96