乐于分享
好东西不私藏

MyBatis插件模块详解

MyBatis插件模块详解

MyBatis插件模块详解

一、MyBatis整体架构与插件模块

在深入插件模块之前,我们先了解MyBatis的整体架构,以及插件模块在其中的重要地位。

从上图可以看出,MyBatis采用了分层架构设计,而插件模块(Plugin)通过动态代理机制横切整个框架,能够在核心组件的执行过程中插入自定义逻辑。它为MyBatis提供了强大的扩展能力,使得开发者可以在不修改源码的情况下增强框架功能。

1.1 插件模块的核心职责

插件模块主要承担以下核心职责:

  1. 扩展框架功能:在不动源码的情况下增强MyBatis功能
  2. 拦截核心组件:拦截Executor、StatementHandler、ParameterHandler、ResultSetHandler
  3. 实现AOP功能:通过动态代理实现面向切面编程
  4. 统一权限控制:实现数据权限过滤
  5. 性能监控:记录SQL执行时间和性能指标
  6. 分页查询:实现物理分页功能

1.2 为什么需要插件

在实际开发中,我们经常有这些需求:

需求场景:├── SQL性能监控:记录慢查询├── 数据权限控制:根据用户权限过滤数据├── 分页查询:自动实现物理分页├── 乐观锁:自动更新版本号├── 审计日志:记录操作日志└── 加密解密:敏感数据加密存储

如果没有插件机制,我们需要修改MyBatis源码或编写大量重复代码。插件机制让我们可以优雅地实现这些功能。

1.3 插件的实现原理

MyBatis的插件基于动态代理实现:

目标对象    ↓使用Plugin.wrap()包装    ↓生成代理对象    ↓调用intercept()方法    ↓执行自定义逻辑    ↓调用目标方法或继续传递

二、插件拦截机制

MyBatis的插件机制基于Java动态代理和责任链模式。

2.1 Interceptor接口

MyBatis插件的核心是Interceptor接口:

publicinterfaceInterceptor{/**     * 拦截方法,在这里实现自定义逻辑     * @param invocation 代理调用对象     * @return 方法执行结果     * @throws Throwable 异常     */Object intercept(Invocation invocation)throws Throwable;/**     * 包装目标对象,生成代理对象     * @param target 目标对象     * @return 代理对象     */default Object plugin(Object target){return Plugin.wrap(target, this);    }/**     * 设置插件属性     * @param properties 配置属性     */defaultvoidsetProperties(Properties properties){// NOP    }}

2.2 Invocation类

Invocation封装了方法调用的相关信息:

publicclassInvocation{privatefinal Object target;      // 目标对象privatefinal Method method;      // 目标方法privatefinal Object[] args;      // 方法参数publicInvocation(Object target, Method method, Object[] args){this.target = target;this.method = method;this.args = args;    }// 执行目标方法public Object proceed()throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);    }// Getter方法public Object getTarget()return target; }public Method getMethod()return method; }public Object[] getArgs() { return args; }}

2.3 Plugin类

Plugin是代理对象的InvocationHandler:

publicclassPluginimplementsInvocationHandler{privatefinal Object target;privatefinal Interceptor interceptor;privatefinal Map<Class<?>, Set<Method>> signatureMap;privatePlugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap){this.target = target;this.interceptor = interceptor;this.signatureMap = signatureMap;    }// 静态工厂方法,包装目标对象publicstatic Object wrap(Object target, Interceptor interceptor){// 获取拦截的类和方法签名        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);        Class<?> type = target.getClass();// 获取需要拦截的接口        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {// 创建代理对象return Proxy.newProxyInstance(                type.getClassLoader(),                interfaces,new Plugin(target, interceptor, signatureMap)            );        }return target;    }@Overridepublic Object invoke(Object proxy, Method method, Object[] args)throws Throwable {try {// 获取需要拦截的方法集合            Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 执行拦截器的intercept方法return interceptor.intercept(new Invocation(target, method, args));            }// 不需要拦截,直接执行return method.invoke(target, args);        } catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);        }    }// 获取签名映射privatestatic Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);if (interceptsAnnotation == null) {thrownew PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());        }        Signature[] sigs = interceptsAnnotation.value();        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();for (Signature sig : sigs) {            Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());try {                Method method = sig.type().getMethod(sig.method(), sig.args());                methods.add(method);            } catch (NoSuchMethodException e) {thrownew PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);            }        }return signatureMap;    }// 获取所有需要拦截的接口privatestatic Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {        Set<Class<?>> interfaces = new HashSet<>();while (type != null) {for (Class<?> c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {                    interfaces.add(c);                }            }            type = type.getSuperclass();        }return interfaces.toArray(new Class<?>[interfaces.size()]);    }}

三、四大核心拦截点

MyBatis允许拦截四个核心组件。

3.1 Executor(执行器)

Executor是MyBatis的核心执行器,负责SQL的执行。

可拦截方法:

  • update(MappedStatement ms, Object parameter) – 执行增删改
  • query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) – 查询
  • query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) – 查询(带缓存)
  • flushStatements() – 刷新批处理
  • commit() – 提交事务
  • rollback() – 回滚事务
  • getTransaction() – 获取事务
  • close() – 关闭会话
  • isClosed() – 是否关闭

示例:性能监控插件

@Intercepts({@Signature(type = Executor.class,method"update",               args = {MappedStatement.classObject.class}),    @Signature(type= Executor.class,method"query",               args = {MappedStatement.classObject.classRowBounds.classResultHandler.class})})publicclassPerformanceMonitorPluginimplementsInterceptor{privatelong threshold; // 慢查询阈值@Overridepublic Object intercept(Invocation invocation)throws Throwable {        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];long start = System.currentTimeMillis();try {// 执行目标方法return invocation.proceed();        } finally {long end = System.currentTimeMillis();long time = end - start;if (time > threshold) {                String sql = ms.getBoundSql(null).getSql();                System.out.println("慢查询警告: " + ms.getId() + " 耗时: " + time + "ms");                System.out.println("SQL: " + sql);            }        }    }@OverridepublicvoidsetProperties(Properties properties){this.threshold = Long.parseLong(properties.getProperty("threshold""1000"));    }}

3.2 StatementHandler(语句处理器)

StatementHandler负责创建Statement对象并设置参数。

可拦截方法:

  • prepare(Connection connection) – 准备Statement
  • parameterize(Statement statement) – 设置参数
  • batch(Statement statement) – 批处理
  • update(Statement statement) – 执行更新
  • query(Statement statement, ResultHandler resultHandler) – 执行查询
  • getBoundSql() – 获取BoundSql
  • getParameterHandler() – 获取ParameterHandler
  • getResultHandler() – 获取ResultSetHandler

示例:SQL重写插件

@Intercepts({@Signature(type = StatementHandler.class,method"prepare",               args = {Connection.classInteger.class})})publicclassSQLRewritePluginimplementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {        StatementHandler handler = (StatementHandler) invocation.getTarget();        BoundSql boundSql = handler.getBoundSql();        String sql = boundSql.getSql();// 重写SQL:添加数据权限过滤if (sql.toLowerCase().startsWith("select")) {            sql = rewriteSQL(sql);// 使用反射修改sql字段            Field field = boundSql.getClass().getDeclaredField("sql");            field.setAccessible(true);            field.set(boundSql, sql);        }return invocation.proceed();    }private String rewriteSQL(String sql){// 添加数据权限过滤条件return sql + " AND tenant_id = '" + getCurrentTenantId() + "'";    }private String getCurrentTenantId(){// 获取当前租户IDreturn"1001";    }}

3.3 ParameterHandler(参数处理器)

ParameterHandler负责设置PreparedStatement的参数。

可拦截方法:

  • getParameterObject() – 获取参数对象
  • setParameters(PreparedStatement ps) – 设置参数

示例:参数加密插件

@Intercepts({@Signature(type = ParameterHandler.class,method"setParameters",               args = {PreparedStatement.class})})publicclassParameterEncryptPluginimplementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {        ParameterHandler handler = (ParameterHandler) invocation.getTarget();        PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];// 获取参数对象        Object parameterObject = handler.getParameterObject();if (parameterObject instanceof User) {            User user = (User) parameterObject;// 加密敏感字段if (user.getPassword() != null) {                user.setPassword(encrypt(user.getPassword()));            }        }// 继续执行return invocation.proceed();    }private String encrypt(String plainText){// Base64简单加密return Base64.getEncoder().encodeToString(plainText.getBytes());    }}

3.4 ResultSetHandler(结果集处理器)

ResultSetHandler负责将ResultSet映射为Java对象。

可拦截方法:

  • handleResultSets(Statement stmt) – 处理结果集
  • handleOutputParameters(CallableStatement cs) – 处理存储过程输出参数

示例:结果解密插件

@Intercepts({@Signature(type = ResultSetHandler.class,method"handleResultSets",               args = {Statement.class})})publicclassResultDecryptPluginimplementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {// 执行查询        Object result = invocation.proceed();if (result instanceof List) {            List<?> list = (List<?>) result;for (Object obj : list) {if (obj instanceof User) {                    User user = (User) obj;// 解密敏感字段if (user.getIdCard() != null) {                        user.setIdCard(decrypt(user.getIdCard()));                    }                }            }        }return result;    }private String decrypt(String cipherText){// Base64解密byte[] decoded = Base64.getDecoder().decode(cipherText);returnnew String(decoded);    }}

四、插件实现流程

理解插件的实现和配置流程非常重要。

4.1 实现自定义插件

步骤1:实现Interceptor接口

@Intercepts({@Signature(type = Executor.class,method"query",               args = {MappedStatement.classObject.classRowBounds.classResultHandler.class})})publicclassCustomPluginimplementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {// 前置逻辑        System.out.println("Before: " + invocation.getMethod().getName());// 执行目标方法        Object result = invocation.proceed();// 后置逻辑        System.out.println("After: " + invocation.getMethod().getName());return result;    }@Overridepublic Object plugin(Object target){// 使用Plugin.wrap包装目标对象return Plugin.wrap(target, this);    }@OverridepublicvoidsetProperties(Properties properties){// 读取配置属性        String propertyName = properties.getProperty("propertyName");        System.out.println("Property: " + propertyName);    }}

4.2 配置插件

方式1:XML配置

<configuration><plugins><!-- 性能监控插件 --><plugininterceptor="com.example.plugin.PerformanceMonitorPlugin"><propertyname="threshold"value="1000"/></plugin><!-- 分页插件 --><plugininterceptor="com.github.pagehelper.PageInterceptor"><propertyname="helperDialect"value="mysql"/></plugin><!-- 自定义插件 --><plugininterceptor="com.example.plugin.CustomPlugin"><propertyname="propertyName"value="propertyValue"/></plugin></plugins></configuration>

方式2:代码配置

// 创建ConfigurationConfiguration configuration = new Configuration();// 添加插件configuration.addInterceptor(new PerformanceMonitorPlugin());configuration.addInterceptor(new PageInterceptor());configuration.addInterceptor(new CustomPlugin());// 创建SqlSessionFactorySqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

方式3:Spring配置

@ConfigurationpublicclassMyBatisConfig{@Beanpublic PerformanceMonitorPlugin performanceMonitorPlugin(){        PerformanceMonitorPlugin plugin = new PerformanceMonitorPlugin();        Properties properties = new Properties();        properties.setProperty("threshold""1000");        plugin.setProperties(properties);return plugin;    }@Beanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource)throws Exception {        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();        sessionFactory.setDataSource(dataSource);// 添加插件        sessionFactory.setPlugins(new Interceptor[]{                performanceMonitorPlugin(),new PageInterceptor()            }        );return sessionFactory.getObject();    }}

4.3 插件链的执行顺序

当配置了多个插件时,它们会形成责任链:

原始Executor    ↓被Plugin1包装    ↓被Plugin2包装    ↓被Plugin3包装    ↓最终代理对象

执行顺序:

// 配置顺序<plugin interceptor="Plugin1"/><plugin interceptor="Plugin2"/><plugin interceptor="Plugin3"/>// 执行顺序Plugin3.intercept() → Plugin2.intercept() → Plugin1.intercept() → 目标方法// 返回顺序目标方法 → Plugin1处理 → Plugin2处理 → Plugin3处理 → 最终结果

五、动态代理链

多个插件会形成多层代理。

5.1 代理链的构建

// 原始对象Executor target = new SimpleExecutor();// 第一次包装Executor proxy1 = (Executor) Plugin.wrap(target, plugin1);// 第二次包装(包装的是代理对象)Executor proxy2 = (Executor) Plugin.wrap(proxy1, plugin2);// 第三次包装Executor proxy3 = (Executor) Plugin.wrap(proxy2, plugin3);// proxy3是最外层的代理

5.2 代理链的执行

调用 proxy3.query()    ↓Plugin3.invoke()    ↓Plugin3.intercept()    ↓invocation.proceed() 调用 proxy2.query()    ↓Plugin2.invoke()    ↓Plugin2.intercept()    ↓invocation.proceed() 调用 proxy1.query()    ↓Plugin1.invoke()    ↓Plugin1.intercept()    ↓invocation.proceed() 调用 target.query()    ↓执行真正的查询    ↓返回结果    ↓Plugin1处理返回值    ↓Plugin2处理返回值    ↓Plugin3处理返回值    ↓最终返回

5.3 代理链示例代码

@Intercepts(@Signature(type = Executor.classmethod"query",        args = {MappedStatement.classObject.classRowBounds.classResultHandler.class}))publicclassPlugin1implementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {        System.out.println("Plugin1 Before");        Object result = invocation.proceed();        System.out.println("Plugin1 After");return result;    }}@Intercepts(@Signature(type = Executor.classmethod"query",        args = {MappedStatement.classObject.classRowBounds.classResultHandler.class}))publicclassPlugin2implementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {        System.out.println("Plugin2 Before");        Object result = invocation.proceed();        System.out.println("Plugin2 After");return result;    }}// 输出结果:// Plugin2 Before// Plugin1 Before// 执行查询// Plugin1 After// Plugin2 After

六、典型应用场景

插件在实际开发中有很多应用场景。

6.1 分页插件

PageHelper是最著名的MyBatis分页插件:

<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.3.2</version></dependency>

配置:

<plugininterceptor="com.github.pagehelper.PageInterceptor"><propertyname="helperDialect"value="mysql"/><propertyname="reasonable"value="true"/></plugin>

使用:

// 查询前调用分页PageHelper.startPage(110);List<User> users = userMapper.selectAll();// 获取分页信息PageInfo<User> pageInfo = new PageInfo<>(users);System.out.println("总数: " + pageInfo.getTotal());System.out.println("页数: " + pageInfo.getPages());

6.2 性能监控插件

@Intercepts({@Signature(type = Executor.class,method"update",               args = {MappedStatement.classObject.class}),    @Signature(type= Executor.class,method"query",               args = {MappedStatement.classObject.classRowBounds.classResultHandler.class})})publicclassSlowQueryMonitorPluginimplementsInterceptor{privatestaticfinal Logger logger = LoggerFactory.getLogger(SlowQueryMonitorPlugin.class);privatelong slowQueryThreshold;@Overridepublic Object intercept(Invocation invocation)throws Throwable {        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];long start = System.currentTimeMillis();try {return invocation.proceed();        } finally {long cost = System.currentTimeMillis() - start;if (cost > slowQueryThreshold) {                BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);                String sql = boundSql.getSql();                logger.warn("慢查询: {} 耗时: {}ms\nSQL: {}", ms.getId(), cost, sql);            }        }    }@OverridepublicvoidsetProperties(Properties properties){this.slowQueryThreshold = Long.parseLong(properties.getProperty("threshold""1000"));    }}

6.3 数据权限插件

@Intercepts({@Signature(type = StatementHandler.class,method"prepare",               args = {Connection.classInteger.class})})publicclassDataPermissionPluginimplementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {        StatementHandler handler = (StatementHandler) invocation.getTarget();        BoundSql boundSql = handler.getBoundSql();        String sql = boundSql.getSql();// 获取当前用户的数据权限        Set<Long> deptIds = getCurrentUserDeptIds();// 重写SQL,添加数据权限过滤if (sql.toLowerCase().startsWith("select") && !deptIds.isEmpty()) {            String condition = "dept_id IN (" + String.join(",", deptIds.toString()) + ")";            sql = addDataPermission(sql, condition);// 使用反射修改SQL            Field field = boundSql.getClass().getDeclaredField("sql");            field.setAccessible(true);            field.set(boundSql, sql);        }return invocation.proceed();    }private String addDataPermission(String sql, String condition){// 简单实现:在WHERE后添加条件if (sql.toLowerCase().contains("where")) {return sql + " AND " + condition;        } else {return sql + " WHERE " + condition;        }    }private Set<Long> getCurrentUserDeptIds(){// 从上下文获取当前用户的数据权限return SecurityContextHolder.getCurrentUserDataPermission();    }}

6.4 乐观锁插件

@Intercepts({@Signature(type = Executor.class,method"update",               args = {MappedStatement.classObject.class})})publicclassOptimisticLockPluginimplementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {        Object parameter = invocation.getArgs()[1];if (parameter instanceof BaseEntity) {            BaseEntity entity = (BaseEntity) parameter;// 自动设置版本号if (entity.getVersion() == null) {                entity.setVersion(0);            } else {                entity.setVersion(entity.getVersion() + 1);            }        }return invocation.proceed();    }}

6.5 审计日志插件

@Intercepts({@Signature(type = Executor.class,method"update",               args = {MappedStatement.classObject.class})})publicclassAuditLogPluginimplementsInterceptor{@Overridepublic Object intercept(Invocation invocation)throws Throwable {        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];        Object parameter = invocation.getArgs()[1];// 记录操作日志        String operation = ms.getId();        String operator = getCurrentUser();        Date operateTime = new Date();        System.out.println("操作人: " + operator);        System.out.println("操作时间: " + operateTime);        System.out.println("操作类型: " + operation);        System.out.println("操作数据: " + parameter);// 执行目标方法        Object result = invocation.proceed();// 记录操作结果        System.out.println("影响行数: " + result);return result;    }private String getCurrentUser(){// 获取当前登录用户return SecurityContextHolder.getCurrentUser().getUsername();    }}

七、最佳实践

7.1 插件设计原则

  1. 最小侵入:尽量不修改原有逻辑
  2. 可配置性:通过属性配置开关
  3. 性能考虑:避免在插件中执行耗时操作
  4. 异常处理:妥善处理异常,避免影响正常流程
  5. 日志记录:记录关键操作日志

7.2 性能优化

  1. 减少反射使用:缓存Field/Method对象
  2. 避免复杂计算:插件逻辑要简单高效
  3. 使用缓存:缓存常用数据
  4. 异步处理:日志等操作异步执行

7.3 常见问题解决

问题1:插件不生效

// 原因:@Signature配置错误// 错误:方法签名不匹配@Signature(type = Executor.class,method"query",           args = {MappedStatement.classObject.class}) // 缺少参数// 正确:完整的方法签名@Signature(type= Executor.class,method"query",           args = {MappedStatement.classObject.classRowBounds.classResultHandler.class})

问题2:修改SQL失败

// 原因:直接修改sql字段不生效boundSql.setSql(newSql); // BoundSql没有setSql方法// 正确:使用反射修改Field field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);field.set(boundSql, newSql);

问题3:插件顺序混乱

<!-- 建议:按执行顺序配置 --><!-- 分页插件应该先执行 --><plugininterceptor="PageInterceptor"/><!-- 数据权限插件后执行 --><plugininterceptor="DataPermissionPlugin"/>

7.4 插件开发模板

/** * 插件开发模板 */@Intercepts({@Signature(type = Executor.class,method"query",               args = {MappedStatement.classObject.classRowBounds.classResultHandler.class})})publicclassTemplatePluginimplementsInterceptor{privatestaticfinal Logger logger = LoggerFactory.getLogger(TemplatePlugin.class);// 配置属性private String configProperty;@Overridepublic Object intercept(Invocation invocation)throws Throwable {// 1. 前置处理        Object target = invocation.getTarget();        Method method = invocation.getMethod();        Object[] args = invocation.getArgs();        logger.debug("Before intercept: {}", method.getName());try {// 2. 执行目标方法            Object result = invocation.proceed();// 3. 后置处理            logger.debug("After intercept: {}", method.getName());return result;        } catch (Exception e) {// 4. 异常处理            logger.error("Plugin error", e);throw e;        }    }@Overridepublic Object plugin(Object target){return Plugin.wrap(target, this);    }@OverridepublicvoidsetProperties(Properties properties){this.configProperty = properties.getProperty("configProperty""defaultValue");        logger.info("Plugin initialized with property: {}", configProperty);    }}

八、总结

MyBatis的插件模块提供了强大的扩展能力,使得开发者可以在不修改源码的情况下增强框架功能。

核心要点

  1. Interceptor接口:定义插件的核心接口
  2. 四大拦截点:Executor、StatementHandler、ParameterHandler、ResultSetHandler
  3. 动态代理:基于JDK动态代理实现
  4. 责任链模式:多个插件形成责任链
  5. 应用场景:分页、监控、权限、加密、日志等
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » MyBatis插件模块详解

评论 抢沙发

9 + 2 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