MyBatis log module analysis.

1. Log module demand analysis

  1. MyBatis does not provide a Log implementation class and needs to access third-party Log components, but the third-party Log components have their own Log levels, which are different, while MyBatis provides four levels of trace, DEBUG, WARN and Error.
  2. Automatic log scanning is implemented and the third-party log plug-in load priority is as follows: slf4J → commonsLoging →Log4J2 →Log4J → JdkLog.
  3. The use of logging should be gracefully embedded into the principal functionality.

2. Access third-party log files

1. Adapter mode

The first requirement for the logging module is a typical scenario using the Adapter Pattern, which acts as a bridge between incompatible interfaces to convert one class’s interface into another that the customer wants. The adapter pattern allows classes that otherwise would not work together due to interface incompatibilities to work together; The class diagram is as follows:

  • Target: Target role, expected interface.
  • Adaptee: The role of the adaptor, the interface to be adapted.
  • Adapter: Adapter role that converts the source interface to the target interface.

Application scenario: The adaptor pattern can be used to reuse existing components when neither of the calling parties can easily modify it. It is often used when third-party components are added to the system.

Note: The designer should consider refactoring the system if there are too many adapters in the system, which can increase the complexity of the system.

2. How does the log module use adapter mode

  • Target: Target role, expected interface. Org. Apache. Ibatis. Logging. The Log interface, internal provides a unified interface Log.
  • Adaptee: The role of the adaptor, the interface to be adapted. Other log components, such as slf4J, commonsLoging, and Log4J2, are included in the adapter.
  • Adapter: Adapter role that converts the source interface to the target interface. Adapters are provided for each logging component, and each adaptor packs and converts a specific log component. Such as Slf4jLoggerImpl and Log4jImpl.

Conclusion:

  • Log module implementation adopts adapter mode, Log component (Target), adapter and unified interface (Log interface), clearly defined in accordance with the single responsibility principle.
  • At the same time, when using logs, the client programming is oriented to the Log interface and does not need to care about the implementation of the underlying Log module, which conforms to the principle of dependency inversion.
  • Most importantly, if other third-party logging frameworks need to be added later, the new module can only be extended to meet the new requirements without changing the original code, which again conforms to the open closed principle.

How to realize priority loading of log module

In the org. Apache. Ibatis. Logging. LogFactory, can see is the use of static block of code to implement priority load, posted on the part of the important source code:

public final class LogFactory {

  /** * The selected third party logging component adapter constructor */
  private static Constructor<? extends Log> logConstructor;

  // Automatically scan logs, and the third-party log plug-in load priority is as follows: slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
  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) {
      // Execute the constructor when it is not null
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore}}}/** * initializes the constructor with the specified log class */
  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); }}}Copy the code

4. How to embed logs gracefully into the principal function

1. Proxy mode

Proxy pattern definition: a proxy object is provided to a target object, and the proxy object controls the reference to the target object.

Objective:

  1. The proxy object is introduced to indirectly access the target object to prevent unnecessary complexity caused by direct access to the target object.
  2. Proxy objects are used to enhance existing services.

Proxy pattern class diagram:

Static agent

This type of proxy requires that the proxy object and the target object implement the same interface.

Advantage: You can extend the functionality of the target object without modifying the target object.

Disadvantages: Redundancy. Because the proxy object needs to implement the same interface as the target object, too many proxies will be generated. It is not easy to maintain. Once a method is added to an interface, both the target object and the proxy object have to be modified.

A dynamic proxy

Dynamic proxy utilizes the JDK API to dynamically build proxy objects in memory to implement proxy functions to target objects.

Dynamic proxies are also called JDK proxies or interface proxies. The differences between static proxy and dynamic proxy are as follows:

  • The static proxy is implemented at compile time, and the proxy class is an actual class file.
  • Dynamic proxies are generated dynamically at run time, meaning that there are no actual class files after compilation, but instead class bytecode is generated dynamically at run time and loaded into the JVM.

Note: The dynamic proxy object does not need to implement the interface, but the target object must implement the interface, otherwise the dynamic proxy cannot be used.

There are two classes involved in generating proxy objects in the JDK.

  • The first class, java.lang.reflect.proxy, generates a Proxy object through the static method newProxyInstance.
  • The second for Java. Lang. Reflect. InvocationHandler interface, through the invoke method to enhance their business.

2. Enhance the log function

First of all, where do you need to print logs?

  1. When you create a prepareStatement, print the SQL statement that is executed.
  2. Print the types and values of the parameters when accessing the database.
  3. After querying the structure, print the number of results.

So the log module package org. Apache. Ibatis. Logging. BaseJdbcLogger of JDBC, ConnectionLogger, PreparedStatementLogger ResultSetLogge Responsible for printing logs at different locations through dynamic proxies; The class diagram for several related classes is as follows:

BaseJdbcLogger: Abstract base class for all logging enhancements. It is used to record JDBC methods that need to be enhanced, save runtime SQL parameter information, and post part of the code below

