日志框架-2 Mybatis日志框架介绍

发布于:2022-12-02 ⋅ 阅读:(465) ⋅ 点赞:(0)

背景

本文作为《日志框架》专题的第二篇, 介绍Mybatis关于日志的实现,会涉及源码介绍。

Mybatis没有重复造轮子, 通过接入第三方日志组件实现日志功能, 且通过适配器使得日志实现随着接入的日志框架变化而变化;另外, 通过动态代理实现JDBC和Mybatis日志的合一, 方便用户使用Mybatis的日志。

1.Log接口和实现类

Mybatis中定义了自己的Log接口和其实现类,先看一下Log接口的内容:

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

如上所示,提供了trace、debug、warn、error四种记录日志的级别,同时提供了isTraceEnabledisDebugEnabled两个用于判断当前日志级别的接口。

日志框架-1 jul与log4j与logback介绍中介绍的日志实现,Log在Mybatis的logging包中都有对应的实现类:
![在这里
其中:StdOutImpl表示使用JDK的System.out和System.err进行日志输出,NoLoggingImpl表示不进行日志输出;其他Log实现类均通过适配模式,调用类名关联的日志框架从而实现日志功能, 这里以为JakartaCommonsLoggingImpl和Slf4jLoggerImpl为例进行介绍。
JakartaCommonsLoggingImpl源码如下:

package org.apache.ibatis.logging.commons;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JakartaCommonsLoggingImpl implements org.apache.ibatis.logging.Log {

  private final Log log;

  public JakartaCommonsLoggingImpl(String clazz) {
    log = LogFactory.getLog(clazz);
  }

  // 将所有方法委托给Log对象
  @Override public boolean isDebugEnabled() {return log.isDebugEnabled();}
  @Override public boolean isTraceEnabled() {return log.isTraceEnabled();}
  @Override public void error(String s, Throwable e) {log.error(s, e);}
  @Override public void error(String s) {log.error(s);}
  @Override public void debug(String s) {log.debug(s);}
  @Override public void trace(String s) {log.trace(s);}
  @Override public void warn(String s) {log.warn(s);}
}

JakartaCommonsLoggingImpl类结构比较简单,内部维持一个Log类对象,并将所有方法委托给这个对象实现;注意这里的Log是org.apache.commons.logging.Log类型,且由org.apache.commons.logging.LogFactory日志工厂生成。
Slf4jLoggerImpl源码如下:

package org.apache.ibatis.logging.slf4j;

import org.apache.ibatis.logging.Log;
import org.slf4j.Logger;

class Slf4jLoggerImpl implements Log {

  private final Logger log;

  public Slf4jLoggerImpl(Logger logger) {
    log = logger;
  }
  // 将所有方法委托给Log对象
  @Override public boolean isDebugEnabled() {return log.isDebugEnabled();}
  @Override public boolean isTraceEnabled() {return log.isTraceEnabled();}
  @Override public void error(String s, Throwable e) {log.error(s, e);}
  @Override public void error(String s) {log.error(s);}
  @Override public void debug(String s) {log.debug(s);}
  @Override public void trace(String s) {log.trace(s);}
  @Override public void warn(String s) {log.warn(s);}
}

可以看出Slf4jLoggerImpl关于日志接口的实现也是通过内部维持一个org.slf4j.Logger类对象,并将所有方法委托给改类实现。其他实现类除去再次封装和适配,实现原理基本相似。

2.LogFactory

如章节1中所述,Mybatis为Log接口提供了11种实现类,而没有介绍程序运行时具体使用哪种日志框架;LogFactory作为Mybatis的日志工厂,提供了一种选择策略:基于日志框架是否被引入。
在分析LogFactory功能前,先看一下Mybatis的pom.xml文件:
Mybatis的pom.xml文件中日志片段依赖:
在这里插入图片描述
由于JakartaCommonsLoggingImpl等Log实现类中引用了jcl, log4j以及slf4j等日志对象,因此Mybatis-x.x.x.jar包必须依赖日志框架,如上所示。
另外,mybatis对日志框架依赖项中的标签均为true, 表示依赖不会进行传递;即依赖mybatis的项目不会因此间接依赖日志框架。
分析LogFactory源码前,有必要先交代一下,Mybatis打印日志前LogFactory因没有使用而不会被加载;第一次打印日志时才会被加载进JVM, 加载时会先执行static代码块。

 类加载相关技术可以参考JVM-3 类加载机制(上)JVM-3 类加载机制(下)

LogFactory源码:

// 省去部分无关逻辑, 调整代码顺序,请忽略代码规范
public final class LogFactory {
// =========================第一部分:日志工厂API=========================
  private static Constructor<? extends Log> logConstructor;
  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);
    }
  }

