深入剖析MyBatis插件机制:从自定义插件到PageHelper分页原理(附源码)

关键词:MyBatis, 插件机制, Interceptor, 动态代理, PageHelper, 分页原理


在日常的开发工作中,你是否遇到过这样的需求:想要在 SQL 执行前后添加一些通用逻辑,比如分页、日志记录、性能监控、数据脱敏等?如果每次都修改业务代码,不仅重复劳动,还容易造成代码混乱。MyBatis 的插件机制正是为了解决这类问题的利器。本文将从自定义插件的实现讲起,深入剖析其底层原理(JDK 动态代理),并以 PageHelper 为例,带你彻底搞懂分页插件的工作机制。


📑 目录


一、MyBatis 插件简介

插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或者改变原有的功能。MyBatis 中也提供的有插件,虽然叫插件,但是实际上是通过**拦截器(Interceptor)**实现的。

在 MyBatis 的插件模块中涉及到两种重要的设计模式:

  1. 责任链模式:多个插件按顺序执行
  2. JDK 动态代理:实现方法的拦截和增强

这两种设计模式的技术知识是大家要掌握的,也是理解 MyBatis 插件机制的基础。


二、自定义插件实战

下面我们来看下如何实现一个自定义的插件。

2.1 创建 Interceptor 实现类

我们创建的拦截器必须要实现 Interceptor 接口,该接口的定义如下:

public interface Interceptor {

  // 执行拦截逻辑的方法
  Object intercept(Invocation invocation) throws Throwable;

  // 决定是否触发 intercept() 方法
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  // 根据配置初始化 Intercept 对象
  default void setProperties(Properties properties) {
    // NOP
  }

}

在 MyBatis 中,Interceptor 允许拦截的内容是:

目标对象可拦截的方法
Executorupdate, query, flushStatements, commit, rollback, getTransaction, close, isClosed
ParameterHandlergetParameterObject, setParameters
ResultSetHandlerhandleResultSets, handleOutputParameters
StatementHandlerprepare, parameterize, batch, update, query

我们创建一个拦截 Executor 中的 queryclose 方法的示例:

package com.boboedu.interceptor;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

/**
 * 自定义的拦截器
 * @Signature 注解表示一个方法签名,唯一确定一个方法
 */
@Intercepts({
        @Signature(
                type = Executor.class // 需要拦截的类型
                , method = "query"     // 需要拦截的方法
                // args 中指定被拦截方法的参数列表
                , args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ),
        @Signature(
                type = Executor.class
                , method = "close"
                , args = {boolean.class}
        )
})
public class FirstInterceptor implements Interceptor {

    private int testProp;

