Mybatis 框架日志相关源码分析(三)

技术 · 2023-04-18

通过上篇文章《Mybatis 框架日志相关源码分析(二)》了解了 Mybatis 通过工厂模式创建 Log 接口的实现类,那么拿到实现类之后, Mybatis 是如何输出日志的呢?

本文将分析 Mybatis 框架的日志相关源码,了解 Mybatis 使用 JDBC 时,是通过何种方式输出日志。

Mybatis 执行过程也可使用 JDBC 差不多,首先是要获取 Connection 对象。而获取此对象是通过 BaseExecutor#getConnection() 方法。

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

这个方法的新参看到了熟悉的 Log 接口,假设 isDebugEnabled() 方法返回false,则返回正常的 Connection 对象;而 true 情况,则是通过 org.apache.ibatis.logging.jdbc.ConnectionLogger 类获取 Connection 对象。看来真相应该就在 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);
}

先分析 ConnectionLogger 类。此类继承 BaseJdbcLogger 并实现 InvocationHandler 接口。而实现 InvocationHandler 接口的类,说明这是一个动态代理类。当代理的原对象方法被调用时,代理对象会执行 invoke() 方法。所以 ConnectionLogger 类中,肯定实现了 invoke() 方法。

public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  try {
    // 如果是 Object 类的方法,直接执行
    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(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
      }
      // 返回 PreparedStatementLogger 代理对象
      PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
      return PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
    }
    // 如果方法名为:createStatement
    if ("createStatement".equals(method.getName())) {
      Statement stmt = (Statement) method.invoke(connection, params);
      // 返回 StatementLogger 对象
      return StatementLogger.newInstance(stmt, statementLog, queryStack);
    } else {
      // 执行被代理对象方法
      return method.invoke(connection, params);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

这个时候再回顾下 JDBC 的使用方法:

...
PreparedStatement updateSales = con.prepareStatement("SELECT * FROM USER WHERE ACCOUNT = ?");
...

假设 con 是代理对象,此时调用了 prepareStatement 方法,那么程序就会走到代理对象中的 invoke 方法。

同理,Mybatis 在上面获取的 Connection 对象是 ConnectionLogger 代理对象,那么当调用 Connection 对象的 prepareStatement 方法时,也就自然就调用 ConnectionLogger 的 invoke 方法。于是,上面 Mybatis 代码中的 debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true); 就可以被系统执行,从而实现输出日志。

到此,我们知道了 Mybatis 是通过动态代理的方式,来输出调用 Connection 类中的方法时的日志。而在源代码中,不仅仅只针对 Connection 使用了代理,还对 PreparedStatement 、ResultSet 、Statement 也都是使用了代理,代理类为:PreparedStatementLoggerResultSetLoggerStatementLogger

Java MyBatis
  1. 商城系统 2023-11-03

    感谢分享

  2. 大雄 2023-07-11

    天啊,居然还是个java大佬

    1. 此间少年 (作者)  2023-07-12
      @大雄

      刚入门,不是大佬

  3. 为啥是这样

Theme Jasmine by Kent Liao