mybatis插件

mybatis插件
MyBatis 插件Interceptor是 MyBatis 框架留给开发者的一扇“后门”。通过它你可以在不修改 MyBatis 核心代码的情况下干预或增强 MyBatis 的执行过程。它的设计非常巧妙接下来我从“它能做什么”、“它的核心原理”和“一个生动的比喻”三个方面帮你彻底搞懂它。 它能做什么典型应用场景MyBatis 插件在真实项目中应用广泛常见的有应用场景具体实现SQL 日志打印拦截 SQL 执行打印完整的 SQL 语句、参数和耗时方便开发和调试。数据权限控制在 SQL 执行前自动拼接WHERE条件如WHERE tenant_id ?实现多租户数据隔离。分页查询拦截Executor的query方法自动改写 SQL在末尾拼接LIMIT和OFFSET这是 PageHelper 等分页插件的实现原理。SQL 性能监控统计 SQL 的执行时间当超过设定的慢查询阈值时自动记录日志或发送告警。加密/解密字段在 SQL 执行前对敏感字段如手机号、身份证进行加密执行后对结果集进行解密。动态替换表名在 SQL 执行前替换表名常用于分表场景如将order替换为order_202607。 它的核心原理基于“责任链 动态代理”MyBatis 插件的底层原理是JDK 动态代理。它的执行逻辑可以概括为四个步骤允许拦截的核心对象MyBatis 只允许对4 大核心接口的方法进行拦截Executor执行器负责 SQL 的整体执行。StatementHandler处理器负责 JDBC 语句的创建和参数设置。ParameterHandler参数处理器负责将参数设置到 SQL 中。ResultSetHandler结果集处理器负责将查询结果映射成 Java 对象。创建代理对象在 MyBatis 启动时会解析你配置的插件。当创建上述 4 大核心对象时MyBatis 会检查是否有插件需要拦截该对象。如果有就会通过JDK 动态代理生成一个代理对象并将原对象作为目标对象传入。形成责任链如果多个插件都拦截了同一个对象MyBatis 会层层嵌套地创建代理形成一个责任链。调用时会从最外层代理开始依次向内执行。执行拦截逻辑当你调用代理对象的方法时如执行 SQL 的query方法会先执行插件中intercept方法里的代码。在这里你可以获取或修改传入的 SQL、参数。执行invocation.proceed()继续执行原方法或后续插件。修改或包装返回的结果。 一个生动的比喻小区的“安防系统”假设你住在一个有严格安防的小区MyBatis外卖员用户请求想通过大门4 大核心对象进入。小区门口有一个保安亭插件。保安Interceptor会检查外卖员的证件SQL 语句和物品参数。保安可以根据规定放行执行 SQL或拒绝抛出异常甚至可以对物品进行登记记录日志或替换改写 SQL。这个“保安”就是 MyBatis 插件它在大门核心方法入口处对进出的所有活动进行控制。️ 简单使用步骤创建类并实现Interceptor接口。使用Intercepts注解指定要拦截的目标接口和方法。在intercept方法中通过Invocation参数获取和修改 SQL、参数等信息然后调用invocation.proceed()继续执行。配置插件在 MyBatis 配置文件 (mybatis-config.xml) 中通过plugin标签注册插件或在 Spring Boot 中配置为 Bean。⚠️ 注意事项不要过度使用每个插件都会增加一层代理过多插件会影响性能。方法签名敏感Intercepts中指定的方法签名必须精准匹配否则插件不会生效。注意分页插件的顺序如果一个请求中同时有分页插件和数据权限插件它们的执行顺序在mybatis-config.xml中的配置顺序会影响最终生成的 SQL。内部对象复杂操作StatementHandler或ResultSetHandler时涉及较多的内部对象对 MyBatis 源码的熟悉程度要求较高。如果想了解如何编写一个简单的 SQL 日志插件或者想看看 PageHelper 分页插件的核心源码是怎么实现的我们可以继续深入。这四个核心组件是 MyBatis 的内部机制通常我们不会直接实例化或调用它们而是通过 MyBatis 的 API 间接使用。不过理解它们的使用方式对于编写插件、调试源码、深入理解 MyBatis非常有帮助。我分两部分来讲正常使用方式和插件开发中的使用方式。一、正常使用方式通过 MyBatis API 间接调用作为普通开发者我们不会直接new Executor()或调用StatementHandler而是通过 MyBatis 提供的 API 来使用它们。1. Executor —— 通过SqlSession使用// 方式1通过 SqlSession 直接调用最常用 try (SqlSession sqlSession sqlSessionFactory.openSession()) { UserMapper mapper sqlSession.getMapper(UserMapper.class); User user mapper.selectById(1); // 内部会调用 Executor } // 方式2SqlSession 的 selectOne/selectList 方法 try (SqlSession sqlSession sqlSessionFactory.openSession()) { User user sqlSession.selectOne(com.example.mapper.UserMapper.selectById, 1); }2. StatementHandler —— 通过SqlSession使用StatementHandler是Executor内部使用的我们不会直接调用它。但它负责的工作创建PreparedStatement、设置参数、执行 SQL最终会通过 JDBC 完成。3. ParameterHandler —— 通过SqlSession使用我们通过 Mapper 接口传入参数MyBatis 内部会使用ParameterHandler来处理这些参数但对我们来说是透明的。4. ResultSetHandler —— 通过SqlSession使用当我们调用 Mapper 方法时返回的 Java 对象就是ResultSetHandler处理完ResultSet后映射的结果。二、插件开发中的使用方式进阶这是最常用的场景。在自定义 MyBatis 插件Interceptor中我们可以直接操作这四个组件的核心方法。1. Executor 的使用拦截查询实现 SQL 审计Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class SqlAuditInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取参数 MappedStatement ms (MappedStatement) invocation.getArgs()[0]; Object parameter invocation.getArgs()[1]; // 2. 获取 SQL 信息 BoundSql boundSql ms.getBoundSql(parameter); String sql boundSql.getSql(); // 3. 记录日志审计 System.out.println(执行的 SQL sql); System.out.println(参数 parameter); long startTime System.currentTimeMillis(); // 4. 继续执行原方法调用下一个拦截器或真正的查询 Object result invocation.proceed(); long endTime System.currentTimeMillis(); System.out.println(执行耗时 (endTime - startTime) ms); return result; } }关键点通过invocation.getArgs()获取MappedStatement和参数。通过BoundSql获取完整的 SQL。调用invocation.proceed()让 MyBatis 继续执行。2. StatementHandler 的使用改写 SQL如分页插件Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class PageInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取 StatementHandler StatementHandler statementHandler (StatementHandler) invocation.getTarget(); // 2. 通过反射获取 BoundSql MetaObject metaObject SystemMetaObject.forObject(statementHandler); BoundSql boundSql (BoundSql) metaObject.getValue(delegate.boundSql); // 3. 获取原始 SQL String originalSql boundSql.getSql(); // 4. 判断是否需要分页通过 ThreadLocal 传递分页参数 PageParam pageParam PageContext.get(); if (pageParam ! null) { // 5. 改写 SQL拼接 LIMIT String newSql originalSql LIMIT pageParam.getOffset() , pageParam.getPageSize(); // 6. 通过反射修改 BoundSql 中的 SQL Field field BoundSql.class.getDeclaredField(sql); field.setAccessible(true); field.set(boundSql, newSql); } // 7. 继续执行 return invocation.proceed(); } }关键点通过MetaObject反射获取BoundSql。修改BoundSql中的 SQL 语句。利用 ThreadLocal 传递分页参数。3. ParameterHandler 的使用修改参数值Intercepts({ Signature(type ParameterHandler.class, method setParameters, args {PreparedStatement.class}) }) public class ParameterEncryptInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取 ParameterHandler 和 PreparedStatement ParameterHandler parameterHandler (ParameterHandler) invocation.getTarget(); PreparedStatement ps (PreparedStatement) invocation.getArgs()[0]; // 2. 通过反射获取参数对象 MetaObject metaObject SystemMetaObject.forObject(parameterHandler); Object parameterObject metaObject.getValue(parameterObject); // 3. 如果参数是手机号进行加密 if (parameterObject instanceof User) { User user (User) parameterObject; String phone user.getPhone(); if (phone ! null phone.length() 11) { // 加密手机号示例替换中间4位 user.setPhone(phone.substring(0, 3) **** phone.substring(7)); } } // 4. 继续执行这里会调用 JDBC 的 setXxx 方法 return invocation.proceed(); } }关键点通过反射获取parameterObject传入的参数对象。修改参数值后继续执行setParametersMyBatis 会用修改后的值设置到PreparedStatement。4. ResultSetHandler 的使用修改返回结果Intercepts({ Signature(type ResultSetHandler.class, method handleResultSets, args {Statement.class}) }) public class ResultDecryptInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 1. 继续执行原方法获取结果 Object result invocation.proceed(); // 2. 如果结果是 List遍历解密 if (result instanceof List) { List? list (List?) result; for (Object obj : list) { if (obj instanceof User) { User user (User) obj; String phone user.getPhone(); if (phone ! null phone.contains(****)) { // 解密示例这里简单还原实际需要真正的解密算法 user.setPhone(phone.replace(****, 1234)); } } } } return result; } }关键点先调用invocation.proceed()获取结果再修改结果。在返回之前对结果集中的敏感字段进行解密。三、四种组件的插件使用场景总结组件拦截方法典型应用场景Executorquery(),update()SQL 审计、慢查询日志、全局限流StatementHandlerprepare(),parameterize(),query()分页插件PageHelper、SQL 改写、表名替换ParameterHandlersetParameters()参数加密、参数校验、参数转换ResultSetHandlerhandleResultSets()结果加密/解密、字段脱敏、结果转换四、重要提示不要在插件中频繁创建对象插件会拦截每次 SQL 执行性能敏感避免在intercept方法中做重操作。记得调用invocation.proceed()否则 MyBatis 不会继续执行SQL 不会真正执行。注意拦截顺序多个插件会按配置顺序形成责任链顺序会影响最终结果如分页插件通常在 SQL 改写插件之前。反射有风险使用MetaObject或原生反射时注意字段名可能在不同 MyBatis 版本中有变化。