3. 日志模块(下)

news2024/11/16 17:47:19

在日志模块的上篇中,我们详细拆解了 MyBatis 是如何整合第三方日志框架,实现了完善的日志功能的。那么在本节中,我们再来具体分析下:为了实现“将日志功能优雅地嵌入到核心流程中,实现无侵入式地日志打印”这一目标,MyBatis 内部做了怎样的设计。

日志打印功能点

为了便于分析,我们先来回顾一下原生 JDBC 的执行流程。直接上代码:

/**
 * @author ZhangShenao
 * @date 2023/5/29 2:07 PM
 * Description 原生JDBC的使用方式
 */
public class JdbcDemo {
    public static void main(String[] args) throws Exception {
        //1. 注册数据库驱动/创建数据源DataSource
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2. 创建数据库连接Connection
        Connection conn = DriverManager.getConnection("xxx");

        //3. 创建执行语句Statement
        String sql = " select * from `user` ";
        PreparedStatement stmt = conn.prepareStatement(sql);

        //4. 执行SQL语句,获取结果集ResultSet
        ResultSet resultSet = stmt.executeQuery();

        //5. 解析ResultSet,获取业务对象
        List<User> users = new ArrayList<>();
        while (resultSet.next()) {
            long id = resultSet.getLong("id");
            String groupName = resultSet.getString("name");
            users.add(new User(id, groupName));
        }
        System.out.println("Users: " + users);

        //6. 释放资源对象
        resultSet.close();
        stmt.close();
        conn.close();
    }

    @Data
    @AllArgsConstructor
    @ToString
    private static class User {
        private long id;
        private String name;
    }
}

可以看到,一次典型的 JDBC 操作,会经历如下几个核心流程:

  1. 注册数据库驱动/创建数据源 DataSource
  2. 创建数据库连接 Connection
  3. 创建预编译执行语句 PreparedStatement
  4. 执行 SQL 语句,获取结果集 ResultSet
  5. 解析 ResultSet,获取业务对象;
  6. 释放资源。

在上述步骤中,可以认为最核心的需要打印日志的功能点为:

1. 创建 PrepareStatement 时:打印待执行的 SQL 语句;
2. 访问数据库时:打印实际参数的类型和值;
3. 查询出结果集后:打印结果行数及结果值。

Proxy Pattern 代理模式

需要打印日志的功能点已经明确了,接下来就是分析下怎么实现。总不能在每处直接logger.info() 吧?

在业务执行的主流程外,需要额外织入一些通用的增强逻辑,以实现对现有功能的扩展。这是典型的 Proxy Pattern 代理模式的适用场景。

按照惯例,我们来回顾一下代理模式的 UML 结构图:
Proxy Pattern
(图片来源:https://refactoring.guru/design-patterns/proxy)

对照代理模式,我们可以理所当然地想到:可以通过创建一个动态代理类,在 MyBatis 的核心执行流程之外,额外增加日志打印的功能。那么 MyBatis 具体是如何实现的呢?

MyBatis 日志增强器

我们来看下 MyBatis 日志增强器的类结构图:
日志增强器
**看到 InvocationHandler,大家肯定第一时间就能想到动态代理!**没错,这些日志增强器都是通过 JDK 原生动态代理的方式创建的代理类。下面具体介绍下每个类的功能:

BaseJdbcLogger

BaseJdbcLogger 是所有日志增强器的抽象父类,它用于记录 JDBC 那些需要增强的方法,并保存运行期间的 SQL 参数信息:

/**
 * 所有日志增强器的抽象父类,用于记录JDBC那些需要增强的方法,并保存运行期间的SQL参数信息
 */
public abstract class BaseJdbcLogger {
  //记录需要被增强的方法
  protected static final Set<String> SET_METHODS;
  protected static final Set<String> EXECUTE_METHODS = new HashSet<>();

  //记录运行期间的SQL参数相关信息
  private final Map<Object, Object> columnMap = new HashMap<>();

  private final List<Object> columnNames = new ArrayList<>();
  private final List<Object> columnValues = new ArrayList<>();

  //...省略非必要代码

  //在初始化时,记录所有需要被日志增强的JDBC方法
  static {
    //记录PreparedStatement中的setXXX()方法
    SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods())
        .filter(method -> method.getName().startsWith("set")).filter(method -> method.getParameterCount() > 1)
        .map(Method::getName).collect(Collectors.toSet());

    //记录executeXXX()方法
    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch");
  }

  //...省略非必要代码

  //通过Log完成日志打印
  protected boolean isDebugEnabled() {
    return statementLog.isDebugEnabled();
  }

  protected boolean isTraceEnabled() {
    return statementLog.isTraceEnabled();
  }

  protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
      statementLog.debug(prefix(input) + text);
    }
  }

  protected void trace(String text, boolean input) {
    if (statementLog.isTraceEnabled()) {
      statementLog.trace(prefix(input) + text);
    }
  }
  
  //...省略非必要代码

}

