Mybatis源码解析之模块解析:日志模块

介绍

mybatis没有提供日志的实现类,需要接入第三方的日志组件

从LogFactory类的静态代码块中可以看到第三方日志加载优先级如下 slf4j > apache commons log > log4j2 > log4j > jul

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4J2Logging();
      }
    });
	
	// 省略了其他模块的加载逻辑
  }

从LogFactory类名可以看出用到了工厂模式,对工厂模式不了解的可以看如下文章

工厂模式(静态工厂模式、工厂方法模式、抽象工厂模式)详解

但是第三方日志都有各自的log级别,mybatis用适配器模式统一提供了trace,debug,warn,error四个级别,

org.apache.ibatis.logging.Log接口有多个实现类,实现类即Mybatis提供的适配器
,例如Log4jImpl,Log4j2Impl等,一种实现类提供一个适配器,UML类图如下

在这里插入图片描述

上图中Logger对象是org.apache.log4j.Logger,即通过Log4jImpl的适配将对Log接口的操作转化为Logger对象的操作

适配器模式设计的几个角色如下

  1. 目标接口(Target):调用者能够直接使用的接口,即Log接口
  2. 需要适配的类(Adaptee):Adaptee类中有真正的逻辑,但是不能被调用者直接使用,即Logger对象
  3. 适配器(Adapter):实现了Target接口,包装了Adaptee对象

当然我们在项目中一般也不配置Mybatis适配后的Logger对象,因为级别实在是太少了,INFO级别都没有

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
public static final Log log = LogFactory.getLog(Test.class);

直接配置slf4j的即可

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public static final Logger logger = LoggerFactory.getLogger(Test.class);

之所有要适配,是因为JDBC模块也会打印日志,但是它没有和特定的日志框架绑定,用的是Mybatis适配后的Log对象

JDBC调试,打印不同类型的日志

在mybatis源码logging模块下有一个jdbc包,当日志级别设置为DEBUG级别时它通过动态代理的方式输出很多实用信息,如输出SQL语句,用户传入的绑定参数,SQL语句影响的行数等信息

你可能会想,为什么要通过动态代理来打印debug级别的日志呢?用log.debug()不就行了,主要还是为了避免日志逻辑和正常逻辑耦合到一块

BaseJdbcLogger是一个抽象类,它是jdbc包下其他Logger类的父类,继承关系如下图
在这里插入图片描述
ConnectionLogger:负责打印连接信息和SQL语句,并创建PreparedStatementLogger
PreparedStatementLogger:负责打印参数信息,并创建ResultSetLogger
StatementLogger:负责打印参数信息,并创建ResultSetLogger
ResultSetLogger:负责打印数据结果信息

4个类实现的思路是一样的,这里只分析一下ConnectionLogger

当日志级别为debug时,返回的是被代理后的Connection对象,否则就是正常的Connection对象

  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

ConnectionLogger对象实现了InvocationHandler接口,返回了代理后的Connection对象

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

具体的拦截逻辑如下

  @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,createStatement的方法,打印要执行的SQL语句
      // 并返回PreparedStatement的代理对象,让PreparedStatement也具备日志能力,打印参数
      if ("prepareStatement".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 ("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);
    }
  }

可以看到对3个方法进行了增强prepareStatement,prepareCall,createStatement

如果是调用prepareStatement,prepareCall的方法,打印要执行的SQL语句
3个方法都会返回PreparedStatement的代理对象,让PreparedStatement也具备日志能力,打印参数

看看最后打印出的DEBUG日志

DEBUG 2020-02-24 18:09:13,647 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==>  Preparing: select id, `name`, phone from author 
DEBUG 2020-02-24 18:09:13,766 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: 
DEBUG 2020-02-24 18:09:13,813 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==      Total: 1

是不是看着很熟悉,就是用动态代理实现的

欢迎关注

在这里插入图片描述

发布了385 篇原创文章 · 获赞 1471 · 访问量 90万+

猜你喜欢

转载自blog.csdn.net/zzti_erlie/article/details/104398887