MyBatis日志模块分析

1,652 阅读8分钟

MyBatis日志模块分析。

一、日志模块需求分析

  1. MyBatis 没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的 Log 级别,且各不相同,而 MyBatis 统一提供了 trace、debug、warn、error 四个级别。
  2. 自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging →Log4J2 → Log4J → JdkLog 。
  3. 日志的使用要优雅的嵌入到主体功能中。

二、接入第三方日志文件

1. 适配器模式

日志模块的第一个需求是一个典型的使用适配器模式的场景,适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作;类图如下:

适配器模式类图

  • Target:目标角色,期待得到的接口。
  • Adaptee:适配者角色,被适配的接口。
  • Adapter:适配器角色,将源接口转换成目标接口。

适用场景:当调用双方都不太容易修改的时候,为了复用现有组件可以使用适配器模式,在系统中接入第三方组件的时候经常被使用到。

注意:如果系统中存在过多的适配器,会增加系统的复杂性,设计人员应考虑对系统进行重构。

2. 日志模块是怎么使用适配器模式

  • Target:目标角色,期待得到的接口。org.apache.ibatis.logging.Log 接口,对内提供了统一的日志接口。
  • Adaptee:适配者角色,被适配的接口。其他日志组件组件如 slf4J 、commonsLoging 、Log4J2 等被包含在适配器中。
  • Adapter:适配器角色,将源接口转换成目标接口。针对每个日志组件都提供了适配器,每 个 适 配 器 都 对 特 定 的 日 志 组 件 进 行 封 装 和 转 换。如 Slf4jLoggerImpl、Log4jImpl 等。

日志模块适配器结构类图

总结

  • 日志模块实现采用适配器模式,日志组件(Target)、适配器以及统一接口(Log 接口),定义清晰明确符合单一职责原则。
  • 同时,客户端在使用日志时,面向 Log 接口编程,不需要关心底层日志模块的实现,符合依赖倒转原则。
  • 最为重要的是,如果后面还需要加入其他第三方日志框架,只需要扩展新的模块满足新需求,而不需要修改原有代码,这又符合了开闭原则。

三、日志模块怎么实现优先级加载

org.apache.ibatis.logging.LogFactory中,可以看到是使用静态代码块来实现优先级加载,贴出源码中一部分重要的代码:

public final class LogFactory {

  /**
   * 被选定的第三方日志组件适配器的构造方法
   */
  private static Constructor<? extends Log> logConstructor;

  // 自动扫描日志实现,并且第三方日志插件加载优先级如下: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) {
      // 当构造方法不为空才执行方法
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  /**
   * 通过指定的log类来初始化构造方法
   */
  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);
    }
  }

}

四、怎么将日志优雅的嵌入主体功能中

1. 代理模式

代理模式定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用。

目的

  1. 通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来的不必要复杂性。
  2. 通过代理对象对原有的业务增强。

代理模式类图

代理模式类图

静态代理

这种代理方式需要代理对象和目标对象实现一样的接口。

优点:可以在不修改目标对象的前提下扩展目标对象的功能。

缺点:冗余,由于代理对象要实现与目标对象一致的接口,会产生过多的代理;不易维护,一旦接口增加方法,目标对象与代理对象都要进行修改。

动态代理

动态代理利用了 JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。

动态代理又被称为 JDK 代理或接口代理。静态代理与动态代理的区别主要在:

  • 静态代理在编译时就已经实现,编译完成后代理类是一个实际的 class 文件。
  • 动态代理是在运行时动态生成的,即编译完成后没有实际的 class 文件,而是在运行时动态生成类字节码,并加载到 JVM 中。

注意:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

JDK 中生成代理对象主要涉及两个类。

  • 第一个类为 java.lang.reflect.Proxy,通过静态方法 newProxyInstance 生成代理对象。
  • 第二个为 java.lang.reflect.InvocationHandler 接口,通过 invoke 方法对业务进行增强。

2. 增强日志功能

首先搞清楚那些地方需要打印日志?

  1. 在创建 prepareStatement 时,打印执行的 SQL 语句。
  2. 访问数据库时,打印参数的类型和值。
  3. 查询出结构后,打印结果数据条数。

因此在日志模块包 org.apache.ibatis.logging.jdbc 中有 BaseJdbcLogger、ConnectionLogger、PreparedStatementLogger ResultSetLogge 通过动态代理负责在不同的位置打印日志;几个相关类的类图如下:

日志模块类图

BaseJdbcLogger:所有日志增强的抽象基类,用于记录 JDBC 那些方法需要增强,保存运行期间 sql 参数信息,贴出一部分代码如下

public abstract class BaseJdbcLogger {