    /**
     * 执行拦截逻辑的方法
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("FirstInterceptor 拦截之前 ....");
        Object obj = invocation.proceed(); // 执行目标方法
        System.out.println("FirstInterceptor 拦截之后 ....");
        return obj;
    }

    /**
     * 决定是否触发 intercept 方法
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println("---->" + properties.get("testProp"));
    }

    // getter & setter
    public int getTestProp() {
        return testProp;
    }

    public void setTestProp(int testProp) {
        this.testProp = testProp;
    }
}

关键点说明

  • @Intercepts:声明这是一个拦截器
  • @Signature:定义要拦截的方法签名,包含 type(目标类)、method(方法名)、args(参数类型)
  • intercept():核心方法,编写拦截逻辑
  • invocation.proceed():调用目标方法,继续执行链

2.2 配置拦截器

创建好自定义的拦截器后,需要在 MyBatis 全局配置文件中注册:

<plugins>
    <plugin interceptor="com.bobo.interceptor.FirstInterceptor">
        <property name="testProp" value="1000"/>
    </plugin>
</plugins>

2.3 运行程序

执行查询操作进行测试:

@Test
public void test1() throws Exception {
    // 1. 获取配置文件
    InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
    // 2. 加载解析配置文件并获取 SqlSessionFactory 对象
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
    // 3. 根据 SqlSessionFactory 对象获取 SqlSession 对象
    SqlSession sqlSession = factory.openSession();
    // 4. 通过 SqlSession 中提供的 API 方法来操作数据库
    List<User> list = sqlSession.selectList("com.bobo.mapper.UserMapper.selectUserList");
    for (User user : list) {
        System.out.println(user);
    }
    // 5. 关闭会话
    sqlSession.close();
}

运行后,控制台会输出:

FirstInterceptor 拦截之前 ....
FirstInterceptor 拦截之后 ....

三、插件实现原理深度解析

自定义插件的步骤虽然简单,但背后的实现原理却很精妙。下面我们来深入分析。

3.1 初始化操作

首先,我们来看下全局配置文件加载解析时做了什么操作。

MyBatis 会解析 <plugins> 标签,对应的代码逻辑如下:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 获取 <plugin> 节点的 interceptor 属性的值
            String interceptor = child.getStringAttribute("interceptor");
            // 获取 <plugin> 下的所有的 properties 子节点
            Properties properties = child.getChildrenAsProperties();
            // 获取 Interceptor 对象
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor)
                    .getDeclaredConstructor().newInstance();
            // 设置 interceptor 的属性
            interceptorInstance.setProperties(properties);
            // Configuration 中记录 Interceptor
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

该方法用来解析全局配置文件中的 plugins 标签,然后创建对应的 Interceptor 对象,并封装属性信息。最后调用了 Configuration 对象中的方法:

public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

InterceptorChain 是拦截器链,负责管理所有的拦截器:

public class InterceptorChain {

    // 保存所有的 Interceptor
    private final List<Interceptor> interceptors = new ArrayList<>();

    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target); // 创建对应的拦截器的代理对象
        }
        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}

3.2 代理对象的创建

在解析时创建了对应的 Interceptor 对象并保存在 InterceptorChain 中,那么这个拦截器是如何和目标对象关联的呢?

可拦截的四个核心对象

  • Executor(SQL 执行器)
  • ParameterHandler(参数处理器)
  • ResultSetHandler(结果集处理器)
  • StatementHandler(SQL 语句处理器)

这些对象在创建时都会调用 pluginAll() 方法:

// Executor 创建时的代码示例
executor = (Executor) interceptorChain.pluginAll(executor);

// StatementHandler 创建时的代码示例
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);

进入 plugin() 方法,默认实现是:

default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

然后进入 Plugin 工具类的 wrap() 方法:

/**
 * 创建目标对象的代理对象
 * 目标对象:Executor、ParameterHandler、ResultSetHandler、StatementHandler
 */
public static Object wrap(Object target, Interceptor interceptor) {
    // 获取用户自定义 Interceptor 中 @Signature 注解的信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 获取目标类型
    Class<?> type = target.getClass();
    // 获取目标类型实现的所有接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 如果目标类型有实现的接口,就创建代理对象
    if (interfaces.length > 0) {
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
    }
    // 否则原封不动地返回目标对象
    return target;
}

Plugin 类的核心方法

public class Plugin implements InvocationHandler {

    private final Object target;           // 目标对象
    private final Interceptor interceptor; // 拦截器
    private final Map<Class<?>, Set<Method>> signatureMap; // 记录 @Signature 注解的信息

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 获取当前方法所在类或接口中,可被当前 Interceptor 拦截的方法
            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);
        }
    }
}

流程图解析

请求 -> Executor.query() -> Plugin.invoke() -> 
    是否需要拦截?
    ├── 是 -> FirstInterceptor.intercept() -> invocation.proceed() -> 目标方法
    └── 否 -> 直接执行目标方法

3.3 执行流程详解

以 Executor 的 query 方法为例,当查询请求到来时:

  1. 调用 executor.query() 方法(实际调用的是代理对象的方法)
  2. 触发 Plugin.invoke() 方法
  3. invoke() 中判断是否需要拦截
  4. 如果需要拦截,执行自定义的 intercept() 方法
  5. intercept() 中通过 invocation.proceed() 调用目标方法
  6. 返回结果

3.4 多拦截器执行顺序

如果配置了多个拦截器,执行顺序是怎样的呢?

