3. 日志模块(下)

2023-10-12 09:03:49 浏览数 (2)

在日志模块的上篇中,我们详细拆解了 MyBatis 是如何整合第三方日志框架,实现了完善的日志功能的。那么在本节中,我们再来具体分析下:为了实现“将日志功能优雅地嵌入到核心流程中,实现无侵入式地日志打印”这一目标,MyBatis 内部做了怎样的设计。

日志打印功能点

为了便于分析,我们先来回顾一下原生 JDBC 的执行流程。直接上代码:

代码语言:javascript复制
/**
 * @author ZhangShenao
 * @date 2023/5/29 2:07 PM
 * Description 原生JDBC的使用方式
 */
public class JdbcDemo {
    public static void main(String[] args) throws Exception {
        //1. 注册数据库驱动/创建数据源DataSource
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2. 创建数据库连接Connection
        Connection conn = DriverManager.getConnection("xxx");

        //3. 创建执行语句Statement
        String sql = " select * from `user` ";
        PreparedStatement stmt = conn.prepareStatement(sql);

        //4. 执行SQL语句,获取结果集ResultSet
        ResultSet resultSet = stmt.executeQuery();

        //5. 解析ResultSet,获取业务对象
        List<User> users = new ArrayList<>();
        while (resultSet.next()) {
            long id = resultSet.getLong("id");
            String groupName = resultSet.getString("name");
            users.add(new User(id, groupName));
        }
        System.out.println("Users: "   users);

        //6. 释放资源对象
        resultSet.close();
        stmt.close();
        conn.close();
    }

    @Data
    @AllArgsConstructor
    @ToString
    private static class User {
        private long id;
        private String name;
    }
}

可以看到,一次典型的 JDBC 操作,会经历如下几个核心流程:

  1. 注册数据库驱动/创建数据源 DataSource
  2. 创建数据库连接 Connection
  3. 创建预编译执行语句 PreparedStatement
  4. 执行 SQL 语句,获取结果集 ResultSet
  5. 解析 ResultSet,获取业务对象;
  6. 释放资源。

在上述步骤中,可以认为最核心的需要打印日志的功能点为:

1. 创建 PrepareStatement 时:打印待执行的 SQL 语句;

2. 访问数据库时:打印实际参数的类型和值;

3. 查询出结果集后:打印结果行数及结果值。

Proxy Pattern 代理模式

需要打印日志的功能点已经明确了,接下来就是分析下怎么实现。总不能在每处直接logger.info() 吧?

在业务执行的主流程外,需要额外织入一些通用的增强逻辑,以实现对现有功能的扩展。这是典型的 Proxy Pattern 代理模式的适用场景。

按照惯例,我们来回顾一下代理模式的 UML 结构图:

(图片来源:https://refactoring.guru/design-patterns/proxy)

对照代理模式,我们可以理所当然地想到:可以通过创建一个动态代理类,在 MyBatis 的核心执行流程之外,额外增加日志打印的功能。那么 MyBatis 具体是如何实现的呢?

MyBatis 日志增强器

我们来看下 MyBatis 日志增强器的类结构图:

看到 InvocationHandler,大家肯定第一时间就能想到动态代理!没错,这些日志增强器都是通过 JDK 原生动态代理的方式创建的代理类。下面具体介绍下每个类的功能:

BaseJdbcLogger

BaseJdbcLogger 是所有日志增强器的抽象父类,它用于记录 JDBC 那些需要增强的方法,并保存运行期间的 SQL 参数信息:

代码语言:javascript复制
/**
 * 所有日志增强器的抽象父类,用于记录JDBC那些需要增强的方法,并保存运行期间的SQL参数信息
 */
public abstract class BaseJdbcLogger {
  //记录需要被增强的方法
  protected static final Set<String> SET_METHODS;
  protected static final Set<String> EXECUTE_METHODS = new HashSet<>();

  //记录运行期间的SQL参数相关信息
  private final Map<Object, Object> columnMap = new HashMap<>();

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

  //...省略非必要代码

  //在初始化时,记录所有需要被日志增强的JDBC方法
  static {
    //记录PreparedStatement中的setXXX()方法
    SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods())
        .filter(method -> method.getName().startsWith("set")).filter(method -> method.getParameterCount() > 1)
        .map(Method::getName).collect(Collectors.toSet());

    //记录executeXXX()方法
    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch");
  }

  //...省略非必要代码

  //通过Log完成日志打印
  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);
    }
  }
  
  //...省略非必要代码

}

ConnectionLogger

ConnectionLogger:数据库连接的日志增强器,用于打印 PreparedStatement 相关参数,并通过动态代理方式,创建 StatementLoggerPreparedStatementLogger 两个日志增强器。

