8. Mybatis插件体系

Mybatis插件体系

一、概述

到此为止,Mybatis的四大组件我们都已经介绍过了,下面通过时序图把各个组件的作用串联一下:

sequenceDiagram participant SqlSession participant Executor participant StatementHandler participant ParameterHandler participant ResultSetHandler participant JDBC as JDBC(Statement/ResultSet) Note over SqlSession,Executor: 1. 开启会话,获取执行器 SqlSession->>Executor: 调用query(statementId, params) Note over Executor: 2. 执行器负责整体调度(缓存/事务) Executor->>Executor: 检查一级缓存,未命中则继续 Executor->>StatementHandler: 创建StatementHandler(prepareStatement) Note over StatementHandler: 3. 负责SQL构建和执行 StatementHandler->>ParameterHandler: 调用setParameters(statement) Note over ParameterHandler: 4. 处理SQL参数(设置?占位符值) ParameterHandler->>JDBC: 给PreparedStatement设置参数 StatementHandler->>JDBC: 执行SQL(executeQuery/executeUpdate) JDBC->>StatementHandler: 返回结果(ResultSet/影响行数) StatementHandler->>ResultSetHandler: 调用handleResultSets(resultSet) Note over ResultSetHandler: 5. 处理结果集(映射为Java对象) ResultSetHandler->>Executor: 返回映射后的结果 Executor->>Executor: 将结果存入一级缓存 Executor->>SqlSession: 返回结果给用户

各组件核心作用

  • Executor:MyBatis 的执行核心,负责管理事务、缓存,调度StatementHandler执行 SQL。
  • StatementHandler:负责与 JDBC 交互,创建Statement、协调ParameterHandlerResultSetHandler设置SQL参数、执行 SQL、处理返回值。
  • ParameterHandler:负责将 Java 参数转换为 JDBC 参数,设置到 Statement 的占位符中。
  • ResultSetHandler:负责将 JDBC 的ResultSet转换为Java对象(如 POJO、List 等)。

二、MyBatis 插件体系及使用示例

1. 插件体系核心概念

Mybatis基于插件,提供了强大的扩展能力,允许在四大组件的方法执行前后插入自定义逻辑(如日志记录、性能监控、数据加密等)。实现Mybatis的插件,需要:

  1. 通过@Intercepts指定拦截四大组件中的哪个组件,通过@Signature注解指定拦截的方法。注意,Mybatis通过JDK的动态代理实现,只能拦截四大组件接口中声明的方法 (如Executor接口的query方法),不能拦截实现类的私有方法或非接口方法。
  2. 实现Interceptor接口,拦截组件方法,可以在方法前后实现自定义逻辑。
  3. 在xml配置文件中,配置插件。

2. 插件使用示例:SQL 执行日志插件

下面我们实现一个简单的打印 SQL 语句、参数和执行时间的例子。

(1)定义插件类

我们先看下Interceptor接口的定义:

java 复制代码
public interface Interceptor {
  // 核心方法:实现拦截逻辑,通过Invocation参数控制原方法执行
  Object intercept(Invocation invocation) throws Throwable;
  
  // 生成代理对象:默认调用Plugin.wrap
  default Object plugin(Object target){
      Plugin.wrap(target, this);
  }
  
  // 设置插件属性:读取配置文件中插件的参数
  default void setProperties(Properties properties){}
}

核心是要实现intercept方法,如果需要自定义配置,可以实现setProperties方法。