public abstract class BaseJdbcLogger {

  /** * Save the set method commonly used in prepareStatment */
  protected static final Set<String> SET_METHODS;
  /** * Save the method of executing SQL statements in prepareStatment */
  protected static final Set<String> EXECUTE_METHODS = new HashSet<>();
  /** * Save the set key-value pairs */ in prepareStatment
  private final Map<Object, Object> columnMap = new HashMap<>();
  /** * Save the key of the set method */ in prepareStatment
  private final List<Object> columnNames = new ArrayList<>();
  /** * Save the value of the set method in prepareStatment */
  private final List<Object> columnValues = new ArrayList<>();
  
  static {
    // Select * from 'set' where 'set' = 'set'
    SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods())
            .filter(method -> method.getName().startsWith("set"))
            .filter(method -> method.getParameterCount() > 1)
            .map(Method::getName)
            .collect(Collectors.toSet());

    // The method to execute the SQL statement
    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch"); }}Copy the code

ConnectionLogger: Prints connection information and SQL statements. If prepareStatement, prepareCall, or createStatement methods are called, Print the SQL statement to execute and return the proxy object (PreparedStatementLogger) of the prepareStatement, make prepareStatement also log capable, print the parameters, and post the following part of the code

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  /** * Real connection object */
  private final Connection connection;

  /** * Enhancements to connections */
  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      // If the method inherited from Obeject is ignored directly
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      PrepareStatement, prepareCall, createStatement, print the SQL statement to execute
      // Return a proxy object for the prepareStatement, so that the prepareStatement can also log and print parameters
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          // Prints logs
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        // Enhanced PreparedStatement
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        // Create a proxy object
        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

PreparedStatementLogger: A prepareStatement object is enhanced by:

  • Enhance the setXXX method of PreparedStatement to set parameters to columnMap, columnNames, and columnValues in preparation for printing parameters.
  • Enhance the Execute related method in a PreparedStatement. When the method is executed, it prints parameters through the dynamic proxy and returns a resultSet with dynamic proxy capabilities.
  • In the case of queries, enhance the getResultSet method in PreparedStatement to return a resultSet with dynamic proxy capability; in the case of updates, directly print the number of affected rows.

Post a portion of the code as follows:

public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {

  private final PreparedStatement statement;

  /** * 1, enhance setxxx method in PreparedStatement to set parameters to columnMap, columnNames, columnValues * 2. Enhance the execute related method in a PreparedStatement. When the method is executed, print the parameters through the dynamic proxy and return a resultSet * 3 with dynamic proxy capabilities. For queries, enhance the getResultSet method in PreparedStatement to return a resultSet * 4 with dynamic proxy capabilities. If it is an update, print directly the number of affected lines */
  @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 {
          returnmethod.invoke(statement, params); }}else if (SET_METHODS.contains(method.getName())) {
        // Set parameters to columnMap, columnNames, and columnValues in preparation for printing parameters
        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())) {
        // If it is an update, print the number of affected lines directly
        int updateCount = (Integer) method.invoke(statement, params);
        if(updateCount ! = -1) {
          debug(" Updates: " + updateCount, false);
        }
        return updateCount;
      } else {
        returnmethod.invoke(statement, params); }}catch (Throwable t) {
      throwExceptionUtil.unwrapThrowable(t); }}}Copy the code

ResultSetLogge: Responsible for printing data result information, post part of the code as follows

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // Execute the result.next method to see if there is any data left
      Object o = method.invoke(rs, params);
      if ("next".equals(method.getName())) {
        if ((Boolean) o) {
          // If there is still data, the counter rows is added by one
          rows++;
          if (isTraceEnabled()) {
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false; printColumnHeaders(rsmd, columnCount); } printColumnValues(columnCount); }}else {
          // If no data is available, print "Rows" and print the number of queried data
          debug(" Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throwExceptionUtil.unwrapThrowable(t); }}Copy the code

Above said so much, is the implementation of the log function, that log function is how to join the main function?

Since the Executor is the component that accesses the database in Mybatis, the logging function is embedded in the Executor. Specific code in the org. Apache. Ibatis. Executor. SimpleExecutor. PrepareStatement (StatementHandler, Log) method.

  / / create the Statement
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // Get the dynamic proxy for the Connection object and add the logging capability;
    Connection connection = getConnection(statementLog);
    // Create a (prepare) Statement using a connection with a different StatementHandler
    stmt = handler.prepare(connection, transaction.getTimeout());
    // Use parameterHandler to handle placeholders
    handler.parameterize(stmt);
    return stmt;
  }
Copy the code

Now that you’ve read this, like, comment, follow, or collect it!

Author: IT wang2 xiao3 er4 starting address: www.itwxe.com/posts/75767… Copyright notice: The content of the article is subject to the authorship-non-commercial-no deduction 4.0 international license. If you want to reprint, please link to the author in a prominent position on the article page.