JAVA面试宝典 - 《MyBatis 进阶:插件开发与二级缓存》
文章目录
- 🚀《MyBatis 进阶:插件开发与二级缓存》
- 一、开场引导:MyBatis 不只是 ORM 工具
- 二、MyBatis 插件机制详解
- 2.1 插件本质是“执行栈拦截器”
- 2.2 插件开发实战:记录 SQL 执行耗时
- 步骤一:实现 `Interceptor`
- 步骤二:注册插件
- 2.3 插件原理揭秘
- 三、MyBatis 二级缓存机制
- 3.1 一级缓存 vs 二级缓存
- 3.2 启用二级缓存的步骤
- 3.3 缓存失效与扩展
- 四、实战案例:自定义 Cache 接入 Redis
- 4.1 实现 Cache 接口
- 4.2 注册自定义缓存
- 五、常见问题与性能建议
- 六、总结
- 📚 推荐阅读
🚀《MyBatis 进阶:插件开发与二级缓存》
🎯 目标读者
已掌握 MyBatis 基础映射与 Mapper 使用,
想深入了解 MyBatis 插件机制与二级缓存原理及实践。
一、开场引导:MyBatis 不只是 ORM 工具
MyBatis 以 灵活、可插拔、轻量级 著称,但你是否真正了解它底层“魔法”?
- 插件 能干预 SQL 执行流程:日志、权限、监控……
- 二级缓存 跨
SqlSession
提升查询性能:内存、分布式都可扩展!
问题抛出:
- 如何开发一个统计 SQL 执行耗时的插件?
- 二级缓存如何从内存跑到 Redis?
二、MyBatis 插件机制详解
2.1 插件本质是“执行栈拦截器”
类比:Spring AOP 是“分层切面”,MyBatis 插件更像给执行栈每一站“排队拦车”——
每到关键节点,拦下并加入自定义逻辑,再放行。
可拦截的四大方法(对应核心四大处理器):
处理器 | 可拦截方法 |
---|---|
Executor | query() 、update() |
ParameterHandler | setParameters() |
ResultSetHandler | handleResultSets() |
StatementHandler | prepare() 、parameterize() 、batch() |
2.2 插件开发实战:记录 SQL 执行耗时
步骤一:实现 Interceptor
@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SqlTimingInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {long start = System.nanoTime();Object result = invocation.proceed();long elapsedMs = (System.nanoTime() - start) / 1_000_000;MappedStatement ms = (MappedStatement) invocation.getArgs()[0];if (elapsedMs > 100) {System.err.printf("[Slow SQL] %s took %d ms%n", ms.getId(), elapsedMs);}return result;}@Override public Object plugin(Object target) {return Plugin.wrap(target, this);}@Override public void setProperties(Properties props) { }
}
步骤二:注册插件
<plugins><plugin interceptor="com.example.mybatis.SqlTimingInterceptor"/>
</plugins>
2.3 插件原理揭秘
- Plugin.wrap(target, interceptor)
- 如果目标类型匹配 @Signature,则用 JDK 动态代理或 CGLIB 生成代理对象。
- 链式拦截
- 多个插件按配置顺序“嵌套”,先注册的最外层,最后执行的最内层。
- 执行流程
ExecutorImpl.query() → interceptor1.intercept() → interceptor2.intercept() → 实际 query 方法
三、MyBatis 二级缓存机制
3.1 一级缓存 vs 二级缓存
缓存类型 | 范围 | 生命周期 | 默认开启 | 可配置性 |
---|---|---|---|---|
一级缓存 | 单个 SqlSession | 会话内 | 是 | 不可关闭 |
二级缓存 | Mapper 映射空间 | 跨 SqlSession | 否 | 可开启/禁用 |
-
一级缓存:类似图书馆阅览室,只在“当天”可重复借阅。
-
二级缓存:像书架借书,可以跨会话“长期”缓存(甚至跨进程)。
3.2 启用二级缓存的步骤
- Mapper 接口/映射文件 加上注解或 XML:
@CacheNamespace(implementation=RedisCache.class, eviction=FifoCache.class, flushInterval=60000)
public interface UserMapper { ... }
- 实体类实现 Serializable
- 查询方法返回可序列化对象,不能是流或 ResultHandler
写操作(insert/update/delete)会清空对应 namespace 下的二级缓存。
3.3 缓存失效与扩展
- Session 关闭:一级缓存清空
- 写操作:清空二级缓存
- 命名空间隔离:不同 Mapper 之间不共享
- 自定义缓存:可实现 org.apache.ibatis.cache.Cache 接口,用 Redis、Guava 等
四、实战案例:自定义 Cache 接入 Redis
4.1 实现 Cache 接口
public class RedisCache implements Cache {private final String id;private RedisTemplate<String,Object> redis;public RedisCache(String id) { this.id = id; }@Override public String getId() { return id; }@Override public void putObject(Object key, Object value) {redis.opsForHash().put(id, key.toString(), value);}@Override public Object getObject(Object key) {return redis.opsForHash().get(id, key.toString());}@Override public Object removeObject(Object key) {return redis.opsForHash().delete(id, key.toString());}@Override public void clear() {redis.delete(id);}@Override public int getSize() {return redis.opsForHash().size(id).intValue();}
}
4.2 注册自定义缓存
<cache type="com.example.mybatis.RedisCache"/>
发布到分布式集群时,所有节点都可共享同一 Redis 二级缓存。
五、常见问题与性能建议
问题 | 建议 |
---|---|
插件过多影响性能 | 精简插件链,按需拦截;生产环境剔除非必要插件 |
二级缓存命中率低 | 减少写操作频率;针对热点数据单独设计缓存清理策略 |
数据一致性难保证 | 配合消息队列或 Redis 发布/订阅实现失效通知 |
统计所有 SQL | 在 Executor.query() 与 update() 中同时拦截 |
六、总结
- 插件机制:通过 Interceptor 拦截执行栈,灵活扩展 SQL 监控、审计、权限等功能。
- 二级缓存:跨会话缓存结果,默认内存实现,可自定义 Redis、Guava 等持久化方式。
- 进阶能力:掌握插件与缓存设计,意味着你已步入 MyBatis 的“进阶圈”,能让 ORM 不止“映射”,还能“加速”与“智能化”!
📚 推荐阅读
- MyBatis 官方文档:插件开发 & 缓存章节
- MyBatis 源码深度解析
- Redis 官方文档:Hash 结构与内存优化