java 复制代码
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.plugin.*;
    import java.util.Properties;

    // 声明拦截Executor的query和update方法
    @Intercepts({
        @Signature(
            type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ),
        @Signature(
            type = Executor.class,
            method = "update",
            args = {MappedStatement.class, Object.class}
        )
    })
    public class SqlLogPlugin implements Interceptor {

        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            // 1. 获取拦截的方法和参数
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];
            String sql = ms.getBoundSql(parameter).getSql(); // 获取SQL语句

            // 2. 执行前:打印SQL和参数
            long startTime = System.currentTimeMillis();
            System.out.printf("执行SQL:%s,参数:%s%n", sql, parameter);

            // 3. 执行原方法(继续流程)
            Object result = invocation.proceed();

            // 4. 执行后:打印耗时
            long endTime = System.currentTimeMillis();
            System.out.printf("SQL执行完成,耗时:%dms%n", endTime - startTime);

            return result;
        }

        // 读取插件配置的属性(如在mybatis-config.xml中配置)
        @Override
        public void setProperties(Properties properties) {
            // 可读取配置的属性,如日志级别
            String logLevel = properties.getProperty("logLevel", "INFO");
            System.out.println("插件日志级别:" + logLevel);
        }
    }
(2)配置插件

mybatis-config.xml中注册插件:

xml 复制代码
    <configuration>
        <plugins>
            <plugin interceptor="com.example.SqlLogPlugin">
                <!-- 可选:配置插件属性 -->
                <property name="logLevel" value="DEBUG"/>
            </plugin>
        </plugins>
    </configuration>

注:插件执行顺序由配置顺序决定,形成责任链。

(3)效果

执行 SQL 时,控制台会输出:

sql 复制代码
    执行SQL:SELECT * FROM user WHERE id = ?,参数:1
    SQL执行完成,耗时:12ms

三、MyBatis 插件实现原理

MyBatis插件的核心是JDK动态代理 ,通过Plugin类实现拦截逻辑。

1. 核心类与接口

  • Interceptor:插件接口,自定义插件至少需要实现其intercept方法;如果需要设置属性,可以实现setProperties方法;一般不需要重新实现plugin(新版本有默认实现)。
  • Plugin实现InvocationHandler接口,是代理对象的实际处理器,负责调用拦截器和原方法。
  • Invocation:封装拦截方法的调用信息(目标对象、方法、参数),并提供proceed()方法继续执行原逻辑。

2. 插件初始化流程

当 MyBatis 启动时,会解析mybatis-config.xml中的<plugins>标签,将插件实例存入Configurationinterceptors列表中,这里不不去看源码了。

3. 代理对象创建(Plugin.wrap()

当四大组件(如 Executor)初始化时,MyBatis 会通过InterceptorChain.pluginAll()为其创建代理对象:

java 复制代码
    public Object pluginAll(Object target) {
      // 遍历所有配置的插件,对目标对象(四大组件)进行代理
      for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target); // 调用插件的plugin方法
      }
      return target;
    }

自定义插件的实现,默认是调用Plugin类的wrap方法实现:

java 复制代码
    public static Object wrap(Object target, Interceptor interceptor) {
      // 1. 获取插件要拦截的方法映射(type -> 方法集合)
      Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
      Class<?> type = target.getClass();
      // 2. 确定需要代理的接口,也就是注解生命的接口与目标对象的接口的交集
      Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
      if (interfaces.length > 0) {
        // 3. 创建JDK动态代理(Plugin为InvocationHandler)
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap)
        );
      }
      return target; // 无需代理则返回原对象
    }

4. 拦截逻辑执行(Plugin.invoke()

Plugin是动态代理接口InvocationHandler的实现类,当代理对象的方法被调用时,invoke方法会触发拦截逻辑:

java 复制代码
    // Plugin类的invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      try {
        // 1. 检查当前方法是否在插件的拦截列表中
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        if (methods != null && methods.contains(method)) {
          // 2. 若需要拦截,调用插件的intercept方法(传入Invocation)
          return interceptor.intercept(new Invocation(target, method, args));
        }
        // 3. 无需拦截,直接执行原方法
        return method.invoke(target, args);
      } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
      }
    }

    // Invocation类的proceed方法(继续执行原逻辑)
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
      return method.invoke(target, args); // 调用被代理对象的原方法
    }

Invocation处理方法和参数,还提供了继续执行的proceed方法:

java 复制代码
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
      return method.invoke(target, args); // 调用被代理对象的原方法
    }

关键说明

  • Invocation.proceed()是流程继续的核心:若target是原始对象,则执行原方法;若target是上一层代理,则触发该代理的invoke方法,形成链式调用;
  • 插件的intercept方法中,必须调用invocation.proceed(),否则会中断原方法执行(除非业务需要主动阻断)。
5. 责任链执行

若配置多个插件,MyBatis 会为组件创建多层代理 (每层代理对应一个插件)。调用方法时,代理会按插件配置顺序依次执行intercept方法,最终执行原方法。现在假设我们有两个插件A/B,那么执行流程为:

sequenceDiagram participant 调用者 participant 插件B代理 participant 插件A代理 participant 原始对象 调用者->>插件B代理: 调用方法 插件B代理->>插件B代理: 执行PluginB前置逻辑 插件B代理->>插件B代理: 调用proceed 插件B代理->>插件A代理: 触发调用 插件A代理->>插件A代理: 执行PluginA前置逻辑 插件A代理->>插件A代理: 调用proceed 插件A代理->>原始对象: 执行原方法 原始对象-->>插件A代理: 返回结果 插件A代理->>插件A代理: 执行PluginA后置逻辑 插件A代理-->>插件B代理: 返回结果 插件B代理->>插件B代理: 执行PluginB后置逻辑 插件B代理-->>调用者: 返回最终结果

说明:

  • 插件配置顺序:PluginA 先于 PluginB 配置(代理链:插件 B 代理 → 插件 A 代理 → 原始对象)
  • 执行顺序:
    1. 外层插件 B 先执行前置逻辑
    2. 调用 proceed 后进入内层插件 A
    3. 插件 A 执行前置逻辑并调用 proceed 进入原始对象
    4. 原始对象返回后,插件 A 先执行后置逻辑
    5. 最终插件 B 执行后置逻辑并返回结果

四、总结

MyBatis 插件机制基于 JDK 动态代理和责任链模式,通过Interceptor接口定义扩展点,Plugin类实现代理逻辑,InterceptorChain管理插件链,最终实现对四大组件的灵活拦截。其核心优势在于:

  • 低侵入性:无需修改框架源码,通过配置即可扩展功能;
  • 灵活性:支持单个插件拦截多个组件,或多个插件协同工作;
  • 精确性 :通过@Signature精确控制拦截的方法,避免不必要的性能损耗。

一些知名的Mybatis的扩展,都是通过插件实现的,比如

在实际使用中,需注意插件的执行顺序和性能影响,避免过度拦截导致的效率问题。理解插件的实现原理,不仅能更好地使用现有插件,还能根据业务需求开发自定义插件,充分发挥 MyBatis 的扩展能力。

相关推荐
舒一笑9 分钟前
如何优雅统计知识库文件个数与子集下不同文件夹文件个数
后端·mysql·程序员
IT果果日记10 分钟前
flink+dolphinscheduler+dinky打造自动化数仓平台
大数据·后端·flink
Java技术小馆22 分钟前
InheritableThreadLoca90%开发者踩过的坑
后端·面试·github
寒士obj31 分钟前
Spring容器Bean的创建流程
java·后端·spring
掉鱼的猫43 分钟前
Spring AOP 与 Solon AOP 有什么区别?
java·spring
不是光头 强1 小时前
axure chrome 浏览器插件的使用
java·chrome
笨蛋不要掉眼泪1 小时前
Spring Boot集成腾讯云人脸识别实现智能小区门禁系统
java·数据库·spring boot
桃源学社(接毕设)1 小时前
云计算下数据隐私保护系统的设计与实现(LW+源码+讲解+部署)
java·云计算·毕业设计·swing·隐私保护
数字人直播1 小时前
视频号数字人直播带货,青否数字人提供全套解决方案!
前端·javascript·后端
shark_chili2 小时前
提升Java开发效率的秘密武器:Jadx反编译工具详解
后端