# 第07天:MyBatis插件机制 ## 📚 今日目标 1. 理解MyBatis插件机制 2. 理解拦截器(Interceptor)的概念 3. 实现DynamicMybatisPlugin插件 4. 学会修改SQL语句 --- ## 🎯 知识点1:MyBatis插件机制 ### 为什么需要插件? **场景**:我们需要在SQL执行前修改表名 ```sql -- 原始SQL SELECT * FROM user WHERE id = ? -- 需要修改为 SELECT * FROM user_01 WHERE id = ? ``` **解决方案**:MyBatis插件 - 拦截SQL执行 - 修改SQL语句 - 不修改业务代码 ### MyBatis插件原理 ``` SQL执行流程: 1. 创建Statement ↓ 2. 插件拦截(可以修改SQL) ↓ 3. 执行SQL ↓ 4. 返回结果 ``` --- ## 🎯 知识点2:实现拦截器 ### Interceptor接口 ```java public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); } ``` ### @Intercepts注解 ```java @Intercepts({ @Signature( type = StatementHandler.class, // 拦截StatementHandler method = "prepare", // 拦截prepare方法 args = {Connection.class, Integer.class} // 方法参数 ) }) ``` --- ## 🛠️ 实践任务:实现DynamicMybatisPlugin ### 步骤1:创建DynamicMybatisPlugin类 在 `src/main/java/cn/bugstack/middleware/db/router/dynamic/` 目录下创建 `DynamicMybatisPlugin.java`: ```java package cn.bugstack.middleware.db.router.dynamic; import cn.bugstack.middleware.db.router.DBContextHolder; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.DefaultReflectorFactory; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import java.lang.reflect.Field; import java.sql.Connection; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * MyBatis动态表名插件 * * 拦截SQL执行,修改表名 * * @author 小傅哥 */ @Intercepts({ @Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} ) }) public class DynamicMybatisPlugin implements Interceptor { private Pattern pattern = Pattern.compile("(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)\\s+", Pattern.CASE_INSENSITIVE); @Override public Object intercept(Invocation invocation) throws Throwable { // 获取StatementHandler StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // 获取BoundSql(包含SQL语句) BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); // 获取表索引 String tbKey = DBContextHolder.getTBKey(); if (null != tbKey && !tbKey.isEmpty()) { // 修改SQL中的表名 sql = sql.replaceAll("(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)\\s+", "$1 $2_" + tbKey + " "); } // 使用反射修改BoundSql的sql字段 MetaObject metaObject = SystemMetaObject.forObject(boundSql); metaObject.setValue("sql", sql); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 可以读取配置 } } ``` ### 代码解释 1. **@Intercepts**:指定拦截StatementHandler的prepare方法 2. **intercept方法**: - 获取SQL语句 - 从ThreadLocal获取表索引 - 使用正则表达式替换表名 - 使用反射修改BoundSql的sql字段 3. **正则表达式**: - `(from|into|update)`:匹配SQL关键字 - `\\s+`:匹配空格 - `(\\w+)`:匹配表名 - `$1 $2_01`:替换为 `from user_01` --- ## 🎓 知识点拓展 ### 拓展1:MyBatis插件拦截点 **可以拦截的对象**: - `Executor`:执行器 - `StatementHandler`:SQL处理器 - `ParameterHandler`:参数处理器 - `ResultSetHandler`:结果集处理器 **我们选择StatementHandler的原因**: - 可以获取和修改SQL语句 - 在SQL执行前拦截 - 不影响参数和结果处理 ### 拓展2:MetaObject的使用 **为什么用MetaObject?** - BoundSql的sql字段是final的,不能直接修改 - MetaObject可以通过反射修改final字段 **例子**: ```java // 方式1:直接修改(不行,sql是final的) boundSql.setSql(newSql); // 编译错误 // 方式2:使用MetaObject(可以) MetaObject metaObject = SystemMetaObject.forObject(boundSql); metaObject.setValue("sql", newSql); // 可以修改 ``` ### 拓展3:SQL替换的精确性 **问题**:简单的replace可能误替换 ```sql -- 原始SQL SELECT * FROM user WHERE name = 'from table' -- 简单replace会误替换 SELECT * FROM user_01 WHERE name = 'from table' -- 正确 SELECT * FROM user_01 WHERE name = 'from_01 table' -- 错误! ``` **解决方案**:使用正则表达式精确匹配 ```java // 精确匹配表名(考虑表别名) Pattern pattern = Pattern.compile( "(from|into|update|FROM|INTO|UPDATE)\\s+(\\w+)(\\s+\\w+)?\\s+", Pattern.CASE_INSENSITIVE ); ``` --- ## ✅ 今日检查清单 - [ ] 理解了MyBatis插件机制 - [ ] 理解了拦截器的概念 - [ ] 实现了DynamicMybatisPlugin插件 - [ ] 理解了SQL替换的原理 - [ ] 理解了MetaObject的使用 - [ ] 完成了拓展阅读 --- ## 🎯 明日预告 明天我们将学习: - ThreadLocal深入理解 - 为什么用ThreadLocal存储路由信息 - 如何避免内存泄漏 --- ## 💡 思考题 1. 为什么选择拦截StatementHandler而不是Executor? 2. 如何精确匹配SQL中的表名? 3. MetaObject的作用是什么? --- ## 📚 参考资源 - [MyBatis插件文档](https://mybatis.org/mybatis-3/zh/configuration.html#plugins) - [MyBatis拦截器原理](https://www.baeldung.com/mybatis-interceptors) - [正则表达式教程](https://www.runoob.com/regexp/regexp-tutorial.html)