@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
你提到的 @Signature 是 MyBatis 框架中用于自定义插件(Interceptor)的注解,通常配合 @Intercepts 一起使用,用来声明要拦截的目标方法签名。
注解结构说明
@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class, CacheKey.class, BoundSql.class})
type:指定要拦截的接口类,例如Executor、StatementHandler、ParameterHandler、ResultSetHandler等核心对象。method:要拦截的具体方法名称,比如query、update、prepare等。args:该方法对应的完整参数类型数组(顺序需与原始方法一致)。MyBatis 通过参数类型来唯一确定重载方法。
使用场景示例
当你编写一个 MyBatis 插件时,会这样定义:
@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 接口的一个特定重载方法:
<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 通过以下要素来计算哈希值:
- MappedStatement ID —— 映射语句的唯一标识(如
com.example.mapper.UserMapper.findById)。 - 参数值列表 —— 用户传入的实际参数(按顺序拼接)。
- RowBounds 的 offset 和 limit —— 分页信息。
- 环境标识 —— 包括
Environment.id、SqlSessionFactory的哈希码等,防止不同数据源混淆。 - BoundSql 的 SQL 语句 —— 已经解析完成的 SQL(含
?占位符)。
> 实际上,CacheKey 内部维护了一个 List<Object> updateList 和一个 int multiplier(默认37),通过迭代所有要素并乘以乘数累加得到最终的 hashCode。
为什么需要显式传递 CacheKey?
在六参数版本的 query() 方法中,CacheKey 是在进入该方法之前就已经由 MyBatis 计算好的。这样做有两个好处:
- 避免重复计算:如果同一个查询先检查一级缓存(PerpetualCache),再尝试二级缓存,不需要重新计算键。
- 允许插件干预:插件可以在
intercept()中修改CacheKey(例如添加租户ID、忽略某些参数),从而影响缓存行为。
典型使用场景
1. 强制刷新缓存
@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. 基于租户隔离缓存
cacheKey.update(tenantId); // 将租户ID加入键计算
3. 调试 / 日志输出
打印 cacheKey.hashCode() 观察缓存命中情况。
注意事项
- 一级缓存(SqlSession 级别) 不使用
CacheKey,而是使用简单的HashMap,键是MappedStatement + parameter + RowBounds。 - 二级缓存(namespace 级别) 才依赖
CacheKey,且必须开启<cache>标签或使用第三方缓存库(如 Redis)。 - 如果你的插件拦截的是四参数版本的
query()(没有CacheKey和BoundSql),那么这两个参数会在内部自动构造,插件无法直接干预。
如果你需要深入了解 CacheKey 的源码实现(org.apache.ibatis.cache.CacheKey),或者想知道如何在 Spring Boot 项目中调试二级缓存,请告诉我。