// =========================第二部分:静态代码初始化过程=========================
  static {
  	// 框架为兼容jdk版本使用匿名类形式,这里为了阅读方便改成lambda表达式
    tryImplementation(() -> useSlf4jLogging());
    tryImplementation(() -> useCommonsLogging());
	tryImplementation(() -> useLog4J2Logging());
	tryImplementation(() -> useLog4JLogging());
	tryImplementation(() -> useJdkLogging());
	tryImplementation(() -> useNoLogging());
  }

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

LogFactory类按照功能可以被划分为两个部分:第一部分为本职工作,作为工厂以静态方法getLog对外提供获取日志对象的能力,内部通过反射调用logConstructor.newInstance(logger)创建日志对象;logConstructor对象的初始化工作在第二部分代码中实现。
第二部分代码执行发生在LogFactory类加载阶段,入口为static代码块:

  static {
  	// 框架为兼容jdk版本使用匿名类形式,这里为了阅读方便改成lambda表达式
    tryImplementation(() -> useSlf4jLogging());
    tryImplementation(() -> useCommonsLogging());
	tryImplementation(() -> useLog4J2Logging());
	tryImplementation(() -> useLog4JLogging());
	tryImplementation(() -> useJdkLogging());
	tryImplementation(() -> useNoLogging());
  }

tryImplementation(() -> useXxxLogging());只是对useXxxLogging()进行了一层包装:捕获useXxxLogging()方法抛出的异常并直接忽略; 且执行前会进行logConstructor属性的非空判断,为空才执行useXxxLogging(); 由此确定了优先级:Slf4j > CommonsLog > Log4J2 > Log4J > JdkLog > NoLog

useXxxLogging()方法内部逻辑相同,以最高优先级的useSlf4jLogging()为例进行介绍:

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

此时,如果项目没有引入Slf4j框架,会在加载org.apache.ibatis.logging.slf4j.Slf4jImpl类时(因加载其引用的属性类型org.slf4j.Logger失败)抛出NotClassDefFoundError异常, 异常被tryImplementation方法捕获并忽略,然后继续向下调用useCommonsLogging()尝试加载…直到找到一个可被使用的日志框架;其中,Mybatis提供的useNoLogging日志实现类作为保底方案。
如果此时项目引入了Slf4j依赖,则org.apache.ibatis.logging.slf4j.Slf4jImpl正常加载,然后调用setImplementation(Class<? extends Log> implClass)方法将Slf4jImpl的带String构造函数对象赋值给logConstructor属性,完成初始化工作。
另外,也可以直接在Mybatis的配置文件中通过指定日志实现类。

3.整合Mybatis日志与JDBC日志

Mybatis作为一个ORM框架,作用是进行数据库信息(入参、返回值)与Java对象的映射,并不直接参与数据库操作。数据库的读写操作由JDBC完成,且JDBC有一套自己的日志输入系统;因此无法通过Mybatis框架统一打印出映射过程的日志和数据库操作的日志。
这给用户定位问题带来了不便,因为Mybatis中的异常经常是由于数据库操作导致的,如查询异常导致的返回结果无法映射成Java对象;查询失败是参数的问题、SQL的问题还是返回结果的问题,这些通过JDBC的数据库操作日志可以快速得出结论。
Mybatis在logging包中增加了一个jdbc子包,通过代理解决上述问题:
在这里插入图片描述
包中仅有5个类,抽出公共部分形成BaseJdbclogger作为基类;ConnectionLoggerPreparedStatementLogger,ResultSetLogger,StatementLogger四个类对应不同的数据库操作场景且均实现了InvocationHandler接口,通过动态代理实现JDBC和Mybatis的日志合一,以下以ConnectionLogger为例进行介绍。
Mybatis获取connection对象涉及的源码:

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

通过事务对象获取Connection对象后,根据是否启用调试日志走不同的逻辑分支:未开启时,直接返回原Connection对象;否则通过ConnectionLogger.newInstance(connection, statementLog, queryStack)返回包装后的代理类。

跟进ConnectionLogger的newInstance方法:

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

因此,ConnectionLogger.newInstance方法返回一个Connection类对象(动态代理对象),且该对象的方法被调用时–调用会转发到ConnectionLogger这个InvocationHandler实现类的invoke方法中;动态代理部分内容请参考:JAVASE-14 静态代理与动态代理.

跟进ConnectionLogger的invoke方法:
在这里插入图片描述
如上所示,在调用Connection对象的prepareStatement或者prepareCall之前,可以打印出传递给sql或者存储过程的参数;即在代理Connection过程中对其进行增强,添加了日志打印功能。


网站公告

今日签到

点亮在社区的每一天
去签到