mybatis插件

MyBatis 插件(Interceptor)是 MyBatis 框架留给开发者的一扇"后门"。通过它,你可以在不修改 MyBatis 核心代码的情况下,干预或增强 MyBatis 的执行过程

它的设计非常巧妙,接下来我从"它能做什么"、"它的核心原理"和"一个生动的比喻"三个方面,帮你彻底搞懂它。


🚀 它能做什么?(典型应用场景)

MyBatis 插件在真实项目中应用广泛,常见的有:

应用场景 具体实现
SQL 日志打印 拦截 SQL 执行,打印完整的 SQL 语句、参数和耗时,方便开发和调试。
数据权限控制 在 SQL 执行前自动拼接 WHERE 条件(如 WHERE tenant_id = ?),实现多租户数据隔离。
分页查询 拦截 Executorquery 方法,自动改写 SQL,在末尾拼接 LIMITOFFSET,这是 PageHelper 等分页插件的实现原理。
SQL 性能监控 统计 SQL 的执行时间,当超过设定的慢查询阈值时,自动记录日志或发送告警。
加密/解密字段 在 SQL 执行前对敏感字段(如手机号、身份证)进行加密,执行后对结果集进行解密。
动态替换表名 在 SQL 执行前替换表名,常用于分表场景(如将 order 替换为 order_202607)。

🔧 它的核心原理(基于"责任链 + 动态代理")

MyBatis 插件的底层原理是 JDK 动态代理。它的执行逻辑可以概括为四个步骤:

  1. 允许拦截的核心对象 :MyBatis 只允许对 4 大核心接口 的方法进行拦截:

    • Executor:执行器,负责 SQL 的整体执行。
    • StatementHandler:处理器,负责 JDBC 语句的创建和参数设置。
    • ParameterHandler:参数处理器,负责将参数设置到 SQL 中。
    • ResultSetHandler:结果集处理器,负责将查询结果映射成 Java 对象。
  2. 创建代理对象 :在 MyBatis 启动时,会解析你配置的插件。当创建上述 4 大核心对象时,MyBatis 会检查是否有插件需要拦截该对象。如果有,就会通过 JDK 动态代理 生成一个代理对象,并将原对象作为目标对象传入。

  3. 形成责任链 :如果多个插件都拦截了同一个对象,MyBatis 会层层嵌套地创建代理,形成一个责任链。调用时,会从最外层代理开始,依次向内执行。

  4. 执行拦截逻辑 :当你调用代理对象的方法时(如执行 SQL 的 query 方法),会先执行插件中 intercept 方法里的代码。在这里,你可以:

    • 获取或修改传入的 SQL、参数。
    • 执行 invocation.proceed(),继续执行原方法(或后续插件)。
    • 修改或包装返回的结果。

🏠 一个生动的比喻:小区的"安防系统"

假设你住在一个有严格安防的小区(MyBatis),外卖员(用户请求)想通过大门(4 大核心对象)进入。

  • 小区门口有一个保安亭(插件)
  • 保安(Interceptor)会检查外卖员的证件(SQL 语句)物品(参数)
  • 保安可以根据规定放行(执行 SQL),或拒绝(抛出异常),甚至可以对物品进行登记(记录日志)替换(改写 SQL)

这个"保安"就是 MyBatis 插件,它在大门(核心方法)入口处,对进出的所有活动进行控制。


🛠️ 简单使用步骤

  1. 创建类并实现 Interceptor 接口
  2. 使用 @Intercepts 注解,指定要拦截的目标接口和方法。
  3. intercept 方法中 ,通过 Invocation 参数获取和修改 SQL、参数等信息,然后调用 invocation.proceed() 继续执行。
  4. 配置插件 :在 MyBatis 配置文件 (mybatis-config.xml) 中,通过 <plugin> 标签注册插件,或在 Spring Boot 中配置为 Bean。

⚠️ 注意事项

  • 不要过度使用:每个插件都会增加一层代理,过多插件会影响性能。
  • 方法签名敏感@Intercepts 中指定的方法签名必须精准匹配,否则插件不会生效。
  • 注意分页插件的顺序 :如果一个请求中同时有分页插件和数据权限插件,它们的执行顺序(在 mybatis-config.xml 中的配置顺序)会影响最终生成的 SQL。
  • 内部对象复杂 :操作 StatementHandlerResultSetHandler 时,涉及较多的内部对象,对 MyBatis 源码的熟悉程度要求较高。

