boxmoe_header_banner_img

Hello! 欢迎来到悠悠畅享网!

文章导读

mybatis 如何编写一个自定义插件?


avatar
作者 2025年8月31日 13

mybatis自定义插件通过实现Interceptor接口,结合@Intercepts和@Signature注解拦截Executor、ParameterHandler、ResultSetHandler、StatementHandler四大接口,在不修改源码的前提下,于sql执行关键节点插入逻辑,实现功能扩展、性能监控等;需注意调用invocation.proceed()、避免性能开销、处理多插件顺序及线程安全,并确保外部操作与事务一致性。

mybatis 如何编写一个自定义插件?

MyBatis自定义插件的核心,在于它提供了一种非侵入式的能力,让你能在MyBatis执行SQL的生命周期中,插入自己的逻辑。简单来说,它就像在MyBatis内部流程的几个关键节点上安插了“哨兵”,这些“哨兵”可以在数据准备、SQL执行、结果处理等环节,读取、修改甚至替换掉原本的行为,而这一切都不需要你动MyBatis源码一根指头。这对于功能扩展、性能监控、日志记录或者数据权限控制来说,简直是神来之笔。

解决方案

要编写一个MyBatis自定义插件,你主要需要实现MyBatis提供的

Interceptor

接口。这个接口有三个核心方法:

intercept

plugin

setProperties

  1. 实现
    Interceptor

    接口:这是插件的骨架。

  2. 使用
    @Intercepts

    @Signature

    注解:这些注解是告诉MyBatis你的插件想要拦截哪个接口的哪个方法。一个插件可以拦截多个方法,甚至多个接口。

    • @Intercepts

      : 标记这是一个拦截器。

    • @Signature

      : 定义要拦截的方法签名。

      type

      指定要拦截的接口(

      Executor

      ,

      ParameterHandler

      ,

      ResultSetHandler

      ,

      StatementHandler

      之一),

      method

      指定该接口下的方法名,

      args

      指定该方法的参数类型列表。

  3. 实现
    intercept(Invocation invocation)

    方法:这是插件的核心逻辑所在。

    invocation

    对象包含了被拦截的目标对象(

    getTarget()

    )、被拦截的方法(

    getMethod()

    )以及方法参数(

    getArgs()

    )。你可以在这里执行你的业务逻辑,然后必须调用

    invocation.proceed()

    来放行,让MyBatis继续执行原来的流程,否则整个链条就断了。

  4. 实现
    plugin(Object target)

    方法:这个方法的作用是,当MyBatis发现你的插件需要拦截某个目标对象时,会调用这个方法来生成一个代理对象。你通常会在这里返回

    Plugin.wrap(target, this)

    ,让MyBatis帮你生成一个代理,以便拦截器能够真正地拦截到目标对象的方法调用。

  5. 实现
    setProperties(Properties properties)

    方法:这个方法用于在插件初始化时,接收你在MyBatis配置文件中为插件设置的属性。如果你需要一些配置参数,可以通过这里获取。

  6. mybatis-config.xml

    中注册插件:最后一步,你需要告诉MyBatis你的插件在哪里。

这是一个简单的示例,展示如何拦截

Executor

update

方法,并打印SQL执行时间:

import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds;  import Java.util.Properties;  @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 PerformanceInterceptor implements Interceptor {      private String authorName; // 演示如何获取配置属性      @Override     public Object intercept(Invocation invocation) throws Throwable {         long start = System.currentTimeMillis();         try {             // 这是最关键的一步,放行,让MyBatis继续执行原有的方法             Object result = invocation.proceed();             return result;         } finally {             long end = System.currentTimeMillis();             System.out.println("方法:" + invocation.getMethod().getName() + " 执行耗时:" + (end - start) + "ms. 作者: " + authorName);             // 还可以获取更多信息,比如 invocation.getTarget() 得到被代理的Executor对象             // 或者 invocation.getArgs() 得到方法的参数         }     }      @Override     public Object plugin(Object target) {         // 返回一个代理对象,如果目标对象是Executor,就为其创建代理         // 这样MyBatis在调用Executor的方法时,就会先经过这个拦截器         return Plugin.wrap(target, this);     }      @Override     public void setProperties(Properties properties) {         // 在这里可以获取到MyBatis配置文件中为该插件设置的属性         this.authorName = properties.getProperty("author");         if (this.authorName == null) {             this.authorName = "Unknown";         }         System.out.println("PerformanceInterceptor 初始化,获取到属性 author=" + authorName);     } }

然后在

mybatis-config.xml

中注册:

<configuration>     <!-- ... 其他配置 ... -->     <plugins>         <plugin interceptor="your.package.PerformanceInterceptor">             <property name="author" value="YourNameHere"/>         </plugin>     </plugins>     <!-- ... 其他配置 ... --> </configuration>

MyBatis插件能拦截哪些核心操作?

MyBatis的插件机制,本质上是基于JDK动态代理实现的。它允许你拦截MyBatis内部的四个核心接口。理解这些接口各自的职责,是写好插件的前提。

  1. Executor

    (执行器):这是MyBatis执行sql语句的核心。所有的SQL操作,无论是查询还是更新,最终都会通过

    Executor

    来完成。拦截

    Executor

    ,你可以在SQL执行前进行一些预处理,比如权限校验、数据源切换,或者在执行后进行性能统计。这是最常用的拦截点,因为它处于SQL执行的“大门口”。例如,你可以在这里获取到

    MappedStatement

    对象,从而知道当前执行的是哪个SQL。

  2. ParameterHandler

    (参数处理器):这个接口负责处理SQL语句的参数。它会把Java对象中的参数,正确地设置到JDBC的

    PreparedStatement

    中。如果你需要对传入的参数进行加密、脱敏,或者进行一些复杂的参数转换,

    ParameterHandler

    就是你的目标。比如,你想把一个Java枚举类型自动转换为数据库中的特定字符串或数字,就可以在这里动手脚。

  3. ResultSetHandler

    (结果集处理器):顾名思义,它负责处理JDBC返回的结果集(

    ResultSet

    ),并将其映射成Java对象。如果你需要对从数据库中读取出来的数据进行二次处理,比如解密、格式化,或者进行一些聚合操作,那么就应该拦截

    ResultSetHandler

    。这对于统一的数据清洗或视图层数据准备很有用。

  4. StatementHandler

    (语句处理器):这个接口负责构建JDBC的

    Statement

    对象,包括SQL语句的准备、参数的设置以及结果集的处理。它更接近于JDBC层的操作。拦截

    StatementHandler

    ,你可以在SQL语句真正发送到数据库之前,对其进行修改(比如动态添加

    WHERE

    条件)、分页SQL的重写,或者获取到最终要执行的SQL字符串。这对于实现复杂的分页逻辑或者多租户数据隔离尤其有用。

通常情况下,我们最常打交道的是

Executor

StatementHandler

,因为它们能让你在SQL执行的关键节点进行干预。而

ParameterHandler

ResultSetHandler

则更专注于数据的“进出”处理。

编写MyBatis插件时常见的“坑”有哪些?

写MyBatis插件,虽然功能强大,但确实有些地方稍不注意就会掉坑里。我个人就遇到过几次,挺让人抓狂的。

  1. 忘记调用
    invocation.proceed()

    :这是最常见的错误,没有之一。如果你在

    intercept

    方法里执行完自己的逻辑后,忘记调用

    invocation.proceed()

    ,那么MyBatis的原始流程就会被中断,SQL根本不会被执行,或者结果集无法被正确处理。这就像你在一个流水线上,把东西拿过来检查了,但没放回传送带,后面的人就没法继续工作了。

  2. ClassCastException

    :MyBatis的插件机制是通过动态代理实现的。

    invocation.getTarget()

    返回的是一个代理对象,它可能并不是你预期的原始对象类型。在某些情况下,你可能需要进行类型判断或者通过更通用的接口来操作。尤其是在处理

    args

    参数时,务必小心参数的类型和顺序,否则很容易在运行时抛出类型转换异常。

  3. 性能开销:插件会在每次被拦截的方法调用时执行。如果你在
    intercept

    方法里做了大量耗时操作,比如复杂的计算、网络请求或者文件I/O,那对整个应用的性能影响是巨大的。插件应该尽可能地轻量级,或者只在必要时才执行重型操作。我曾经见过有人在拦截器里做rpc调用,结果整个系统慢得像蜗牛。

  4. 多插件的执行顺序:当配置了多个插件时,它们的执行顺序是按照在
    mybatis-config.xml

    中配置的顺序来决定的。前一个插件的

    plugin

    方法会返回一个代理对象,这个代理对象又会作为下一个插件的

    target

    。如果插件之间有依赖关系,或者对同一个目标对象进行修改,顺序就变得非常重要。调试这种问题会比较头疼,因为你得理清代理链。

  5. MappedStatement

    的误解:在拦截

    Executor

    时,

    MappedStatement

    是一个非常重要的参数,它包含了SQL ID、SQL类型、参数映射等大量信息。但

    MappedStatement

    是MyBatis在启动时构建的,它是不可变的。如果你想修改SQL,不能直接修改

    MappedStatement

    ,而是需要通过创建新的

    MappedStatement

    来替换,或者在

    StatementHandler

    中直接修改SQL字符串。直接修改

    MappedStatement

    的属性是无效的,因为你操作的是一个不可变对象的引用。

  6. 线程安全问题:如果你的插件内部持有状态(比如一个计数器、一个缓存),并且这个状态在多个线程之间共享,那么你必须确保它是线程安全的。否则,在高并发环境下,数据可能会出现混乱。大部分情况下,插件的
    intercept

    方法应该是无状态的,或者只处理当前请求的局部状态。

自定义插件如何与MyBatis的事务管理协同工作?

自定义MyBatis插件与事务管理协同工作,其实是一个比较自然的过程,因为插件本身就是MyBatis执行流程的一部分,它运行在MyBatis所管理的事务上下文之内。

  1. 事务边界内的操作:当你通过插件拦截

    Executor

    并执行SQL操作时,例如在

    update

    query

    方法拦截器中进行数据修改,这些修改是默认包含在当前MyBatis事务中的。这意味着,如果外部调用者开启了一个事务,你的插件内部进行的任何数据库操作,都将受这个事务的控制。如果事务最终回滚,你的插件所做的修改也会随之回滚。这通常是我们期望的行为,保持了数据的一致性。

  2. 外部操作与事务:如果你的插件在

    intercept

    方法中,除了调用

    invocation.proceed()

    之外,还执行了额外的、与当前MyBatis事务无关的操作,比如:

    • 向另一个数据库写入日志。
    • 调用一个外部API。
    • 写入文件系统。 这些操作不会被当前MyBatis事务管理。如果MyBatis事务回滚,这些外部操作不会自动回滚。在这种情况下,你需要自己考虑这些操作的原子性和一致性。你可能需要:
    • 在插件内部开启一个独立的事务来管理这些外部操作。
    • 使用消息队列等异步机制来解耦,或者通过补偿事务来处理回滚情况。
    • 确保这些外部操作本身是幂等的,即使重复执行也不会导致问题。
  3. Executor

    拦截与事务控制:在

    Executor

    层面的拦截器,对于理解事务行为尤其重要。MyBatis的事务管理主要由

    SqlSession

    和其内部的

    Executor

    来协调。当你拦截

    Executor

    时,你实际上是在事务的“核心”部分进行操作。

    • 你可以获取到当前
      SqlSession

      ,甚至可以获取到其内部的

      Transaction

      对象(虽然直接操作

      Transaction

      不常见,但可以获取其状态)。

    • 如果你在
      intercept

      方法中抛出异常,并且这个异常没有被捕获,MyBatis的事务管理器通常会捕获这个异常并触发事务回滚。这是MyBatis事务的默认行为,插件的行为会融入其中。

总的来说,插件在MyBatis事务的“保护伞”下运行。大部分情况下,你不需要为插件的数据库操作单独考虑事务,它们会自然地融入当前会话的事务中。只有当你执行与MyBatis当前数据库连接无关的外部操作时,才需要特别注意事务边界和一致性问题。



评论(已关闭)

评论已关闭

text=ZqhQzanResources