Apache Commons Logging、Log4j、Log4j2、java.util.logging 等是 Java 开发中常用的几款日志框架,这些日志框架来源于不同的开源组织,给用户暴露的接口也有很多不同之处,所以很多开源框架会自己定义一套统一的日志接口,兼容上述第三方日志框架,供上层使用。

一般实现的方式是使用适配器模式,将各个第三方日志框架接口转换为框架内部自定义的日志接口。MyBatis 也提供了类似的实现。

适配器模式

适配器模式主要解决的是由于接口不能兼容而导致类无法使用的问题,这在处理遗留代码以及集成第三方框架的时候用得比较多。其核心原理是:通过组合的方式,将需要适配的类转换成使用者能够使用的接口。

适配器模式的类图如下所示:

适配器模式

在该类图中,我们可以看到适配器模式涉及的三个核心角色:

  • 目标接口 (Target):使用者能够直接使用的接口。以处理遗留代码为例,Target 就是最新定义的业务接口。
  • 需要适配的类/要使用的实现类 (Adaptee):定义了真正要执行的业务逻辑,但是其接口不能被使用者直接使用。这里依然以处理遗留代码为例,Adaptee 就是遗留业务实现,由于编写 Adaptee 的时候还没有定义 Target 接口,所以 Adaptee 无法实现 Target 接口。
  • 适配器 (Adapter):在实现 Target 接口的同时,维护了一个指向 Adaptee 对象的引用。Adapter 底层会依赖 Adaptee 的逻辑来实现 Target 接口的功能,这样就能够复用 Adaptee 类中的遗留逻辑来完成业务。

适配器模式带来的最大好处就是复用已有的逻辑,避免直接去修改 Adaptee 实现的接口,这符合“开放——封闭”原则(也就是程序要对扩展开放、对修改关闭)。

MyBatis 使用的日志接口是自己定义的 Log 接口,但是 Apache Commons Logging、Log4j、Log4j2 等日志框架提供给用户的都是自己的 Logger 接口。为了统一这些第三方日志框架,MyBatis 使用适配器模式添加了针对不同日志框架的 Adapter 实现,使得第三方日志框架的 Logger 接口转换成 MyBatis 中的 Log 接口,从而实现集成第三方日志框架打印日志的功能。

ibatis.logging

MyBatis 自定义的 Log 接口位于 org.apache.ibatis.logging 包中,相关的适配器也位于该包中,下面我们就来看看该模块的具体实现。

首先是 LogFactory 工厂类,它负责创建 Log 对象。这些 Log 接口的实现类中,就包含了多种第三方日志框架的适配器,如下图所示:

日志模块

LogFactory 类中有一段静态代码块,其中会依次加载各个第三方日志框架的适配器:

  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) {}
  }
}
  

在静态代码块执行的 tryImplementation() 方法中,首先会检测 logConstructor 字段是否为空:

  • 如果不为空,则表示已经成功确定当前使用的日志框架,直接返回;
  • 如果为空,则在当前线程中执行传入的 Runnable.run() 方法,尝试确定当前使用的日志框架。

以 JDK Logging 的加载流程(useJdkLogging() 方法)为例,其具体代码实现和注释如下:

  public static synchronized void useJdkLogging() {
  setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
private static void setImplementation(Class<? extends Log> implClass) {
  try {
    // 获取 implClass 这个适配器的构造方法
    Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
    // 尝试加载 implClass 这个适配器,加载失败会抛出异常
    Log log = candidate.newInstance(LogFactory.class.getName());
    if (log.isDebugEnabled()) {
      log.debug("Logging initialized using '" + implClass + "' adapter.");
    }
    // 加载成功,则更新 logConstructor 字段,记录适配器的构造方法
    logConstructor = candidate;
  } catch (Throwable t) {...}
}
  

下面我们以 Jdk14LoggingImpl 为例介绍一下 MyBatis Log 接口的实现。

Jdk14LoggingImpl 作为 Java Logging 的适配器,在实现 MyBatis Log 接口的同时,在内部还封装了一个 java.util.logging.Logger 对象(这是 JDK 提供的日志框架),如下图所示:

Jdk14LoggingImpl

Jdk14LoggingImplLog 接口的实现也比较简单,其中会将日志输出操作委托给底层封装的 java.util.logging.Logger 对象的相应方法,这与前文介绍的典型适配器模式的实现完全一致。Jdk14LoggingImpl 中的核心实现如下:

  public class Jdk14LoggingImpl implements Log {
  private final Logger log;
  public Jdk14LoggingImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }
  // 转换成目标 Logger 的调用方式
  @Override
  public void error(String s, Throwable e) {
      log.log(Level.SEVERE, s, e);
  }
  //...
}
  

ibatis.logging.jdbc

在 MyBatis 的 org.apache.ibatis.logging 包下面,除了集成三方日志框架的适配器实现之外,还有一个 jdbc 包,这个包的功能不是将日志写入数据库中,而是将数据库操作涉及的信息通过指定的 Log 打印到日志文件中。我们可以通过这个包,将执行的 SQL 语句、SQL 绑定的参数、SQL 执行之后影响的行数等信息,统统打印到日志中。

这个包基于 JDK 动态代理实现,主要用途是在测试环境进行调试,很少在线上开启,因为这会产生非常多的日志,拖慢系统性能。

BaseJdbcLogger

