【MyBatis插件全解析】动态代理+拦截链,打造你的专属ORM扩展

你是否想过在MyBatis执行SQL时自动添加分页功能?或者统一加密数据库敏感字段?MyBatis插件就是实现这些黑科技的终极武器!本文将带你彻底掌握插件开发的核心原理与实战技巧!

一、插件能做什么?应用场景一览 ️

1. 典型应用场景

场景 实现效果 代表插件
SQL分页 自动添加LIMIT/OFFSET PageHelper
字段加解密 出入参自动加解密 CryptoInterceptor
SQL性能监控 记录慢查询并告警 SlowQueryInterceptor
多租户隔离 自动添加租户ID过滤条件 TenantInterceptor
审计日志 记录数据变更日志 AuditLogInterceptor

2. 插件核心价值

45% 25% 15% 15% 插件核心价值分布 功能扩展 性能优化 安全增强 统一逻辑

二、插件运行原理揭秘

1. 整体架构:拦截链模式

Executor
插件1
插件2
...
插件N
原生对象

关键机制

  • 目标对象(Executor等)被代理对象包装
  • 多个插件形成拦截链
  • 执行时按顺序通过所有插件

2. 拦截点(四大核心组件)

拦截对象 作用 可拦截方法
Executor SQL执行器 update, query, commit等
ParameterHandler 参数处理器 setParameters
ResultSetHandler 结果集处理器 handleResultSets
StatementHandler SQL语句处理器 prepare, parameterize等

3. 执行流程详解

应用程序 代理对象 拦截器链 原生对象 数据库 Plugin1 Plugin2 执行SQL操作 进入拦截链 前置处理 返回 前置处理 返回 执行原生方法 数据库操作 返回结果 返回结果 后置处理 返回 后置处理 返回 最终结果 返回结果 应用程序 代理对象 拦截器链 原生对象 数据库 Plugin1 Plugin2

三、手把手开发第一个插件

1. 开发三步曲

实现Interceptor接口
标注@Intercepts注解
配置文件中注册

2. 完整代码示例: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 SqlTimerInterceptor implements Interceptor {
    
    private static final Logger log = LoggerFactory.getLogger(SqlTimerInterceptor.class);
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取SQL信息
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        String sqlId = ms.getId();
        
        // 2. 记录开始时间
        long start = System.currentTimeMillis();
        
        try {
            // 3. 执行原方法
            return invocation.proceed();
        } finally {
            // 4. 计算耗时
            long time = System.currentTimeMillis() - start;
            log.info("SQL执行耗时:{}ms - {}", time, sqlId);
            
            // 5. 慢查询告警
            if (time > 1000) {
                log.warn("⚠️ 慢SQL警告:{} 执行耗时 {}ms", sqlId, time);
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        // 创建代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 接收配置文件参数
        String threshold = properties.getProperty("slowThreshold", "1000");
        log.info("慢查询阈值:{}ms", threshold);
    }
}

3. 配置文件注册


<plugins>
    <plugin interceptor="com.example.SqlTimerInterceptor">
        
        <property name="slowThreshold" value="500"/>
    plugin>
plugins>

4. 效果演示

2023-08-20 14:30:22 INFO  SqlTimerInterceptor: SQL执行耗时:45ms - com.example.UserDao.findById
2023-08-20 14:31:05 WARN  SqlTimerInterceptor: ⚠️ 慢SQL警告:com.example.OrderDao.findComplexOrders 执行耗时 1203ms

四、进阶开发技巧

1. 获取SQL参数

@Override
public Object intercept(Invocation invocation) throws Throwable {
    // 获取参数对象
    Object parameter = invocation.getArgs()[1];
    
    // 获取参数处理器
    MetaObject metaParam = SystemMetaObject.forObject(parameter);
    String userName = (String) metaParam.getValue("userName");
    
    // ... 业务逻辑
}

2. 修改SQL语句

@Intercepts({
    @Signature(type = StatementHandler.class, 
               method = "prepare", 
               args = {Connection.class, Integer.class})
})
public class SqlModifierInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        
        // 修改原始SQL
        String newSql = boundSql.getSql() + " LIMIT 10";
        
        // 通过反射修改SQL
        MetaObject metaSql = SystemMetaObject.forObject(boundSql);
        metaSql.setValue("sql", newSql);
        
        return invocation.proceed();
    }
}

3. 多插件顺序控制


<plugins>
    <plugin interceptor="com.example.PluginA"/>
    <plugin interceptor="com.example.PluginB"/>
    <plugin interceptor="com.example.PluginC"/>
plugins>

五、插件开发黄金法则

1. 必须遵守的规范

规则 正确示例 错误示例 后果
不要修改Invocation参数 只读不写 修改args数组内容 破坏内部状态
必须调用invocation.proceed() 在拦截器中调用proceed 忘记调用proceed SQL无法执行
谨慎修改目标对象 通过代理机制安全访问 直接修改原生对象属性 导致不可预知错误

2. 性能优化建议

@Override
public Object plugin(Object target) {
    // 精确拦截:仅拦截Executor类型
    if (target instanceof Executor) {
        return Plugin.wrap(target, this);
    }
    return target; // 其他类型直接返回
}