配置顺序和执行顺序的关系

  • InterceptorChain 的 List 按照配置从上到下的顺序解析、添加
  • 创建代理时也是按照 List 的顺序代理
  • 执行时是从最后代理的对象开始(即配置顺序和执行顺序相反

例如,配置了两个拦截器:Interceptor1、Interceptor2

配置顺序:Interceptor1 -> Interceptor2
执行顺序:Interceptor2 -> Interceptor1

相关对象作用总结

对象作用
Interceptor自定义插件需要实现的接口
InterceptorChain配置的插件解析后保存在 Configuration 中
Plugin触发管理类,用于创建代理对象
Invocation对被代理类进行包装,可调用 proceed() 执行被拦截的方法

四、PageHelper 分页插件原理

PageHelper 是 MyBatis 中最常用的分页插件。我们来看看它的实现原理。

4.1 PageHelper 的基本使用

添加依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>4.1.6</version>
</dependency>

配置插件

<plugin interceptor="com.github.pagehelper.PageHelper">
    <property name="dialect" value="mysql" />
    <property name="offsetAsPageNum" value="true" />
    <property name="rowBoundsWithCount" value="true" />
    <property name="pageSizeZero" value="true" />
    <property name="reasonable" value="false" />
</plugin>

使用示例

// 设置分页参数
PageHelper.startPage(1, 5);
// 执行查询
List<User> list = userMapper.selectUserList();

就是这么简单!一行代码就实现了分页。

4.2 实现原理剖析

PageHelper 同样实现了 Interceptor 接口:

@Intercepts({
    @Signature(type = Executor.class, method = "query", ...)
})
public class PageHelper implements Interceptor {
    // ...
}

核心拦截逻辑

public Object intercept(Invocation invocation) throws Throwable {
    if (autoRuntimeDialect) { // 多数据源
        SqlUtil sqlUtil = getSqlUtil(invocation);
        return sqlUtil.processPage(invocation);
    } else { // 单数据源
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
}

SqlUtil 的作用

  • 数据库类型专用的 SQL 工具类
  • 一个数据库 URL 对应一个 SqlUtil 实例
  • 内部有一个 Parser 对象(如 MySQL 对应 MysqlParser)
  • 负责执行 count 查询、分页查询、保存 Page 对象等

Parser 创建

public static Parser newParser(Dialect dialect) {
    Parser parser = null;
    switch (dialect) {
        case mysql:
        case mariadb:
        case sqlite:
            parser = new MysqlParser();
            break;
        case oracle:
            parser = new OracleParser();
            break;
        case sqlserver:
            parser = new SqlServerParser();
            break;
        // ... 其他数据库方言
    }
    return parser;
}

分页 SQL 的生成

@Override
protected BoundSql getPageBoundSql(Object parameterObject) {
    String tempSql = sql;
    String orderBy = PageHelper.getOrderBy();
    if (orderBy != null) {
        tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
    }
    // 根据方言生成对应的分页 SQL
    tempSql = localParser.get().getPageSql(tempSql);
    return new BoundSql(configuration, tempSql, ...);
}

MySQL 最终生成的分页 SQL:

SELECT * FROM user LIMIT ?, ?

五、应用场景分析

MyBatis 插件可以应用于多种场景:

应用场景描述实现方式
水平分表按月度拆分费用表(如 fee_202001 ~ fee_202012)对 query/update 方法拦截,根据条件修改表名
数据脱敏手机号中间四位、身份证号出生日期脱敏对 query 方法拦截,对结果集脱敏处理
菜单权限控制不同用户展示不同菜单对 query 方法拦截,根据用户权限添加过滤条件
黑白名单禁止执行某些危险 SQL(如 like %%)对 Executor 的 update/query 方法拦截,检查 SQL
全局唯一 ID高并发环境下生成唯一 ID拦截 insert 方法,通过雪花算法生成 ID

六、总结

本文从自定义插件的实现入手,深入剖析了 MyBatis 插件机制的核心原理:

  1. 插件本质:通过 Interceptor 接口和 JDK 动态代理实现方法拦截
  2. 可拦截对象:Executor、ParameterHandler、ResultSetHandler、StatementHandler
  3. 执行流程:解析配置 → 创建拦截器 → 创建代理对象 → 调用时判断拦截 → 执行自定义逻辑
  4. 多拦截器顺序:配置顺序和执行顺序相反
  5. PageHelper 原理:通过拦截 Executor.query(),动态生成分页 SQL

使用建议

  • 合理使用插件,避免过多插件影响性能
  • 注意拦截器的执行顺序,避免逻辑冲突
  • 分页插件建议使用最新版本,支持更多数据库和特性

希望这篇文章能帮助你深入理解 MyBatis 的插件机制,在实际项目中灵活运用!


💡 温馨提示:如果文章对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区留言讨论。


参考资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加倍巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值