org.apache.ibatis.logging包源码分析

啃下MyBatis源码系列目录

啃下MyBatis源码 - 为什么要看MyBatis源码及源码结构

啃下MyBatis源码 - org.apache.ibatis.logging包源码分析

啃下MyBatis源码 - org.apache.ibatis.datasource包源码分析

啃下MyBatis源码 - org.apache.ibatis.cache包源码分析

啃下MyBatis源码 - MyBatis核心流程三大阶段之初始化阶段

啃下MyBatis源码 - MyBatis核心流程三大阶段之代理阶段(binding模块分析)

啃下MyBatis源码 - MyBatis核心流程三大阶段之数据读写阶段

啃下MyBatis源码 - MyBatis面试题总结

--------------------------------------------------------------------------------------------------------------------------

啃下MyBatis源码 - org.apache.ibatis.logging包源码分析

1.MyBatis-logging日志模块源码解读

   1.1 MyBatis没有提供日志的实现类,怎么去对接拥有各自日志级别的第三方日志组件

   1.2 MyBatis怎么做到的自动日志扫描,并规定第三方日志组件加载顺序

   1.3 如何优雅的加载日志

--------------------------------------------------------------------------------------------------------------------------

1.MyBatis-logging日志模块源码解读

   1.1 MyBatis没有提供日志的实现类,怎么去对接拥有各自日志级别的第三方日志组件

       Mybatis没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的Log级别,且各不相同。而Mybatis统一提供了trace/ debug/ warn/ error四个级别,那么MyBatis究竟是怎么做到的呢?

       

       答案就在logging包中

       我们可以看到logging源码包内对应每种第三方日志都有一个对应的实现类,我们点进去看一下Jdk14LoggingImpl的源码

package org.apache.ibatis.logging.jdk14;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.ibatis.logging.Log;
public class Jdk14LoggingImpl implements Log {

  private final Logger log;
  ...
  @Override
  public void error(String s) {
    log.log(Level.SEVERE, s);
  }
  @Override
  public void debug(String s) {
    log.log(Level.FINE, s);
  }
  @Override
  public void trace(String s) {
    log.log(Level.FINER, s);
  }
  @Override
  public void warn(String s) {
    log.log(Level.WARNING, s);
  }
}

       可以看到有一个私有成员变量Logger,这个Logger来自java.util的包内,说白了就是来自JDK。那么MyBatis究竟是怎么实现把JDK中Level类中定义好的八九种日志级别,变成自己框架内4种日志级别的呢?这里就用到了适配器模式:将一个的接口转换成客户希望的另一个接口,起到连通两个不兼容接口之间桥梁的作用。我们可以看到Jdk14LoggingImpl这个类是实现的MyBatis自己的Log接口的,该类中的error()、debug()、trace()、warn()四种日志级别全部调用的log(JDK自己的日志级别),比如error对应JDK中的SEVERE级别,debug对应JDK中的FINE级别等。

1.2 MyBatis怎么做到的自动日志扫描,并规定第三方日志组件加载顺序

       答案在org.apache.ibatis.logging下的LogFactory类内,我们一起撸一下源码

public final class LogFactory {
  public static final String MARKER = "MYBATIS";
  //被选定的第三方日志组件适配器的构造器
  private static Constructor<? extends Log> logConstructor;
  //自动扫描日志实现,并规定第三方日志加载顺序为以下顺序
  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
      }
    }
  }
  //通过反射获取第三方日志的构造函数
  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);
    }
  }
}

       源码写的很清晰,LogFactory类实现了第三方日志的自动扫描,并且定义了第三方日志插件加载优先级,通过tryImplementation方法依次进行判断项目中引入的哪种第三方日志。

1.3 如何优雅的加载日志

       奥妙就在于org.apache.ibatis.logging.jdbc包内(理解这个问题之前需要先理解动态代理,具体可以看我另一篇博文对的JDK动态代理的分析:https://blog.csdn.net/qq_36756682/article/details/108256324)。

       先说下BaseJdbcLogger这个类,省略了一些不重要的代码

//所有日志增强的抽象基类
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<>();
  private final List<Object> columnNames = new ArrayList<>();
  private final List<Object> columnValues = new ArrayList<>();
  ...
}

       看完这个类我们再看下其他几个类的类图

ConnectionLogger:负责打印连接和SQL语句,并创建PrepareStatementLogger

PrepareStatementLogger:负责打印参数信息,并创建ResultSetLogger

ResultSetLogger:负责打印数据结果信息

       我们具体看下ConnectionLogger类的源码,重点在invoke方法打了注释的那几行

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  private final Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }
  //对连接的增强
  @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、prepareCall的方法,打印要执行的SQL
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          //打印SQL
          debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
        }
        //返回PreparedStatement代理对象,让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);
    }
  }
}

       看完invoke方法,我们大致了解了MyBatis是怎样打印自己日志的了,先创建具有打印日志功能的ConnectionLogger,再创建具有打印日志功能的PrepareStatementLogger,最后创建具有打印日志功能的ResultSetLogger,其原理都是通过动态代理的方式对原JDBC方法进行了日志业务的增强。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值