3. 最佳实践总结

  • 轻量处理:避免在拦截器中做重操作
  • 精准拦截:使用@Signature精确指定拦截点
  • 幂等设计:确保插件可重复执行
  • 异常处理:捕获异常并合理处理
  • 版本兼容:考虑MyBatis版本差异

六、企业级插件实战

1. 数据加解密插件

@Intercepts({
    @Signature(type = ParameterHandler.class, 
               method = "setParameters", 
               args = {PreparedStatement.class}),
    @Signature(type = ResultSetHandler.class, 
               method = "handleResultSets", 
               args = {Statement.class})
})
public class CryptoInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 加密:参数设置时
        if (invocation.getTarget() instanceof ParameterHandler) {
            encryptParameters(invocation);
        }
        // 解密:结果集处理时
        else if (invocation.getTarget() instanceof ResultSetHandler) {
            Object result = invocation.proceed();
            return decryptResult(result);
        }
        
        return invocation.proceed();
    }
    
    private void encryptParameters(Invocation invocation) {
        // 获取参数对象并加密敏感字段
        ParameterHandler ph = (ParameterHandler) invocation.getTarget();
        MetaObject metaParam = SystemMetaObject.forObject(ph.getParameterObject());
        if (metaParam.hasGetter("password")) {
            String pwd = (String) metaParam.getValue("password");
            metaParam.setValue("password", AES.encrypt(pwd));
        }
    }
    
    private Object decryptResult(Object result) {
        // 解密结果集中的敏感字段
        if (result instanceof List) {
            ((List<?>) result).forEach(item -> {
                if (item instanceof User) {
                    User user = (User) item;
                    user.setPassword(AES.decrypt(user.getPassword()));
                }
            });
        }
        return result;
    }
}

2. 多租户隔离插件

@Intercepts({
    @Signature(type = StatementHandler.class, 
               method = "prepare", 
               args = {Connection.class, Integer.class})
})
public class TenantInterceptor implements Interceptor {
    
    private final ThreadLocal<Long> tenantId = new ThreadLocal<>();
    
    public void setTenantId(Long id) {
        tenantId.set(id);
    }
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();
        
        // 仅处理SELECT语句
        if (sql.trim().toUpperCase().startsWith("SELECT")) {
            Long tid = tenantId.get();
            if (tid != null) {
                // 添加租户过滤条件
                String newSql = sql + " AND tenant_id = " + tid;
                
                // 通过反射修改SQL
                MetaObject metaSql = SystemMetaObject.forObject(boundSql);
                metaSql.setValue("sql", newSql);
            }
        }
        
        return invocation.proceed();
    }
}

3. 插件配置中心化管理

// 通过Spring配置管理插件
@Configuration
public class MyBatisPluginConfig {
    
    @Bean
    public SqlTimerInterceptor sqlTimerInterceptor() {
        SqlTimerInterceptor interceptor = new SqlTimerInterceptor();
        Properties props = new Properties();
        props.setProperty("slowThreshold", "500");
        interceptor.setProperties(props);
        return interceptor;
    }
    
    @Bean
    public TenantInterceptor tenantInterceptor() {
        return new TenantInterceptor();
    }
    
    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> {
            configuration.addInterceptor(sqlTimerInterceptor());
            configuration.addInterceptor(tenantInterceptor());
        };
    }
}

七、插件开发常见陷阱

1. 代理嵌套问题

原生Executor
插件A代理
插件B代理
插件C代理

问题:多次调用Plugin.wrap()导致多层嵌套代理
解决

@Override
public Object plugin(Object target) {
    // 避免重复代理
    if (target instanceof Interceptor) {
        return target;
    }
    return Plugin.wrap(target, this);
}

2. 线程安全问题

问题:插件中使用了共享可变状态
解决

// 错误:共享变量
private int queryCount;

// 正确:使用ThreadLocal
private ThreadLocal<Integer> queryCount = ThreadLocal.withInitial(() -> 0);

3. 拦截点选择错误

问题:在错误拦截点修改SQL
对比

操作 最佳拦截点 错误选择
修改SQL语句 StatementHandler.prepare Executor.query
修改参数 ParameterHandler.setParameters StatementHandler.parameterize
修改返回结果 ResultSetHandler.handleResultSets 无替代方案

八、插件体系总结

1. 核心要点回顾

【MyBatis插件全解析】动态代理+拦截链,打造你的专属ORM扩展_第1张图片

2. 学习路径建议

  1. 基础:掌握拦截器接口和注解使用
  2. 进阶:理解MyBatis执行流程和核心组件
  3. 高级:学习动态代理和元编程技术
  4. 实战:从简单插件开始,逐步开发复杂插件

终极建议:阅读优秀开源插件源码(如PageHelper)是提升的最佳途径!

插件能力自测

  1. 如何获取正在执行的SQL语句?
  2. 怎样避免插件被多次代理?
  3. 如何统一处理所有查询结果?

现在就开始你的第一个插件开发吧! 尝试实现一个简单的SQL日志美化插件~ ✨

你可能感兴趣的:(【MyBatis插件全解析】动态代理+拦截链,打造你的专属ORM扩展)