专栏系列:MyBatis源码深度复盘全套教程

前面五篇我们吃透了 MyBatis 架构、初始化、动态代理、SQL执行全流程、缓存机制。

本篇我们来搞定 MyBatis 最核心的拓展机制——插件拦截机制

日常开发中我们常用的 PageHelper分页、数据权限、租户隔离、SQL日志打印、字段加密解密 功能,全部基于 MyBatis 插件实现。

很多同学只会引入插件依赖、配置使用,却完全不懂底层原理,面试经常被问懵:

  • MyBatis插件可以拦截哪些对象?

  • 插件多层拦截执行顺序是什么?

  • PageHelper为什么只需一行代码就能实现分页?

  • MyBatis插件底层是动态代理还是拦截器链?

  • 如何手写一个自定义MyBatis插件?

本篇逐行源码拆解插件核心原理,吃透责任链拦截机制、代理嵌套逻辑,彻底掌握MyBatis拓展核心!


一、插件机制核心概述

1.1 插件的核心作用

MyBatis 提供插件拦截机制,允许开发者在不修改源码的前提下,对核心执行流程进行增强、改造,实现各类通用拓展功能,符合开闭原则

1.2 四大可拦截核心对象(面试必背)

MyBatis 仅允许拦截四大核心组件,所有插件功能都是基于这四个对象拓展:

  • Executor(执行器):拦截SQL执行、缓存操作、事务操作(粒度最大)

  • StatementHandler(语句处理器):拦截SQL预处理、SQL执行(最常用,PageHelper基于此实现)

  • ParameterHandler(参数处理器):拦截参数赋值、参数加密解密

  • ResultSetHandler(结果集处理器):拦截结果集封装、数据脱敏、字段转换

重点:无法拦截 Mapper、SqlSession,仅能拦截以上四大底层执行对象


二、插件核心底层原理总览

MyBatis 插件底层依托两大设计模式实现:

  1. 动态代理模式:为四大核心对象生成代理对象,拦截方法执行

  2. 责任链模式:多个插件层层嵌套拦截,有序执行

2.1 核心执行流程

项目启动加载插件 → 为目标对象生成多层代理 → 执行方法时逐层拦截 → 执行自定义拓展逻辑 → 执行原生方法

2.2 核心类介绍

  • Interceptor:插件拦截器顶级接口,自定义插件必须实现

  • InterceptorChain:拦截器链,存储所有注册的插件

  • Plugin:代理生成工具类,负责创建代理对象

  • Invocation:方法调用封装对象,保存目标方法、参数、目标对象


三、源码逐行拆解插件加载与代理生成

3.1 插件加载时机

MyBatis 在初始化阶段就会加载所有自定义插件,解析 plugins 配置,存入拦截器链,对应初始化核心方法 pluginElement()

所有插件在项目启动时完成注册,运行时直接执行,无需重复加载。

3.2 插件代理生成核心源码

四大核心对象创建时,都会调用 Plugin.wrap() 方法,生成代理对象,核心源码如下:

public static Object wrap(Object target, Interceptor interceptor) {
  // 获取当前插件需要拦截的对象类型和方法
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  Class<?> type = target.getClass();
  // 获取目标对象需要被拦截的接口
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  // 如果有需要拦截的接口,生成JDK动态代理对象
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  // 无需拦截,返回原生对象
  return target;
}

核心结论:MyBatis 插件底层依然是 JDK动态代理,只拦截指定接口的指定方法。

3.3 Plugin.invoke 拦截核心

代理对象生成后,目标对象任意方法执行都会触发 invoke 方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // 判断当前方法是否需要被拦截
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
      // 执行自定义插件的拦截逻辑
      return interceptor.intercept(new Invocation(target, method, args));
    }
    // 无需拦截,执行原生方法
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

这就是插件实现方法拦截的核心入口。


四、Interceptor拦截器接口详解

自定义插件必须实现 Interceptor 接口,包含三个核心方法:

public interface Interceptor {
  // 核心:拦截目标方法,执行自定义逻辑
  Object intercept(Invocation invocation) throws Throwable;

  // 包装目标对象,生成代理
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  // 读取插件配置参数
  default void setProperties(Properties properties) {
    // 可读取xml中配置的自定义参数
  }
}