如果想了解如何编写一个简单的 SQL 日志插件,或者想看看 PageHelper 分页插件的核心源码是怎么实现的,我们可以继续深入。

这四个核心组件是 MyBatis 的内部机制,通常我们不会直接实例化或调用它们 ,而是通过 MyBatis 的 API 间接使用。不过,理解它们的使用方式,对于编写插件、调试源码、深入理解 MyBatis 非常有帮助。

我分两部分来讲:正常使用方式插件开发中的使用方式


一、正常使用方式(通过 MyBatis API 间接调用)

作为普通开发者,我们不会直接 new Executor() 或调用 StatementHandler,而是通过 MyBatis 提供的 API 来使用它们。

1. Executor ------ 通过 SqlSession 使用

java 复制代码
// 方式1:通过 SqlSession 直接调用(最常用)
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectById(1);  // 内部会调用 Executor
}

// 方式2:SqlSession 的 selectOne/selectList 方法
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    User user = sqlSession.selectOne("com.example.mapper.UserMapper.selectById", 1);
}

2. StatementHandler ------ 通过 SqlSession 使用

StatementHandlerExecutor 内部使用的,我们不会直接调用它。但它负责的工作(创建 PreparedStatement、设置参数、执行 SQL)最终会通过 JDBC 完成。

3. ParameterHandler ------ 通过 SqlSession 使用

我们通过 Mapper 接口传入参数,MyBatis 内部会使用 ParameterHandler 来处理这些参数,但对我们来说是透明的。

4. ResultSetHandler ------ 通过 SqlSession 使用

当我们调用 Mapper 方法时,返回的 Java 对象就是 ResultSetHandler 处理完 ResultSet 后映射的结果。


二、插件开发中的使用方式(进阶)

这是最常用的场景。在自定义 MyBatis 插件(Interceptor)中,我们可以直接操作这四个组件的核心方法。

1. Executor 的使用:拦截查询,实现 SQL 审计

java 复制代码
@Intercepts({
    @Signature(type = Executor.class, method = "query", 
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SqlAuditInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取参数
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        
        // 2. 获取 SQL 信息
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql();
        
        // 3. 记录日志(审计)
        System.out.println("执行的 SQL:" + sql);
        System.out.println("参数:" + parameter);
        
        long startTime = System.currentTimeMillis();
        
        // 4. 继续执行原方法(调用下一个拦截器或真正的查询)
        Object result = invocation.proceed();
        
        long endTime = System.currentTimeMillis();
        System.out.println("执行耗时:" + (endTime - startTime) + "ms");
        
        return result;
    }
}

关键点

  • 通过 invocation.getArgs() 获取 MappedStatement 和参数。
  • 通过 BoundSql 获取完整的 SQL。
  • 调用 invocation.proceed() 让 MyBatis 继续执行。

2. StatementHandler 的使用:改写 SQL(如分页插件)

java 复制代码
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", 
               args = {Connection.class, Integer.class})
})
public class PageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取 StatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        
        // 2. 通过反射获取 BoundSql
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        
        // 3. 获取原始 SQL
        String originalSql = boundSql.getSql();
        
        // 4. 判断是否需要分页(通过 ThreadLocal 传递分页参数)
        PageParam pageParam = PageContext.get();
        if (pageParam != null) {
            // 5. 改写 SQL,拼接 LIMIT
            String newSql = originalSql + " LIMIT " + pageParam.getOffset() + ", " + pageParam.getPageSize();
            
            // 6. 通过反射修改 BoundSql 中的 SQL
            Field field = BoundSql.class.getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, newSql);
        }
        
        // 7. 继续执行
        return invocation.proceed();
    }
}

关键点

  • 通过 MetaObject 反射获取 BoundSql
  • 修改 BoundSql 中的 SQL 语句。
  • 利用 ThreadLocal 传递分页参数。

3. ParameterHandler 的使用:修改参数值