我们首先来看其中最基础的抽象类 —— BaseJdbcLogger。这个抽象类内部定义了 SET_METHODSEXECUTE_METHODS 两个 Set 类型的集合。其中,SET_METHODS 用于记录绑定 SQL 参数涉及的全部 set*() 方法名称,例如 setString() 方法、setInt() 方法等。EXECUTE_METHODS 用于记录执行 SQL 语句涉及的所有方法名称,例如 execute() 方法、executeUpdate() 方法、executeQuery() 方法、addBatch() 方法等。这两个集合都是在 BaseJdbcLogger 的静态代码块中被填充的:

  public abstract class BaseJdbcLogger {
  protected static final Set<String> SET_METHODS;
  protected static final Set<String> EXECUTE_METHODS = new HashSet<>();

  static {
    SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods())
      .filter(method -> method.getName().startsWith("set"))
      .filter(method -> method.getParameterCount() > 1)
      .map(Method::getName)
      .collect(Collectors.toSet());

    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch");
  }
}
  

BaseJdbcLogger 是 jdbc 包下其他 Logger 类的父类,继承关系如下图所示:

BaseJdbcLogger

从上面的 BaseJdbcLogger 继承关系图中可以看到,BaseJdbcLogger 的子类同时会实现 InvocationHandler 接口。

ConnectionLogger

我们先来看其中的 ConnectionLogger 实现,其底层维护了一个 java.sql.Connection 对象的引用,在 ConnectionLogger.newInstance() 方法中会使用 JDK 动态代理的方式为这个 Connection 对象创建相应的代理对象:

  public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
  private final Connection connection;
  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    // 调用 BaseJdbcLogger 构造方法,注册 logger
    super(statementLog, queryStack);
    // 注册 connection
    this.connection = conn;
  }
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    // 调用 ConnectionLogger 构造方法
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    // 创建 Connection 的代理对象
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }
}
  

invoke() 方法是 Connection 代理对象的核心方法。在该方法中,ConnectionLogger 会为 prepareStatement()prepareCall()createStatement() 三个方法添加代理逻辑,具体实现如下:

  @Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      // 如果调用的是从 Object 继承的方法,则直接调用,不做任何拦截
      return method.invoke(this, params);
    }
    // 调用 prepareStatement() 方法、prepareCall() 方法的时候,
    // 会在创建 PreparedStatement 对象之后,用 PreparedStatementLogger 为其创建代理对象
    if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
      // 执行方法体
      PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
      // 后置创建 PreparedStatement 的增强代理
      stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else if ("createStatement".equals(method.getName())) {
      // 调用 createStatement() 方法的时候,先执行方法体
      Statement stmt = (Statement) method.invoke(connection, params);
      // 后置创建 Statement 的增强代理
      stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else {
      // 除了上述三个方法之外,其他方法的调用将直接传递给底层 Connection 对象的相应方法处理
      return method.invoke(connection, params);
    }
  } catch (Throwable t) {...}
}
  
  • 该方法的核心就是为获取到的 PreparedStatementStatement 创建日志代理。

PreparedStatementLogger & StatementLogger

在上文我们创建了 PreparedStatementStatement 的日志代理后,我们再来看看这些日志增强代理的具体实现。我们以 PreparedStatementLogger 为例,在其 invoke() 方法中调用 SET_METHODS 集合中的方法、EXECUTE_METHODS 集合中的方法或 getResultSet() 方法时,会添加相应的日志增强逻辑:

  public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
  private final PreparedStatement statement;
  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        // 如果调用 Object 的方法,则直接调用,不做任何其他处理
        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);
          // 如果有结果集,则创建 ResultSet 的增强代理
          return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
        } else {
          return method.invoke(statement, params);
        }
      } else 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())) {
        // 执行方法体
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        // 如果有结果集,则创建 ResultSet 的增强代理
        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 ...
  }
}
  

PreparedStatementLogger 核心就是为返回的结果集 ResultSet 创建了增强代理,如此一来就能在获取结果集后完成相应的日志打印。

StatementLogger 中的 Invoke() 方法实现与之类似,这里就不再赘述。

ResultSetLogger

最后我们看下 ResultSetLoggerInvocationHandler 接口的实现,其中会针对 ResultSet.next() 方法进行后置处理,主要是打印结果集中每一行数据以及统计结果集总行数等信息,具体实现和注释如下:

  @Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      // 如果调用 Object 的方法,则直接调用,不做任何其他处理
      return method.invoke(this, params);
    }
    // 先执行方法本身
    Object o = method.invoke(rs, params);
    // 针对 ResultSet.next() 方法进行后置处理
    if ("next".equals(method.getName())) {
      if ((Boolean) o) {
        // 检测 next() 方法的返回值,确定是否还存在下一行数据
        rows++;
        // 记录 ResultSet 中的行数
        if (isTraceEnabled()) {
          // 获取数据集的列元数据
          ResultSetMetaData rsmd = rs.getMetaData();
          // 获取数据集的列数
          final int columnCount = rsmd.getColumnCount();
          if (first) {
            // 如果是数据集的第一行数据,会输出表头信息
            first = false;
            // 这里除了输出表头,还会记录 BLOB 等超大类型的列名
            printColumnHeaders(rsmd, columnCount);
          }
          // 输出当前遍历的这行记录,这里会过滤掉超大类型列的数据,不进行输出
          printColumnValues(columnCount);
        }
      } else {
        // 完成结果集的遍历之后,这里会在日志中输出总行数
        debug("     Total: " + rows, false);
      }
    }
    // 清空 column *集合
    clearColumnInfo();
    return o;
  } // catch ...
}
  

该代理是整个 JDBC Logger 代理体系最终生成日志的地方,即在遍历 ResultSet 时,打印出详细的日志。