MyBatis插件拦截Executor.query方法详解

小豆丁 18小时前 ⋅ 4 阅读
聊天记录
头像

@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),

头像

你提到的 @Signature 是 MyBatis 框架中用于自定义插件(Interceptor)的注解,通常配合 @Intercepts 一起使用,用来声明要拦截的目标方法签名。

注解结构说明

JAVA
@Signature(type = Executor.class, 
          method = "query", 
          args = {MappedStatement.class, Object.class, RowBounds.class, 
                  ResultHandler.class, CacheKey.class, BoundSql.class})
  • type:指定要拦截的接口类,例如 ExecutorStatementHandlerParameterHandlerResultSetHandler 等核心对象。
  • method:要拦截的具体方法名称,比如 queryupdateprepare 等。
  • args:该方法对应的完整参数类型数组(顺序需与原始方法一致)。MyBatis 通过参数类型来唯一确定重载方法。

使用场景示例

当你编写一个 MyBatis 插件时,会这样定义:

JAVA
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, 
                ResultHandler.class, CacheKey.class, BoundSql.class}
    )
})
public class MyPlugin implements Interceptor {
    // 实现 intercept 等方法
}

这样就能拦截 Executor.query(...) 这个六参数的重载版本,常用于 SQL 执行前后的增强(如分页、审计、缓存等)。

如果你有更具体的需求(比如想了解某个参数的作用、或者如何实现拦截逻辑),可以进一步提问,我会帮你详细解答。

头像

args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}

头像

你列出的这六个参数,对应的是 MyBatis 中 Executor 接口的一个特定重载方法:

JAVA
<E> List<E> query(MappedStatement ms, 
                  Object parameter, 
                  RowBounds rowBounds, 
                  ResultHandler resultHandler, 
                  CacheKey cacheKey, 
                  BoundSql boundSql) throws SQLException;

这是 MyBatis 内部执行查询时最完整的调用路径之一,通常由缓存模块或嵌套查询触发。下面逐个解释每个参数的作用:

参数 类型 作用
MappedStatement MappedStatement 封装了映射语句的全部信息,包括SQL、参数映射、结果映射、缓存配置等。
Object 用户传入的参数对象 可以是简单类型、POJO、Map或注解参数,最终会被转换成SQL参数。
RowBounds RowBounds 用于物理分页(offset + limit),但MyBatis默认只做内存分页,实际分页通常通过插件改写SQL实现。
ResultHandler ResultHandler 结果处理器,允许自定义处理每行结果(如流式读取),为null时使用默认的DefaultResultHandler。
CacheKey CacheKey 二级缓存的键对象,包含SQL、参数、环境等信息,用于判断缓存是否命中。
BoundSql BoundSql 已经解析好的SQL语句(含占位符?)、参数映射列表以及额外参数。

为什么这个签名这么长?

因为 MyBatis 在执行一次完整查询时,需要同时携带:

  • 配置信息(MappedStatement)
  • 用户输入(parameter)
  • 分页控制(RowBounds)
  • 结果回调(ResultHandler)
  • 缓存键(CacheKey)—— 提前计算好,避免重复计算
  • 预编译SQL(BoundSql)—— 也提前解析好,减少重复工作

当你在插件中使用这个签名拦截时,可以访问到上述所有上下文,从而实现细粒度的增强,例如:

  • 修改 BoundSql 中的 SQL 语句(添加分页、租户过滤)
  • 替换 CacheKey 强制刷新缓存
  • ResultHandler 中做结果加密/脱敏

如果你需要我展示一个实际的插件拦截示例,或者想了解其他重载版本(如只有4个参数的 query 方法),请告诉我。

头像

缓存键(CacheKey)——

头像

CacheKey 是 MyBatis 中用于标识 二级缓存 中一条查询结果的唯一键。它的设计目标是保证:相同的查询请求(相同 SQL + 相同参数 + 相同环境)返回相同的 CacheKey,从而复用缓存数据。


CacheKey 的核心组成

MyBatis 的 CacheKey 通过以下要素来计算哈希值:

  1. MappedStatement ID —— 映射语句的唯一标识(如 com.example.mapper.UserMapper.findById)。
  2. 参数值列表 —— 用户传入的实际参数(按顺序拼接)。
  3. RowBounds 的 offset 和 limit —— 分页信息。
  4. 环境标识 —— 包括 Environment.idSqlSessionFactory 的哈希码等,防止不同数据源混淆。
  5. BoundSql 的 SQL 语句 —— 已经解析完成的 SQL(含 ? 占位符)。

> 实际上,CacheKey 内部维护了一个 List&lt;Object&gt; updateList 和一个 int multiplier(默认37),通过迭代所有要素并乘以乘数累加得到最终的 hashCode。


为什么需要显式传递 CacheKey?

在六参数版本的 query() 方法中,CacheKey 是在进入该方法之前就已经由 MyBatis 计算好的。这样做有两个好处:

  • 避免重复计算:如果同一个查询先检查一级缓存(PerpetualCache),再尝试二级缓存,不需要重新计算键。
  • 允许插件干预:插件可以在 intercept() 中修改 CacheKey(例如添加租户ID、忽略某些参数),从而影响缓存行为。

典型使用场景

1. 强制刷新缓存

JAVA
@Override
public Object intercept(Invocation invocation) throws Throwable {
    Object[] args = invocation.getArgs();
    CacheKey cacheKey = (CacheKey) args[4];
    // 修改 cacheKey 使其不匹配现有缓存(例如追加随机数)
    cacheKey.update("FORCE_REFRESH_" + System.currentTimeMillis());
    return invocation.proceed();
}

2. 基于租户隔离缓存

JAVA
cacheKey.update(tenantId); // 将租户ID加入键计算

3. 调试 / 日志输出

打印 cacheKey.hashCode() 观察缓存命中情况。


注意事项

  • 一级缓存(SqlSession 级别) 不使用 CacheKey,而是使用简单的 HashMap,键是 MappedStatement + parameter + RowBounds
  • 二级缓存(namespace 级别) 才依赖 CacheKey,且必须开启 &lt;cache&gt; 标签或使用第三方缓存库(如 Redis)。
  • 如果你的插件拦截的是四参数版本的 query()(没有 CacheKeyBoundSql),那么这两个参数会在内部自动构造,插件无法直接干预。

如果你需要深入了解 CacheKey 的源码实现(org.apache.ibatis.cache.CacheKey),或者想知道如何在 Spring Boot 项目中调试二级缓存,请告诉我。

全部评论: 0

    我有话说: