Mybatis源码系列6-独秀日志模块

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Mybatis源码系列6-独秀日志模块

Mybatis本身不提供日志实现,而是兼容第三方日志框架,如:slf4J,commonsLoging,Log4J2,Log4J,JdkLog。为了兼容和使用第三方日志框架,Mybatis进行了优秀的设计。

Mybatis的日志模块可以用两个知识点概括

  • 适配器模式
  • 代理模式


1.日志


为了兼容第三方日志框架,Mybatis使用了适配器模式,并且使用适配器模式实现中的对象适配器


1.1适配器模式

对象适配器: 被适配者作为适配器的属性存在

  • Target目标接口:期望得到的接口,也是直接使用的接口
  • Adapter适配器: 将源接口转为目标接口
  • Adaptee被适配者:源接口
1.1.1目标接口

Mybatis 使用日志接口是Log。但是因为Mybatis 本身不实现日志,实际使用的是第三方的日志接口

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


1.1.2被适配者

第三方日志接口被Mybatis中适配器适配。

  • slf4j日志
  • log4j2日志
  • log4j日志
  • jdklog日志


1.1.3对象适配器
  • slf4j适配器:Slf4jLocationAwareLoggerImpl对应1.6版本以上的slf4j日志适配,Slf4jLoggerImpl对应1.6版本以下的slf4j适配器
  • Log4j2适配器:Log4j2AbstractLoggerImpl 适配器,Log4j2LoggerImpl适配器
  • log4j适配器:Log4jImpl 适配器
  • JDK适配器 : Jdk14LoggingImpl

以log4j的适配器Log4jImpl 为例:被适配者作为适配器的属性存在

public class Log4jImpl implements Log {
  private static final String FQCN = Log4jImpl.class.getName();
  private Logger log;//持有被适配器对象的引用
}


1.2日志优先级

在mybatis中,使用工厂模式来创建日志对象。这样日志的优先级问题也在LogFactory 工厂中处理。

public final class LogFactory {
  public static final String MARKER = "MYBATIS";
  private static Constructor<? extends Log> logConstructor;
//按优先级
  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();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useJdkLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        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);
    }
  }

优先级就是在静态代码块中指定的。先加载slf4J,如果成功,则构造器logConstructor不为空,那么后续加载的时候发现构造器不为空,后续的其他日志不再设置。这样就实现了优先级

优先级顺序:

slf4j > common logging > log4j2 > log4j > jdk logging > 没有日志


2.日志的使用


Mybatis使用日志分为两种方式


2.1普通使用

所谓普通使用,就是从日志工厂获取一个日志对象然后调用日志打印方法打印

public abstract class BaseExecutor implements Executor {
  private static final Log log = LogFactory.getLog(BaseExecutor.class);
  public void close(boolean forceRollback) {
    try {
    } catch (SQLException e) {
    //普通使用
      log.warn("Unexpected exception on closing transaction.  Cause: " + e);
    } finally {
    }
  }
}


2.2代理模式

Mybatis使用日志最多的形式,就是代理模式。

基本方法是:

给需要加日志功能的组件,创建代理对象,并用日志增强器对其进行增强

日志增强器:


image.png


image.png


他们对JDBC的几个核心类进行的动态代理增强,使其具有日志打印功能

  • ConnectionLogger: 连接日志增强器
  • PreparedStatementLogger : PreparedStatement 日志增强器
  • ResultSetLogger : 结果集日志增强器
  • StatementLogger : Statement日志增强器

我们以ConnectionLogger为例来看看其原理

/*
BaseExecutor 
*/
public abstract class BaseExecutor implements Executor {
//获取连接
protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {//如果是日志级别是debug模式则创建代理对象。
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }
}
/*
ConnectionLogger 
*/
public final class ConnectionLogger extends BaseJdbcLogger implements 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);
  }
//增强
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }    
      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);
    }
  }
}

开启debug模式时,获取的Connection对象是代理对象。当执行其方法时, 会走ConnectionLogger#invoke方法,然后会根据方法名,进行日志的打印。

这样通过代理模式优雅的把日志打印功能加入到JDBC重要组件中,为我们排查问题提供了参考。


总结


Mybatis在日志的设计上可谓是精彩干练。

  • 通过适配器模式接入主流的日志框架,兼容万象。
  • 通过动态代理模式对JDBC核心类进行日志功能的增强,让它具备日志打印的能力。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
17天前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
20 3
Golang语言之Prometheus的日志模块使用案例
|
1月前
|
XML Java 数据库连接
mybatis源码研究、搭建mybatis源码运行的环境
这篇文章详细介绍了如何搭建MyBatis源码运行的环境,包括创建Maven项目、导入源码、添加代码、Debug运行研究源码,并提供了解决常见问题的方法和链接到搭建好的环境。
mybatis源码研究、搭建mybatis源码运行的环境
|
29天前
|
Go 开发者
【应用服务 App Service】App Service发生错误请求时,如何查看IIS Freb日志,从中得知错误所发生的模块,请求中所携带的Header信息
【应用服务 App Service】App Service发生错误请求时,如何查看IIS Freb日志,从中得知错误所发生的模块,请求中所携带的Header信息
|
1月前
|
数据挖掘 语音技术
3D-Speaker说话人任务的开源项目问题之语义说话人信息模块在说话人日志系统中的问题如何解决
3D-Speaker说话人任务的开源项目问题之语义说话人信息模块在说话人日志系统中的问题如何解决
|
1月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
1月前
|
SQL Java 关系型数据库
SpringBoot 系列之 MyBatis输出SQL日志
这篇文章介绍了如何在SpringBoot项目中通过MyBatis配置输出SQL日志,具体方法是在`application.yml`或`application.properties`中设置MyBatis的日志实现为`org.apache.ibatis.logging.stdout.StdOutImpl`来直接在控制台打印SQL日志。
SpringBoot 系列之 MyBatis输出SQL日志
|
1月前
|
供应链 前端开发 Java
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
该博客文章介绍了一个使用Mybatis、Layui、MVC和JSP技术栈开发的服装库存管理系统,包括注册登录、权限管理、用户和货号管理、库存管理等功能,并提供了源码下载链接。
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
|
1月前
|
存储 监控 Java
|
1月前
|
Java 数据库连接 数据库
后端框架的学习----mybatis框架(6、日志)
这篇文章介绍了如何在MyBatis框架中使用日志功能,包括配置MyBatis的日志实现、使用log4j作为日志工具,以及如何通过配置文件控制日志级别和输出格式。
|
1月前
|
缓存 Java 数据库连接
我要手撕mybatis源码
该文章深入分析了MyBatis框架的初始化和数据读写阶段的源码,详细阐述了MyBatis如何通过配置文件解析、建立数据库连接、映射接口绑定、动态代理、查询缓存和结果集处理等步骤实现ORM功能,以及与传统JDBC编程相比的优势。
我要手撕mybatis源码