MyBatis插件开发深度解析:从拦截原理到实战应用

MyBatis插件机制是其架构中最具扩展性的部分之一,通过拦截器(Interceptor)我们可以对MyBatis的核心组件进行功能增强。本文将深入剖析插件实现原理,并展示典型应用场景的实现方案。

一、Interceptor核心原理

1.1 拦截点与责任链模式

MyBatis允许在4个核心组件上设置拦截点:

图1

拦截执行顺序:Executor → StatementHandler → ParameterHandler → ResultSetHandler

1.2 注解式拦截配置

通过@Intercepts@Signature定义拦截目标:

@Intercepts({
    @Signature(type = Executor.class,
              method = "query",
              args = {MappedStatement.class, Object.class, 
                     RowBounds.class, ResultHandler.class})
})
public class QueryInterceptor implements Interceptor {
    // 实现方法...
}

参数说明

  • type:拦截的接口类型
  • method:拦截的方法名
  • args:方法参数类型列表(用于精确匹配重载方法)

1.3 插件加载机制

MyBatis通过InterceptorChain管理所有插件,形成代理链:

// 简化后的核心处理逻辑
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

实践建议

  1. 拦截器应按功能顺序配置(如:先分页后缓存)
  2. 避免在拦截器中修改输入参数,可能导致链式调用异常

二、典型插件实现方案

2.1 分页插件实现

以简化版PageHelper为例:

public class PageInterceptor implements Interceptor {
    private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (LOCAL_PAGE.get() != null) {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            
            // 修改SQL
            BoundSql boundSql = ms.getBoundSql(parameter);
            String newSql = boundSql.getSql() + " LIMIT ?, ?";
            resetSql(ms, boundSql, newSql);
            
            // 修改参数
            Object newParam = wrapParameter(parameter);
            args[1] = newParam;
        }
        return invocation.proceed();
    }
    
    // 其他工具方法...
}

关键点

  1. 使用ThreadLocal保持分页参数线程安全
  2. 通过反射修改BoundSql中的SQL内容
  3. 重组参数对象添加分页偏移量

2.2 SQL监控插件

实现执行时间统计与慢SQL告警:

public class SqlMonitorInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (cost > 1000) { // 慢SQL阈值
                log.warn("Slow SQL detected: {}ms", cost);
            }
            // 记录到监控系统...
        }
    }
}

增强建议

  1. 添加SQL指纹(去除参数值后的SQL)
  2. 集成Micrometer上报指标
  3. 区分读写操作分别统计

2.3 动态数据源路由

基于注解的读写分离实现:

@Intercepts(@Signature(type = Executor.class, method = "update", args = {...}))
public class DataSourceRouterInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) {
        Method method = getMethod(invocation);
        if (method.isAnnotationPresent(ReadOnly.class)) {
            DynamicDataSourceHolder.markRead();
        } else {
            DynamicDataSourceHolder.markWrite();
        }
        return invocation.proceed();
    }
}

路由策略扩展

  1. 按方法名前缀路由(如findXXX走从库)
  2. 基于事务注解判断
  3. 表级别分库策略

三、高级开发技巧

3.1 元数据访问技巧

通过MappedStatement获取SQL信息:

MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Configuration config = ms.getConfiguration();
SqlSource sqlSource = ms.getSqlSource();
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
String originalSql = boundSql.getSql();

3.2 插件执行顺序控制

  1. 配置文件中声明顺序决定包装顺序
  2. 使用@AutoConfigureAfter(Spring环境下)
  3. 通过@Order注解调整优先级

3.3 性能优化建议

  1. 避免在拦截器中创建大量临时对象
  2. 高频调用方法中使用缓存(如SQL解析结果)
  3. 减少反射操作,必要时缓存Method对象

四、常见问题排查

问题1:插件不生效

  • 检查是否配置到mybatis-config.xml
  • 确认@Signature匹配了正确的方法签名
  • 验证插件被正确加载(查看日志)

问题2:出现重复代理

  • 检查是否重复配置相同插件
  • 确认插件链没有循环调用

问题3:修改SQL无效

  • BoundSql可能是不可变对象,需要重置
  • 注意参数位置与SQL占位符对应关系

五、最佳实践总结

  1. 单一职责:每个插件只处理一个特定功能
  2. 轻量级:避免在插件中实现复杂业务逻辑
  3. 可观测:添加足够的日志和监控指标
  4. 文档完善:明确插件的使用约束和兼容性说明

通过合理使用插件机制,可以在不改动MyBatis源码的情况下实现各种增强功能,但需注意过度使用会影响代码可维护性。建议优先考虑是否可以通过其他方式(如AOP)实现相同功能。

添加新评论