Mybatis基础模块-日志管理

文章目录

  • [1. 适配器模式](#1. 适配器模式)
  • [2. Log](#2. Log)
    • [2.1 默认实现StdOutImpl](#2.1 默认实现StdOutImpl)
    • [2.2 Log4jImpl](#2.2 Log4jImpl)
  • [3. LogFactory](#3. LogFactory)
  • [4. 解析配置和应用](#4. 解析配置和应用)
    • [4.1 settings配置](#4.1 settings配置)
    • [4.2 解析](#4.2 解析)
  • [5. jdbc日志](#5. jdbc日志)
    • [5. 1 类图](#5. 1 类图)
    • [5.2 BaseJdbcLogger](#5.2 BaseJdbcLogger)
    • [5.3 ConnectionLogger](#5.3 ConnectionLogger)
    • [5.4 ConnectionLogger的具体应用](#5.4 ConnectionLogger的具体应用)

1. 适配器模式

适配器使接口不兼容的对象可以相互合作。

Java的日志框架有很多,Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等,接口不尽相同,Mybatis为了统一匹配这些框架,使用到了适配器模式。

2. Log

Mybatis自定义了日志接口:org.apache.ibatis.logging.Log。

java 复制代码
/**
 * @author Clinton Begin
 */
public interface Log {

  boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

以下为其类图:

上图的实现相当于adapter。

2.1 默认实现StdOutImpl

java 复制代码
public class StdOutImpl implements Log {

  public StdOutImpl(String clazz) {
    // Do Nothing
  }

  @Override
  public boolean isDebugEnabled() {
    return true;
  }

  @Override
  public boolean isTraceEnabled() {
    return true;
  }

  @Override
  public void error(String s, Throwable e) {
    System.err.println(s);
    e.printStackTrace(System.err);
  }

  @Override
  public void error(String s) {
    System.err.println(s);
  }

  @Override
  public void debug(String s) {
    System.out.println(s);
  }

  @Override
  public void trace(String s) {
    System.out.println(s);
  }

  @Override
  public void warn(String s) {
    System.out.println(s);
  }
}

这个实现很简单,就是控制台把日志打印出来。

2.2 Log4jImpl

为了匹配Log4j,该类持有org.apache.log4j.Logger,Logger就是上图adaptee,被匹配的接口。

java 复制代码
/**
 * @author Eduardo Macarron
 */
public class Log4jImpl implements Log {

  private static final String FQCN = Log4jImpl.class.getName();

  private final Logger log;

  public Log4jImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.log(FQCN, Level.ERROR, s, e);
  }

  @Override
  public void error(String s) {
    log.log(FQCN, Level.ERROR, s, null);
  }

  @Override
  public void debug(String s) {
    log.log(FQCN, Level.DEBUG, s, null);
  }

  @Override
  public void trace(String s) {
    log.log(FQCN, Level.TRACE, s, null);
  }

  @Override
  public void warn(String s) {
    log.log(FQCN, Level.WARN, s, null);
  }

}

3. LogFactory

LogFactory使用工厂模式,创建各类适配器。 该类用final修饰,不可继承,同时构造方法是私有的,不能通过new方法创建,像是一个工具类。

java 复制代码
/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public final class LogFactory {

  /**
   * Marker to be used by logging implementations that support markers.
   */
  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  static {
    // 按序加载对应的日志组件,从上往下加载,上面的成功了,下面的就不会在加载了
    /**
     * tryImplementation(LogFactory::useSlf4jLogging); 等价于
     * tryImplementation(new Runnable(){
     *   void run(){
     *     useSlf4jLogging();
     *   }
     * })
     */
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }

  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }

  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }

  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }

  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }

  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  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 字段
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

}

4. 解析配置和应用

问题:mybatis是如何选择日志框架的。

4.1 settings配置

配置值包括:SLF4J , LOG4J(deprecated since 3.5.9) , LOG4J2 , JDK_LOGGING |,COMMONS_LOGGING , STDOUT_LOGGING , NO_LOGGING。

配置的值是如何来的?

在配置对象Configuration的构造方法里面。

4.2 解析

XMLConfigBuilder的方法:loadCustomLogImpl().

java 复制代码
  private void loadCustomLogImpl(Properties props) {
    // 获取 logImpl设置的 日志 类型
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    // 设置日志  这块代码是我们后面分析 日志 模块的 关键代码
    configuration.setLogImpl(logImpl);
  }

继续:

java 复制代码
  public void setLogImpl(Class<? extends Log> logImpl) {
    if (logImpl != null) {
      this.logImpl = logImpl; // 记录日志的类型
      // 设置 适配选择
      LogFactory.useCustomLogging(this.logImpl);
    }
  }

继续:

java 复制代码
  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

继续:

java 复制代码
  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 字段
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

5. jdbc日志

有了以上的类,那mybatis是如何打印日志的?

Mybatis通过JDK动态代理的方式,将JDBC操作通过指定的日志框架打印出来。

5. 1 类图

5.2 BaseJdbcLogger

BaseJdbcLogger是一个基础的抽象类。基本的属性:

java 复制代码
  // 记录 PreparedStatement 接口中定义的常用的set*() 方法
  protected static final Set<String> SET_METHODS;
  // 记录了 Statement 接口和 PreparedStatement 接口中与执行SQL语句有关的方法
  protected static final Set<String> EXECUTE_METHODS = new HashSet<>();

  // 记录了PreparedStatement.set*() 方法设置的键值对
  private final Map<Object, Object> columnMap = new HashMap<>();
  // 记录了PreparedStatement.set*() 方法设置的键 key
  private final List<Object> columnNames = new ArrayList<>();
  // 记录了PreparedStatement.set*() 方法设置的值 Value
  private final List<Object> columnValues = new ArrayList<>();

  protected final Log statementLog;// 用于日志输出的Log对象
  protected final int queryStack;  // 记录了SQL的层数,用于格式化输出SQL

5.3 ConnectionLogger

java 复制代码
/**
 * Connection proxy to add logging.
 *
 * @author Clinton Begin
 * @author Eduardo Macarron
 *
 */
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  // 真正的Connection对象
  private final Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }

  /**
   * Connection 是一个数据库连接对象
   *            通过 Connection 的下一步是创建 Statement 对象
   *            Statement包含对应的子类 PreparedStatement
   *
   *            日志记录  Connection 创建 Statement 的过程
   *            同时会创建 Statement 的代理对象类增强 Statement
   *
   * @param proxy
   * @param method
   * @param params
   * @return
   * @throws Throwable
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      // 如果是调用从Object继承过来的方法,就直接调用 toString,hashCode,equals等
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      // 如果调用的是 prepareStatement方法
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        // 创建  PreparedStatement
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        // 然后创建 PreparedStatement 的代理对象 增强
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
        // 同上
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }
        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);
    }
  }

  /**
   * Creates a logging version of a connection.
   *
   * @param conn - the original connection
   * @return - the connection with logging
   */
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    // 创建了 Connection的 代理对象 目的是 增强 Connection对象 给他添加了日志功能
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

  /**
   * return the wrapped connection.
   *
   * @return the connection
   */
  public Connection getConnection() {
    return connection;
  }

}

5.4 ConnectionLogger的具体应用

在实际处理的时候,日志模块是如何工作的?

  • 执行sql之前先要获取Statement.
    SimpleExecutor的方法doQuery
java 复制代码
  /**
   * 到了 具体的数据库操作的步骤了  JDBC
   *     Connection
   *     Statement
   *     PreparedStatement
   * @param ms
   * @param parameter
   * @param rowBounds
   * @param resultHandler
   * @param boundSql
   * @param <E>
   * @return
   * @throws SQLException
   */
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 注意,已经来到SQL处理的关键对象 StatementHandler >>  同时会完成  parameterHandler和resultSetHandler的实例化
      // 默认创建的是 PreparedStatementHandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 获取一个 Statement对象  对占位符处理
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 执行查询
      return handler.query(stmt, resultHandler);
    } finally {
      // 用完就关闭
      closeStatement(stmt);
    }
  }
  • 继续看prepareStatement
java 复制代码
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 放开了日志 会创建 Connection 的代理对象
    Connection connection = getConnection(statementLog);
    // 获取 Statement 对象 ==》 PreparedStatement对象 Statement  如果放开了日志 则会创建 Statement 对应的代理对象
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 为 Statement 设置参数  如果是PreparedStatement处理则会对 对应的占位符赋值
    handler.parameterize(stmt);
    return stmt;
  }
  • 继续看getConnection方法,
java 复制代码
 protected Connection getConnection(Log statementLog) throws SQLException {
    // 获取到了真正的 Connection 对象 ? 如果有连接池管理 在此处获取的是PooledConnection 是Connection的代理对象
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      // 创建Connection的日志jdk代理对象
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      // 返回的是真正的Connection 没有走代理的方式
      return connection;
    }
  }
  • 再看 handler.prepare(connection, transaction.getTimeout()), 进入到instantiateStatement方法
java 复制代码
  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        // connection 是 日志的代理对象
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        // 在执行 prepareStatement 方法的时候会进入进入到ConnectionLogger的invoker方法中
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }
  • 执行查询handler.query(stmt, resultHandler);
相关推荐
sunnyday04262 小时前
Mybatis-Plus updateById 方法更新无效及空值处理
java·开发语言·mybatis
新手小袁_J6 小时前
java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigur
java·开发语言·spring·spring cloud·bootstrap·maven·mybatis
鹿屿二向箔14 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架搭建一个病人跟踪信息管理系统
spring·mvc·mybatis
鹿屿二向箔17 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架构建一个图书馆仓储管理系统
spring·mvc·mybatis
积极向上的Elbert18 小时前
Mybatis-Plus中的Page方法出现Records的值大于0但是total的值一直是0
java·开发语言·mybatis
bohu8319 小时前
快速搭建springcloud 3.X+mybatis+nacos本地项目
spring cloud·nacos·mybatis
听见~2 天前
MyBatisPlus
mybatis
向阳12182 天前
mybatis SqlSessionFactory
java·mybatis
Suwg2092 天前
《手写Mybatis渐进式源码实践》实践笔记(第七章 SQL执行器的创建和使用)
java·数据库·笔记·后端·sql·mybatis·模板方法模式
秋恬意2 天前
MyBatis动态 SQL 的执行原理
mybatis