  /**
   * 保存preparestatment中常用的set方法(占位符赋值)
   */
  protected static final Set<String> SET_METHODS;
  /**
   * 保存preparestatment中常用的执行sql语句的方法
   */
  protected static final Set<String> EXECUTE_METHODS = new HashSet<>();
  /**
   * 保存preparestatment中set方法的键值对
   */
  private final Map<Object, Object> columnMap = new HashMap<>();
  /**
   * 保存preparestatment中set方法的key值
   */
  private final List<Object> columnNames = new ArrayList<>();
  /**
   * 保存preparestatment中set方法的value值
   */
  private final List<Object> columnValues = new ArrayList<>();
  
  static {
    // 利用反射得到set开头并且参数个数大于1的方法,转换成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());

    // 执行sql语句的方法
    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch");
  }
}

ConnectionLogger:负责打印连接信息和 SQL 语句。通过动态代理,对 connection 进行增强,如果是调用 prepareStatement、prepareCall、createStatement 的方法,打印要执行的 sql 语句并返回 prepareStatement 的代理对象(PreparedStatementLogger),让 prepareStatement 也具备日志能力,打印参数,贴出一部分代码如下

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  /**
   * 真正的连接对象
   */
  private final Connection connection;

  /**
   * 对连接的增强
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      // 如果是从Obeject继承的方法直接忽略
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // 如果是调用prepareStatement、prepareCall、createStatement的方法,打印要执行的sql语句
      // 并返回prepareStatement的代理对象,让prepareStatement也具备日志能力,打印参数
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          // 打印日志
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        // 增强的PreparedStatement
        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);
    }
  }

}

PreparedStatementLogger:对 prepareStatement 对象增强,增强的点如下:

  • 增强 PreparedStatement 的 setxxx 方法将参数设置到 columnMap、columnNames、columnValues,为打印参数做好准备。
  • 增强 PreparedStatement 的 execute 相关方法,当方法执行时,通过动态代理打印参数,返回动态代理能力的 resultSet 。
  • 如果是查询,增强 PreparedStatement 的 getResultSet 方法,返回动态代理能力的resultSet,如果是更新,直接打印影响的行数。

贴出一部分代码如下:

public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {

  private final PreparedStatement statement;

  /**
   * 1,增强PreparedStatement的setxxx方法将参数设置到columnMap、columnNames、columnValues,为打印参数做好准备
   * 2. 增强PreparedStatement的execute相关方法,当方法执行时,通过动态代理打印参数,返回动态代理能力的resultSet
   * 3. 如果是查询,增强PreparedStatement的getResultSet方法,返回动态代理能力的resultSet
   * 4. 如果是更新,直接打印影响的行数
   */
  @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 {
          return method.invoke(statement, params);
        }
      } else if (SET_METHODS.contains(method.getName())) {
        // 将参数设置到columnMap、columnNames、columnValues,为打印参数做好准备
        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())) {
        // 如果是更新,直接打印影响的行数
        int updateCount = (Integer) method.invoke(statement, params);
        if (updateCount != -1) {
          debug("   Updates: " + updateCount, false);
        }
        return updateCount;
      } else {
        return method.invoke(statement, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
}

ResultSetLogge:负责打印数据结果信息,贴出部分代码如下

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // 执行result.next方法,判断是否还有数据
      Object o = method.invoke(rs, params);
      if ("next".equals(method.getName())) {
        if ((Boolean) o) {
          // 如果还有数据,计数器rows加一
          rows++;
          if (isTraceEnabled()) {
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false;
              printColumnHeaders(rsmd, columnCount);
            }
            printColumnValues(columnCount);
          }
        } else {
          // 如果没有数据了,打印rows,打印查询出来的数据条数
          debug("     Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

上面说了这么多,都是日志功能的实现,那日志功能是怎么加入主体功能的?

既然在 Mybatis 中 Executor 才是访问数据库的组件,日志功能是在 Executor 中被嵌入的,具体代码在 org.apache.ibatis.executor.SimpleExecutor.prepareStatement(StatementHandler, Log) 方法中。

  // 创建Statement
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 获取connection对象的动态代理,添加日志能力;
    Connection connection = getConnection(statementLog);
    // 通过不同的StatementHandler,利用connection创建(prepare)Statement
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 使用parameterHandler处理占位符
    handler.parameterize(stmt);
    return stmt;
  }

都读到这里了,来个 点赞、评论、关注、收藏 吧!

文章作者:IT王小二
首发地址:www.itwxe.com/posts/75767…
版权声明:文章内容遵循 署名-非商业性使用-禁止演绎 4.0 国际 进行许可,转载请在文章页面明显位置给出作者与原文链接。