首页
/ MyBatis插件开发完全指南:从原理到实践的6个关键步骤

MyBatis插件开发完全指南:从原理到实践的6个关键步骤

2026-04-02 09:22:36作者:史锋燃Gardner

一、原理剖析:MyBatis插件机制的底层逻辑

痛点直击

在不修改MyBatis源码的情况下,如何实现SQL执行监控、参数加密等自定义功能?为什么有些拦截器配置后没有生效?

MyBatis插件机制基于Java动态代理[一种在运行时创建代理对象的技术]实现,允许开发者在四大核心组件的方法执行前后插入自定义逻辑。这四大核心组件包括:

  • Executor:执行器,负责SQL语句的执行调度
  • ParameterHandler:参数处理器,处理SQL参数的设置
  • ResultSetHandler:结果集处理器,处理查询结果的映射
  • StatementHandler:语句处理器,负责与数据库交互执行SQL

核心接口解析

插件开发的核心是实现Interceptor接口,该接口定义了三个关键方法:

public interface Interceptor {
  // 拦截逻辑的实现
  Object intercept(Invocation invocation) throws Throwable;
  
  // 决定是否为目标对象创建代理
  Object plugin(Object target);
  
  // 设置插件属性
  void setProperties(Properties properties);
}

[!TIP] MyBatis使用JDK动态代理实现插件功能,只有实现了接口的类才能被代理。因此插件只能拦截接口中的方法。

插件执行流程

  1. MyBatis启动时解析配置文件中的插件
  2. 为目标组件创建代理对象
  3. 方法执行时触发代理逻辑
  4. 执行拦截器的intercept方法
  5. 通过Invocation.proceed()调用原始方法

MyBatis插件执行流程

实践检验清单

  • [ ] 能准确说出MyBatis四大可拦截组件及其作用
  • [ ] 理解Interceptor接口三个方法的调用时机
  • [ ] 能描述插件从配置到执行的完整流程

二、场景驱动:插件应用的典型业务场景

痛点直击

哪些业务场景最适合使用MyBatis插件?如何判断一个需求是否应该通过插件实现?

MyBatis插件适用于需要对SQL执行过程进行统一处理的场景,以下是几个典型应用场景:

1. SQL性能监控

通过拦截StatementHandler的query和update方法,记录SQL执行时间,识别慢查询:

@Intercepts({
  @Signature(type = StatementHandler.class, method = "query", 
             args = {Statement.class, ResultHandler.class}),
  @Signature(type = StatementHandler.class, method = "update", 
             args = {Statement.class})
})
public class SqlPerformanceMonitor implements Interceptor {
  private long slowQueryThreshold = 1000; // 慢查询阈值,单位:毫秒
  
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    long startTime = System.currentTimeMillis();
    try {
      return invocation.proceed();
    } finally {
      long executionTime = System.currentTimeMillis() - startTime;
      if (executionTime > slowQueryThreshold) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        log.warn("慢查询预警: {} ms - SQL: {}", executionTime, sql);
      }
    }
  }
  
  // 其他方法实现...
}

2. 数据权限控制

通过拦截Executor的query方法,动态添加数据权限过滤条件:

@Intercepts({
  @Signature(type = Executor.class, method = "query", 
             args = {MappedStatement.class, Object.class, 
                     RowBounds.class, ResultHandler.class})
})
public class DataPermissionPlugin implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // 获取当前用户信息
    UserInfo currentUser = SecurityContext.getCurrentUser();
    
    // 获取原始SQL信息
    MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
    Object parameter = invocation.getArgs()[1];
    
    // 根据用户角色动态修改SQL,添加数据权限过滤
    if (needDataPermission(ms.getId())) {
      BoundSql boundSql = ms.getBoundSql(parameter);
      String sql = boundSql.getSql();
      String newSql = addDataPermissionSql(sql, currentUser);
      
      // 创建新的BoundSql对象
      BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql,
                                        boundSql.getParameterMappings(), 
                                        boundSql.getParameterObject());
      
      // 修改MappedStatement
      MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
      invocation.getArgs()[0] = newMs;
    }
    
    return invocation.proceed();
  }
  
  // 其他方法实现...
}

3. SQL审计日志

通过拦截Executor组件,记录所有执行的SQL语句及其参数,用于审计和问题排查:

@Intercepts({
  @Signature(type = Executor.class, method = "update", 
             args = {MappedStatement.class, Object.class}),
  @Signature(type = Executor.class, method = "query", 
             args = {MappedStatement.class, Object.class, 
                     RowBounds.class, ResultHandler.class})
})
public class SqlAuditPlugin implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
    Object parameter = invocation.getArgs()[1];
    
    // 记录SQL执行信息
    SqlAuditLog log = new SqlAuditLog();
    log.setSqlId(ms.getId());
    log.setSql(ms.getBoundSql(parameter).getSql());
    log.setParameters(parameter);
    log.setOperator(SecurityContext.getCurrentUser().getUsername());
    log.setExecuteTime(new Date());
    
    try {
      Object result = invocation.proceed();
      log.setSuccess(true);
      return result;
    } catch (Exception e) {
      log.setSuccess(false);
      log.setErrorMessage(e.getMessage());
      throw e;
    } finally {
      // 异步保存审计日志
      auditLogService.saveAsync(log);
    }
  }
  
  // 其他方法实现...
}

[!WARNING] 插件会影响MyBatis的性能,尤其是在高频执行的SQL场景下。审计日志建议采用异步方式保存。

知识衔接

了解了插件的典型应用场景后,接下来我们将学习如何解决插件开发中常见的问题和挑战。

实践检验清单

  • [ ] 能根据业务需求选择合适的拦截目标组件
  • [ ] 掌握SQL语句和参数的获取方法
  • [ ] 了解如何安全地修改SQL执行参数

三、问题解决:插件开发的常见挑战与解决方案

痛点直击

拦截器配置后不生效?如何获取和修改SQL参数?多个插件的执行顺序如何控制?

1. 拦截器不生效的排查方法

拦截器配置后未生效是最常见的问题,可按以下步骤排查:

检查@Intercepts注解配置

确保注解配置正确,特别是method和args参数必须与目标接口完全一致:

// 正确示例
@Intercepts({
  @Signature(
    type = StatementHandler.class, 
    method = "prepare", 
    args = {Connection.class, Integer.class}
  )
})

// 错误示例:参数类型不匹配
@Intercepts({
  @Signature(
    type = StatementHandler.class, 
    method = "prepare", 
    args = {Connection.class, int.class} // int是基本类型,而接口定义是Integer
  )
})

验证插件注册配置

确保在mybatis-config.xml中正确注册了插件:

<plugins>
  <plugin interceptor="com.example.SqlMonitorPlugin">
    <!-- 插件属性配置 -->
    <property name="threshold" value="500"/>
  </plugin>
</plugins>

验证类路径和包名

确保拦截器类的全限定名正确,没有拼写错误。

2. 获取和修改SQL及参数

在拦截器中获取和修改SQL及参数是常见需求,以下是实现方法:

获取SQL和参数

// 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

// 获取BoundSql对象,包含SQL和参数信息
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql(); // 获取SQL语句
Object parameterObject = boundSql.getParameterObject(); // 获取参数对象
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 参数映射

修改SQL语句

// 创建新的SQL
String newSql = sql + " LIMIT 100"; // 示例:添加限制条件

// 创建新的BoundSql
BoundSql newBoundSql = new BoundSql(
  statementHandler.getConfiguration(), 
  newSql, 
  boundSql.getParameterMappings(), 
  boundSql.getParameterObject()
);

// 使用反射修改StatementHandler的delegate属性
Field delegateField = statementHandler.getClass().getDeclaredField("delegate");
delegateField.setAccessible(true);
StatementHandler delegate = (StatementHandler) delegateField.get(statementHandler);

Field boundSqlField = delegate.getClass().getDeclaredField("boundSql");
boundSqlField.setAccessible(true);
boundSqlField.set(delegate, newBoundSql);

[!TIP] MyBatis中的某些实现类(如RoutingStatementHandler)使用了装饰器模式,需要通过反射获取实际的代理对象。

3. 多插件执行顺序控制

当配置多个插件时,执行顺序由配置顺序决定:

<plugins>
  <!-- 先执行 -->
  <plugin interceptor="com.example.PluginA"/>
  <!-- 后执行 -->
  <plugin interceptor="com.example.PluginB"/>
</plugins>

上述配置中,PluginA的intercept方法会先于PluginB执行,而在方法返回时则相反(PluginB先返回,然后是PluginA)。

实践检验清单

  • [ ] 能独立排查拦截器不生效的问题
  • [ ] 掌握通过反射修改SQL和参数的方法
  • [ ] 理解多插件的执行顺序规则

四、进阶实践:插件开发的高级技巧与避坑指南

痛点直击

如何实现插件的灵活配置?如何避免插件开发中的性能问题?插件与MyBatis版本兼容性如何保证?

避坑指南:常见问题与解决方案

Q1: 如何在插件中获取MyBatis配置信息?

A: 通过Configuration对象可以获取MyBatis的所有配置信息:

@Override
public Object intercept(Invocation invocation) throws Throwable {
  // 从不同组件获取Configuration的方式
  if (invocation.getTarget() instanceof Executor) {
    Executor executor = (Executor) invocation.getTarget();
    Configuration configuration = executor.getConfiguration();
  } else if (invocation.getTarget() instanceof StatementHandler) {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    Configuration configuration = statementHandler.getConfiguration();
  }
  
  // 使用Configuration获取类型别名、类型处理器等信息
  TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  // ...
}

Q2: 插件如何处理分页查询?

A: 分页插件需要根据不同数据库类型生成不同的分页SQL:

public class PaginationPlugin implements Interceptor {
  private Dialect dialect; // 数据库方言接口
  
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // 判断是否需要分页
    if (isNeedPagination(invocation)) {
      MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
      Object parameter = invocation.getArgs()[1];
      RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
      
      // 如果不是默认分页,则处理分页逻辑
      if (rowBounds != RowBounds.DEFAULT) {
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql();
        
        // 根据数据库方言生成分页SQL
        String paginationSql = dialect.getPaginationSql(
          sql, rowBounds.getOffset(), rowBounds.getLimit());
        
        // 修改SQL并继续执行
        // ...
      }
    }
    return invocation.proceed();
  }
  
  // 其他方法实现...
}

Q3: 如何避免插件导致的性能问题?

A: 遵循以下原则可以避免性能问题:

  1. 减少不必要的拦截:只拦截需要的方法
  2. 简化拦截逻辑:避免在intercept方法中执行复杂操作
  3. 使用缓存:对重复计算的结果进行缓存
  4. 异步处理:非关键逻辑采用异步处理
// 性能优化示例:使用缓存存储反射字段
public class OptimizedPlugin implements Interceptor {
  // 缓存反射字段,避免重复反射操作
  private static final Map<Class<?>, Field> DELEGATE_FIELD_CACHE = new ConcurrentHashMap<>();
  
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    
    // 从缓存获取反射字段
    Field delegateField = DELEGATE_FIELD_CACHE.computeIfAbsent(
      statementHandler.getClass(), 
      clazz -> {
        try {
          Field field = clazz.getDeclaredField("delegate");
          field.setAccessible(true);
          return field;
        } catch (NoSuchFieldException e) {
          return null;
        }
      }
    );
    
    // ...
  }
}

Q4: 如何处理插件与MyBatis版本兼容性?

A: 为保证兼容性,建议:

  1. 避免依赖MyBatis内部实现类,尽量使用接口
  2. 对不同版本的MyBatis进行兼容性测试
  3. 在插件文档中明确支持的MyBatis版本范围

高级插件开发技巧

1. 基于注解的插件配置

除了XML配置外,还可以通过注解实现插件的灵活配置:

// 自定义插件配置注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InterceptSettings {
  boolean logSql() default true;
  long timeout() default 3000;
}

// Mapper接口中使用
public interface UserMapper {
  @InterceptSettings(logSql = true, timeout = 5000)
  User selectById(Long id);
}

// 插件中获取注解配置
String mapperId = ms.getId();
String methodName = mapperId.substring(mapperId.lastIndexOf(".") + 1);
Class<?> mapperClass = Class.forName(mapperId.substring(0, mapperId.lastIndexOf(".")));
Method method = mapperClass.getMethod(methodName, parameterTypes);
InterceptSettings settings = method.getAnnotation(InterceptSettings.class);

if (settings != null && settings.logSql()) {
  // 记录SQL日志
}

2. 插件的条件化拦截

实现根据特定条件决定是否拦截:

public class ConditionalPlugin implements Interceptor {
  private List<String> includeMappers;
  private List<String> excludeMappers;
  
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
    String mapperId = ms.getId();
    
    // 判断是否需要拦截
    if (isNeedIntercept(mapperId)) {
      // 执行拦截逻辑
      // ...
    }
    
    return invocation.proceed();
  }
  
  private boolean isNeedIntercept(String mapperId) {
    // 根据include和exclude规则判断
    // ...
  }
  
  // 其他方法实现...
}

知识衔接

掌握了这些高级技巧和避坑指南后,你已经具备了开发企业级MyBatis插件的能力。接下来我们将通过一个完整的实战案例,综合运用所学知识。

实践检验清单

  • [ ] 能实现基于注解的插件配置
  • [ ] 掌握插件性能优化的方法
  • [ ] 了解插件兼容性处理策略

五、实战案例:完整插件开发流程

痛点直击

如何从零开始开发一个MyBatis插件?完整的开发流程是什么样的?如何测试和部署插件?

步骤1:创建插件类

创建一个实现Interceptor接口的类,并添加@Intercepts注解:

// src/main/java/com/example/plugins/EncryptPlugin.java
package com.example.plugins;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import java.util.Properties;

