前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >3. 日志模块(下)

3. 日志模块(下)

作者头像
张申傲
发布2023-10-12 09:03:49
1490
发布2023-10-12 09:03:49
举报
文章被收录于专栏:漫漫架构路漫漫架构路

日志模块的上篇中,我们详细拆解了 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 内部的实现用到了很多经典的设计模式,如适配器模式、动态代理模式等等,代码简洁且优雅,非常值得我们学习和借鉴。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 日志打印功能点
  • Proxy Pattern 代理模式
  • MyBatis 日志增强器
    • BaseJdbcLogger
      • ConnectionLogger
        • PreparedStatementLogger
          • ResultSetLogger
          • 日志功能优雅嵌入
          • 小结
          相关产品与服务
          数据库
          云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档