ConnectionLogger

ConnectionLogger:数据库连接的日志增强器,用于打印 PreparedStatement 相关参数,并通过动态代理方式,创建 StatementLoggerPreparedStatementLogger 两个日志增强器。

/**
 * 数据库连接的日志增强器
 */
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
  //底层维护JDCB Connection数据库连接对象
  private final Connection connection;

  //...省略非必要代码

  //方法拦截实现
  @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相关方法,创建PreparedStatementLogger日志增强器
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
        }
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        return PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      }

      //针对createStatement相关方法,创建StatementLogger日志增强器
      if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        return StatementLogger.newInstance(stmt, statementLog, queryStack);
      } else {
        //Connection自身的方法,正常执行
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //创建动态代理对象
  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);
  }

  //...省略非必要代码
}

PreparedStatementLogger

PreparedStatementLoggerStatementLogger 这两个增强器的功能类似,这里以更常用的 PreparedStatementLogger 为例,其主要功能为:

  1. 打印 JDBC PreparedStatement 中的动态参数信息;
  2. 拦截 setXXX() 方法,记录封装的参数;
  3. 创建 ResultSetLogger 日志增强器,使得对于结果集的操作具备日志打印的功能。
/**
 * PreparedStatement日志增强器
 */
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
  //底层维护JDBC PreparedStatement对象
  private final PreparedStatement statement;

  //...省略非必要代码

  //方法拦截实现
  @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);
      }

      //拦截executeXXX()方法,打印参数信息
      if (EXECUTE_METHODS.contains(method.getName())) {
        if (isDebugEnabled()) {
          debug("Parameters: " + getParameterValueString(), true);
        }
        clearColumnInfo();
        if ("executeQuery".equals(method.getName())) {
          ResultSet rs = (ResultSet) method.invoke(statement, params);
          return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
        } else {
          return method.invoke(statement, params);
        }
      }

      //拦截setXXX()方法,记录动态参数
      if (SET_METHODS.contains(method.getName())) {
        if ("setNull".equals(method.getName())) {
          setColumn(params[0], null);
        } else {
          setColumn(params[0], params[1]);
        }
        return method.invoke(statement, params);
      } else if ("getResultSet".equals(method.getName())) {
        //拦截getResultSet()方法,返回ResultSetLogger增强器
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      } else if ("getUpdateCount".equals(method.getName())) {
        //拦截getUpdateCount()方法,打印update操作影响的记录行数
        int updateCount = (Integer) method.invoke(statement, params);
        if (updateCount != -1) {
          debug("   Updates: " + updateCount, false);
        }
        return updateCount;
      } else {
        //PreparedStatement中的普通方法,直接调用
        return method.invoke(statement, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //创建动态代理对象
  public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    return (PreparedStatement) Proxy.newProxyInstance(cl,
        new Class[] { PreparedStatement.class, CallableStatement.class }, handler);
  }

  //...省略非必要代码
}

ResultSetLogger

最后一个日志增强器是 ResultSetLogger,它是结果集日志增强器,主要用于打印结果集的总记录数和每条记录的结果。

/**
 * 结果集日志增强器
 */
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {
  //底层维护JDBC ResultSet对象
  private final ResultSet rs;
  
  //...省略非必要代码


  //方法拦截实现
  @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);
      }
      Object o = method.invoke(rs, params);
      if ("next".equals(method.getName())) {
        if ((Boolean) o) {
          rows++;
          if (isTraceEnabled()) {
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false;
              //打印结果列头信息
              printColumnHeaders(rsmd, columnCount);
            }
            //打印结果列值
            printColumnValues(columnCount);
          }
        } else {
          //打印结果行数
          debug("     Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //打印结果列头信息
  private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
    StringJoiner row = new StringJoiner(", ", "   Columns: ", "");
    for (int i = 1; i <= columnCount; i++) {
      if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
        blobColumns.add(i);
      }
      row.add(rsmd.getColumnLabel(i));
    }
    trace(row.toString(), false);
  }

  //打印结果列值
  private void printColumnValues(int columnCount) {
    StringJoiner row = new StringJoiner(", ", "       Row: ", "");
    for (int i = 1; i <= columnCount; i++) {
      try {
        if (blobColumns.contains(i)) {
          row.add("<<BLOB>>");
        } else {
          row.add(rs.getString(i));
        }
      } catch (SQLException e) {
        // generally can't call getString() on a BLOB column
        row.add("<<Cannot Display>>");
      }
    }
    trace(row.toString(), false);
  }

  //创建代理对象
  public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
    InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
    ClassLoader cl = ResultSet.class.getClassLoader();
    return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
  }

 //...省略非必要代码

}

日志功能优雅嵌入