4.1 Invocation执行链核心方法

Invocation 封装了目标执行信息,核心方法 proceed()放行当前拦截,执行下一个插件或原生方法

这是责任链可以层层执行的关键!


五、多层插件执行顺序(面试高频)

5.1 插件嵌套原理

如果配置多个插件,MyBatis 会生成多层嵌套代理对象,类似洋葱结构。

5.2 执行顺序规则

  • 代理创建顺序:按照配置顺序,从上到下创建代理

  • 方法执行顺序:后创建的插件,先执行拦截逻辑

  • 放行执行顺序:先创建的插件,最后放行

通俗总结:先配置后执行,后配置先执行


六、PageHelper分页插件底层源码原理

结合本篇插件机制,彻底解密 PageHelper 核心实现,面试高频考点!

6.1 拦截对象

PageHelper 拦截 StatementHandler

6.2 核心实现逻辑

  1. 通过 PageHelper.startPage()本地线程 ThreadLocal 保存分页参数(页码、条数)

  2. SQL执行前,拦截 StatementHandler 的 prepare 或 query 方法

  3. 获取原生待执行SQL,动态拼接 limit 分页语句

  4. 执行修改后的分页SQL,完成分页查询

  5. 查询总条数,封装分页结果 PageInfo

  6. 清空 ThreadLocal 分页参数,防止线程污染

6.3 核心亮点

基于 ThreadLocal 线程隔离 + SQL动态改写,无侵入、无需改业务代码,这就是 PageHelper 极简使用的底层真相。


七、手写自定义MyBatis插件实战(可直接复用)

手把手实现一个SQL执行耗时统计插件,实战掌握插件开发!

7.1 自定义拦截器代码

public interface Interceptor {
  // 核心:拦截目标方法,执行自定义逻辑
  Object intercept(Invocation invocation) throws Throwable;

  // 包装目标对象,生成代理
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  // 读取插件配置参数
  default void setProperties(Properties properties) {
    // 可读取xml中配置的自定义参数
  }
}

7.2 配置插件

<plugins>
    <plugin interceptor="com.xxx.SqlCostInterceptor"/>
</plugins>

启动项目执行SQL,即可自动打印每条SQL的执行耗时,插件开发就是这么简单!


八、面试高频真题解答

Q1:MyBatis插件可以拦截哪些对象?

MyBatis插件仅支持拦截四大核心对象:Executor执行器、StatementHandler语句处理器、ParameterHandler参数处理器、ResultSetHandler结果集处理器,可分别实现SQL执行拦截、SQL改写、参数处理、结果封装拓展功能。

Q2:MyBatis插件底层原理?

基于JDK动态代理+责任链模式实现,项目启动加载所有插件,为四大核心目标对象生成嵌套代理对象;方法执行时被插件拦截,执行自定义拓展逻辑,通过invocation.proceed()放行,实现多层插件有序执行。

Q3:多个插件执行顺序是什么?

插件按照配置顺序从上到下加载创建代理,执行时遵循后加载先执行、先加载后执行的规则,形成洋葱式嵌套执行结构。

Q4:PageHelper分页原理?

PageHelper自定义插件拦截StatementHandler,通过ThreadLocal存储分页参数,SQL执行前动态拼接limit语句改写原生SQL,执行分页查询并统计总条数,最终封装分页结果,同时清空线程参数避免污染。


九、本篇核心总结

1. MyBatis插件只能拦截四大核心底层对象,不支持拦截Mapper、SqlSession。

2. 底层核心:JDK动态代理 + 责任链模式,实现无侵入拓展。

3. 多层插件为嵌套代理,执行顺序遵循后配置先执行。

4. PageHelper核心是拦截StatementHandler + 动态改写SQL + ThreadLocal线程隔离

5. 自定义插件实现简单,可快速实现SQL监控、数据脱敏、租户隔离等通用功能。


下篇预告

下一篇我们深度拆解 MyBatis事务机制与批量操作源码

✅ MyBatis事务提交、回滚底层逻辑

✅ BATCH批量执行器性能优化原理

✅ 批量操作常见坑点与解决方案

持续更新MyBatis源码全套教程,点赞收藏不迷路!

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