@Intercepts({
  @Signature(
    type = Executor.class,
    method = "update",
    args = {MappedStatement.class, Object.class}
  )
})
public class EncryptPlugin implements Interceptor {
  private Encryptor encryptor; // 加密工具类
  
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
    Object parameter = invocation.getArgs()[1];
    
    // 判断是否需要加密
    if (needEncrypt(ms.getId())) {
      // 对参数进行加密处理
      encryptParameter(parameter);
    }
    
    return invocation.proceed();
  }
  
  @Override
  public Object plugin(Object target) {
    // 只对Executor类型的目标对象创建代理
    if (target instanceof Executor) {
      return Plugin.wrap(target, this);
    }
    return target;
  }
  
  @Override
  public void setProperties(Properties properties) {
    // 初始化加密工具
    String algorithm = properties.getProperty("algorithm", "AES");
    String key = properties.getProperty("key");
    encryptor = new Encryptor(algorithm, key);
  }
  
  // 参数加密逻辑
  private void encryptParameter(Object parameter) {
    // 实现参数加密逻辑
    // ...
  }
  
  // 判断是否需要加密
  private boolean needEncrypt(String mapperId) {
    // 根据mapperId判断是否需要加密
    // ...
  }
}

步骤2:配置插件

在mybatis-config.xml中注册插件并配置属性:

<!-- src/main/resources/mybatis-config.xml -->
<configuration>
  <plugins>
    <plugin interceptor="com.example.plugins.EncryptPlugin">
      <property name="algorithm" value="AES"/>
      <property name="key" value="mySecretKey123"/>
    </plugin>
  </plugins>
  <!-- 其他配置 -->
</configuration>

步骤3:编写测试用例

创建测试类验证插件功能:

// src/test/java/com/example/plugins/EncryptPluginTest.java
package com.example.plugins;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
import static org.junit.Assert.*;

public class EncryptPluginTest {
  private SqlSessionFactory sqlSessionFactory;
  
  @Test
  public void testParameterEncryption() {
    try (SqlSession session = sqlSessionFactory.openSession()) {
      UserMapper userMapper = session.getMapper(UserMapper.class);
      User user = new User();
      user.setName("Test User");
      user.setIdCard("123456789012345678"); // 需要加密的字段
      
      userMapper.insert(user);
      
      // 验证数据库中的数据是否已加密
      User savedUser = userMapper.selectById(user.getId());
      assertNotEquals("123456789012345678", savedUser.getIdCard());
    }
  }
}

步骤4:验证方法

验证插件是否生效的方法:

  1. 日志验证:在插件中添加日志输出,观察控制台日志
  2. 调试验证:在intercept方法中设置断点,检查是否被调用
  3. 结果验证:检查数据库中的数据是否按预期被处理
  4. 性能验证:使用性能测试工具检查插件对性能的影响

步骤5:打包与部署

将插件打包为jar文件,并添加到项目依赖中:

<!-- pom.xml -->
<dependency>
  <groupId>com.example</groupId>
  <artifactId>mybatis-encrypt-plugin</artifactId>
  <version>1.0.0</version>
</dependency>

实践检验清单

  • [ ] 能独立完成插件的编码、配置和测试
  • [ ] 掌握插件功能的验证方法
  • [ ] 了解插件的打包和部署流程

六、总结与展望

MyBatis插件机制为开发者提供了强大的扩展能力,通过拦截四大核心组件,可以实现SQL监控、数据权限、参数加密等多种功能。本文从原理剖析、场景驱动、问题解决到进阶实践,全面介绍了MyBatis插件开发的关键技术和最佳实践。

随着MyBatis的不断发展,插件机制也在不断完善。未来,插件开发可能会更加智能化,提供更多的钩子点和更丰富的上下文信息,使开发者能够更灵活地扩展MyBatis的功能。

作为开发者,我们应该:

  1. 深入理解MyBatis的内部原理,才能开发出高效、稳定的插件
  2. 遵循单一职责原则,每个插件只做一件事,并把它做好
  3. 重视性能和兼容性,避免插件成为系统瓶颈
  4. 持续学习MyBatis的新特性,不断优化插件实现

通过合理使用插件,我们可以在不修改MyBatis源码的情况下,为项目添加强大的自定义功能,提升开发效率和系统质量。

实践检验总清单

  • [ ] 理解MyBatis插件的工作原理和执行流程
  • [ ] 能根据业务需求选择合适的拦截点
  • [ ] 掌握插件开发中的常见问题及解决方案
  • [ ] 能够独立开发、测试和部署MyBatis插件
  • [ ] 了解插件性能优化的方法和最佳实践
登录后查看全文
热门项目推荐
相关项目推荐