有了上面介绍的几个日志增强器,打印日志的功能是如何优雅地嵌入到 MyBatis 的核心执行流程中的呢?
在MyBatis 有个关键的组件 Executor,它是 MyBatis 的核心执行器接口,对于数据库的插入、查询等操作最终都是通过该接口来完成的。后面我们会有专门的篇幅来详细介绍 Executor 的内部实现,这里只需要看一下它创建数据库连接的方法:org.apache.ibatis.executor.BaseExecutor#getConnection()

//创建数据库连接
  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    //创建ConnectionLogger日志增强器,获取打印日志的功能
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    }
    return connection;
  }

可以看到,这里创建的实际上是 ConnectionLogger 这个日志增强器。这样一来,通过 BaseExecutor -> ConnectionLogger -> PreparedStatementLogger -> ResultSetLogger 的执行链路,类似多米诺骨牌方式,完成了日常增强器的创建过程。

小结

在日志模块中,我们首先对 MyBatis 的日志功能进行了需求分析,接下来探讨了 MyBatis 对第三方日志框架的整合方式,进而看到了 MyBatis 如何对 JDBC 原生的组件进行日志功能增强,最后了解了把日志功能优雅嵌入到核心执行流程的小技巧。日志这个功能虽然简单,但是 MyBatis 内部的实现用到了很多经典的设计模式,如适配器模式、动态代理模式等等,代码简洁且优雅,非常值得我们学习和借鉴。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/727736.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

菜谱APP项目实战,可以魔改任意APP——前后端齐全

菜谱APP开发实战&#xff08;可改任意APP&#xff09; 1. 优点 多平台性 由于此APP开发的时候采用的是 uni-app 来开发的&#xff0c;所以说它可以打包成多种形态&#xff0c;在各种平台上进行使用。比如&#xff1a;微信、支付宝等各种小程序。当然也是可以打包成安卓APP&am…

如何学习 Midjourney 绘画,AI绘图

Midjourney 是至今为止最好的 AI 绘图工具&#xff0c;SD还是差了很多。 要用当然用最好的&#xff0c;为了绘制出更符合心意的图&#xff0c;我开始 Midjourney 的学习。 从各种渠道寻找相关的资料&#xff0c;国内国外&#xff0c;或者星球&#xff0c;或者群聊&#xff0c…

简析住宅小区有序充电价格响应的电动汽车有充电策略

安科瑞电气股份有限公司 上海嘉定 201801 摘要&#xff1a;在住宅小区传统建设模式下&#xff0c;充电桩安装难、配套投资大&#xff0c;严重阻碍了充电桩在小区内进行普及使用。为解决该问题&#xff0c;本文首先调研了住宅小区内的电动汽车用户的出行习惯和充电特点&#xf…

SQL力扣练习(六)