代码语言:javascript复制
/**
 * 数据库连接的日志增强器
 */
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
  //底层维护JDCB Connection数据库连接对象
  private final Connection connection;

  //...省略非必要代码

  //方法拦截实现
  @Override
  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相关方法,创建PreparedStatementLogger日志增强器
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: "   removeExtraWhitespace((String) params[0]), true);
        }
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        return PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      }

      //针对createStatement相关方法,创建StatementLogger日志增强器
      if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        return StatementLogger.newInstance(stmt, statementLog, queryStack);
      } else {
        //Connection自身的方法,正常执行
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //创建动态代理对象
  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);
  }

  //...省略非必要代码
}

PreparedStatementLogger

PreparedStatementLoggerStatementLogger 这两个增强器的功能类似,这里以更常用的 PreparedStatementLogger 为例,其主要功能为:

  1. 打印 JDBC PreparedStatement 中的动态参数信息;
  2. 拦截 setXXX() 方法,记录封装的参数;
  3. 创建 ResultSetLogger 日志增强器,使得对于结果集的操作具备日志打印的功能。
代码语言:javascript复制
/**
 * PreparedStatement日志增强器
 */
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
  //底层维护JDBC PreparedStatement对象
  private final PreparedStatement statement;

  //...省略非必要代码

  //方法拦截实现
  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      //继承自Object类中的方法,无需增强,直接放行
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }

      //拦截executeXXX()方法,打印参数信息
      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 {
          return method.invoke(statement, params);
        }
      }

      //拦截setXXX()方法,记录动态参数
      if (SET_METHODS.contains(method.getName())) {
        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())) {
        //拦截getResultSet()方法,返回ResultSetLogger增强器
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      } else if ("getUpdateCount".equals(method.getName())) {
        //拦截getUpdateCount()方法,打印update操作影响的记录行数
        int updateCount = (Integer) method.invoke(statement, params);
        if (updateCount != -1) {
          debug("   Updates: "   updateCount, false);
        }
        return updateCount;
      } else {
        //PreparedStatement中的普通方法,直接调用
        return method.invoke(statement, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //创建动态代理对象
  public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    return (PreparedStatement) Proxy.newProxyInstance(cl,
        new Class[] { PreparedStatement.class, CallableStatement.class }, handler);
  }

  //...省略非必要代码
}

ResultSetLogger

最后一个日志增强器是 ResultSetLogger,它是结果集日志增强器,主要用于打印结果集的总记录数和每条记录的结果。

代码语言:javascript复制
/**
 * 结果集日志增强器
 */
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {
  //底层维护JDBC ResultSet对象
  private final ResultSet rs;
  
  //...省略非必要代码


  //方法拦截实现
  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      //继承自Object类中的方法,无需增强,直接放行。
      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()) {
            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  ) {
      if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
        blobColumns.add(i);
      }
      row.add(rsmd.getColumnLabel(i));
    }
    trace(row.toString(), false);
  }

  //打印结果列值
  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);
  }

  //创建代理对象
  public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
    InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
    ClassLoader cl = ResultSet.class.getClassLoader();
    return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
  }

 //...省略非必要代码

}

日志功能优雅嵌入

有了上面介绍的几个日志增强器,打印日志的功能是如何优雅地嵌入到 MyBatis 的核心执行流程中的呢?

在MyBatis 有个关键的组件 Executor,它是 MyBatis 的核心执行器接口,对于数据库的插入、查询等操作最终都是通过该接口来完成的。后面我们会有专门的篇幅来详细介绍 Executor 的内部实现,这里只需要看一下它创建数据库连接的方法:org.apache.ibatis.executor.BaseExecutor#getConnection()

代码语言:javascript复制
//创建数据库连接
  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    //创建ConnectionLogger日志增强器,获取打印日志的功能
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    }
    return connection;
  }

可以看到,这里创建的实际上是 ConnectionLogger 这个日志增强器。这样一来,通过 BaseExecutor -> ConnectionLogger -> PreparedStatementLogger -> ResultSetLogger 的执行链路,类似多米诺骨牌方式,完成了日常增强器的创建过程。

小结

在日志模块中,我们首先对 MyBatis 的日志功能进行了需求分析,接下来探讨了 MyBatis 对第三方日志框架的整合方式,进而看到了 MyBatis 如何对 JDBC 原生的组件进行日志功能增强,最后了解了把日志功能优雅嵌入到核心执行流程的小技巧。日志这个功能虽然简单,但是 MyBatis 内部的实现用到了很多经典的设计模式,如适配器模式、动态代理模式等等,代码简洁且优雅,非常值得我们学习和借鉴。

0 人点赞