java 复制代码
@Intercepts({
    @Signature(type = ParameterHandler.class, method = "setParameters", 
               args = {PreparedStatement.class})
})
public class ParameterEncryptInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取 ParameterHandler 和 PreparedStatement
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
        
        // 2. 通过反射获取参数对象
        MetaObject metaObject = SystemMetaObject.forObject(parameterHandler);
        Object parameterObject = metaObject.getValue("parameterObject");
        
        // 3. 如果参数是手机号,进行加密
        if (parameterObject instanceof User) {
            User user = (User) parameterObject;
            String phone = user.getPhone();
            if (phone != null && phone.length() == 11) {
                // 加密手机号(示例:替换中间4位)
                user.setPhone(phone.substring(0, 3) + "****" + phone.substring(7));
            }
        }
        
        // 4. 继续执行(这里会调用 JDBC 的 setXxx 方法)
        return invocation.proceed();
    }
}

关键点

  • 通过反射获取 parameterObject(传入的参数对象)。
  • 修改参数值后,继续执行 setParameters,MyBatis 会用修改后的值设置到 PreparedStatement

4. ResultSetHandler 的使用:修改返回结果

java 复制代码
@Intercepts({
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", 
               args = {Statement.class})
})
public class ResultDecryptInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 继续执行原方法,获取结果
        Object result = invocation.proceed();
        
        // 2. 如果结果是 List,遍历解密
        if (result instanceof List) {
            List<?> list = (List<?>) result;
            for (Object obj : list) {
                if (obj instanceof User) {
                    User user = (User) obj;
                    String phone = user.getPhone();
                    if (phone != null && phone.contains("****")) {
                        // 解密(示例:这里简单还原,实际需要真正的解密算法)
                        user.setPhone(phone.replace("****", "1234"));
                    }
                }
            }
        }
        
        return result;
    }
}

关键点

  • 先调用 invocation.proceed() 获取结果,再修改结果。
  • 在返回之前,对结果集中的敏感字段进行解密。

三、四种组件的插件使用场景总结

组件 拦截方法 典型应用场景
Executor query(), update() SQL 审计、慢查询日志、全局限流
StatementHandler prepare(), parameterize(), query() 分页插件(PageHelper)、SQL 改写、表名替换
ParameterHandler setParameters() 参数加密、参数校验、参数转换
ResultSetHandler handleResultSets() 结果加密/解密、字段脱敏、结果转换

四、重要提示

  1. 不要在插件中频繁创建对象 :插件会拦截每次 SQL 执行,性能敏感,避免在 intercept 方法中做重操作。
  2. 记得调用 invocation.proceed():否则 MyBatis 不会继续执行,SQL 不会真正执行。
  3. 注意拦截顺序:多个插件会按配置顺序形成责任链,顺序会影响最终结果(如分页插件通常在 SQL 改写插件之前)。
  4. 反射有风险 :使用 MetaObject 或原生反射时,注意字段名可能在不同 MyBatis 版本中有变化。

相关推荐
扎Zn了老Fe2 天前
MyBatis-Plus必知必会:告别低效CRUD,高效开发持久层
后端·mybatis
醉城夜风~2 天前
MyBatis 基础CRUD全套实战学习笔记
笔记·学习·mybatis
万亿少女的梦1682 天前
基于微信小程序、Spring Boot与Vue3的智慧养老管理系统设计与实现
spring boot·redis·微信小程序·mybatis·vue3
4154113 天前
MyBatis-Plus + PostGIS 实战(四):GeoJSON 序列化与前端地图对接,全局 WKT + 局部 GeoJSON,两种格式优雅共存
java·mybatis·postgis·geojson
Full Stack Developme3 天前
MyBatis-Plus(MP)AST 抽象语法树 设计原理
java·tomcat·mybatis
han_hanker4 天前
mybatis-plus service层拥有的方法
mybatis
SQL-First布道者4 天前
⚡ Spring JDBC 完整体系 · 第 3 讲 · Spring JDBC 最小案例
java·spring boot·后端·spring·mybatis·spring jdbc
ruleslol5 天前
Mybatis11-不同的数据库连接池的配置
数据库·mybatis
干到60岁退休的码农5 天前
整合Mybatis-Plus分页插件并实现分页api方法
java·mybatis