目录 1. 部门工资前三高的所有员工(185) 题解一(dense_rank()窗口函数&#xff09; 题解二&#xff08;自定义函数&#xff09; 2.删除重复的电子邮箱(196) 题解一 题解二&#xff08;官方解析&#xff09; 3.上升的温度(197) 解法一&#xff08;DATEDIFF()&#xff09;…

java中地址问题

//第一个大mapMap<String, Object> map new HashMap<>();HashMap<String, String> map2 new HashMap<>();map2.put("358","999");//给小map赋值并将其添加到map中map.put("666",map2);//获取小map并且修改对应的键值对…

商城小程序有哪些优势?

伴随着移动互联网的高速发展&#xff0c;越来越多的实体商家开始转变营销思路&#xff0c;都纷纷开始布局线上市场&#xff0c;尤其是从小程序出现以后。今天新胜天下小编就来和大家聊一聊商城小程序有哪些优势。 1. 拥有众多流量入口 商城小程序本身就是小程序里的一种类型&a…

基于ArcGIS、ENVI、InVEST、FRAGSTATS等多技术融合提升环境、生态、水文、土地、土壤、农业、大气等领域的数据分析能力与项目科研水平研修

【科研团队必选】老师赋予目的不仅仅是技术的掌握&#xff0c;更能从技术融合与科研经验的视角下&#xff0c;培养科研团队科研素质&#xff0c;挖掘-融合-创新 目的&#xff1a;以科研及项目角度解决您的数据分析问题及热点问题&#xff0c;为您的论文写作及项目推进挖掘更好…

技术服务企业缺成本票,所得税高怎么解决?可有良策?

技术服务企业缺成本票&#xff0c;所得税高怎么解决&#xff1f;可有良策&#xff1f; 《税筹顾问》专注于园区招商、企业税务筹划&#xff0c;合理合规助力企业节税&#xff01; 技术服务型企业最核心的价值就是为客户提供技术支撑&#xff0c;而这类型的企业在税务方面面临的…

Observability:Synthetic monitoring - 合成监测入门(二)

在之前的文章 “Observability&#xff1a;Synthetic monitoring - 合成监测入门&#xff08;一&#xff09;” 里&#xff0c;我详细描述了如何使用 Project monitors 来创建监控器。我们可以通过在 terminal 中打入命令&#xff0c;创建最为基本的测试框架文件。我们可以通过…

python scrapy爬取网站数据(二)

用法很简单&#xff0c;先安装Scrapy&#xff0c;我这里是win10环境&#xff0c;py3.10 安装scrapy pip install Scrapy显示如图安装完毕 创建项目 分三步创建 scrapy stratproject spiderdemo #创建spiderdemo 项目&#xff0c;项目名随意取 cd spiderdemo #进入项目目录下…

【多线程初阶】第一次认识线程

多线程初阶系列目录 持续更新中 1.第一次认识线程 … 文章目录 多线程初阶系列目录前言1. 线程概念1.1 线程是什么1.2 为什么需要线程1.3 进程和线程的区别1.4 Java线程和操作系统线程的关系 2. 第一个Java多线程程序3. 创建线程的方法3.1 继承 Thread 类3.2 实现 Runnable 接…

深兰科技发布《深兰数字智能产业系列报告(2023年):个人数字化》

近日&#xff0c;深兰科技发布《深兰数字智能产业系列报告(2023年)&#xff1a;个人数字化》&#xff0c;这是深兰科技在数字智能产业领域发布的首份研究报告&#xff0c;也是国内第一份个人数字化产业报告。此报告是在上海市经济和信息化委员会和上海市产业技术创新促进会的共…

星辰天合公司产品完成阿里云 PolarDB 数据库产品生态集成认证

近日&#xff0c;XSKY星辰天合旗下产品与阿里云 PolarDB 开源云原生数据库展开产品集成认证测试&#xff0c;并获得阿里云颁发的产品生态集成认证证书。 测试结果表明&#xff0c;星辰天合旗下的融合计算管理平台 XHERE&#xff08;V2&#xff09;、统一数据平台 XEDP&#xf…

appuploder全过程使用教程(Windows版本)

appuploder全过程使用教程&#xff08;Windows版本&#xff09; 转载&#xff1a;使用appuploader工具流程&#xff08;Windows版本&#xff09; 一.登录apple官网&#xff0c;注册账号 1.注册苹果账号 Sign In - Apple 2.登录开发者中心 &#xff0c;出现协议弹框&#xf…

Android 画面显示流程一

DRM,英文全称 Direct Rendering Manager, 即 直接渲染管理器。 DRM是linux内核的一个子系统,提供一组API,用户空间程序,可以通过它,发送画面数据到GPU或者专用图形处理硬件,也可以使用它执行诸如配置分辨率,刷新率之类的设置操作。原本是设计提供PC设备支持复杂的图形设。…

浏览器配置环境

疯掉了 希望是最后一次 0.配置WinSCP和PUTTY 在Windows上使用PuTTY进行SSH连接-腾讯云开发者社区-腾讯云 1.配置conda 如何在Linux服务器上安装Anaconda&#xff08;超详细&#xff09;_linux安装anaconda_流年若逝的博客-CSDN博客 实验室远程登录Linux服务器并配置环境_远…

docker容器更改映射端口

问题描述&#xff1a; 我们在docker中开启nginx以后&#xff0c;如果这时候在nginx中配置除了80以外的端口的监听&#xff0c;会发现无法访问&#xff0c;这时候其实是因为我们没有开启端口映射导致的。 目前发现有两种解决方案 如下&#xff1a; 目录 一. 修改配…

Android AIDL基本使用

AIDL是Android多进程通讯方式一种。 如要使用 AIDL 创建绑定服务&#xff0c;请执行以下步骤&#xff1a; 创建 .aidl 文件 此文件定义带有方法签名的编程接口。 实现接口 Android SDK 工具会基于您的 .aidl 文件&#xff0c;使用 Java 编程语言生成接口。此接口拥有一个名为…

树莓派Pico与MicroSD卡模块接口及MicroPython编制读写MicroSD存储卡程序

介绍树莓派&#xff08;RPi&#xff09;Pico开发板(或树莓派Pico W无线开发板)与MicroSD卡模块SPI接口技术原理及SPI接口硬件连接的具体步骤&#xff0c;讲述采用MicroPython和SDCard类编制程序读写MicroSD存储卡的方法&#xff0c;给出读写MicroSD存储卡文件的测试程序。 一、…

创建数据库Market、Team,按要求完成指定操作

创建数据库Market&#xff0c;在Market中创建数据表customers&#xff0c;customers表结构如表4.6所示&#xff0c;按要求进行操作。 代码如下&#xff1a; #(1&#xff09;创建数据库Market mysql> create database Market; Query OK, 1 row affected (0.00 sec)mysql>…