在 Java 持久层框架中,MyBatis 以其轻量级、灵活性和强大的 SQL 控制能力,成为众多项目的首选。本文将深入探讨 MyBatis 的两大进阶特性——插件(Interceptor)机制与缓存(Cache)机制,帮助你从"会用 MyBatis"迈向"精通 MyBatis"。
MyBatis 插件本质上是一个拦截器(Interceptor),它利用了 JDK 动态代理和责任链模式,允许我们在 SQL 执行的特定节点插入自定义逻辑,实现功能的横向扩展。
可拦截的四大对象:
| 拦截对象 | 说明 | 常见用途 |
|---|---|---|
Executor | 执行器,负责 SQL 执行 | 性能监控、分页插件 |
ParameterHandler | 参数处理器 | 参数加密、敏感字段脱敏 |
ResultSetHandler | 结果集处理器 | 结果脱敏、数据解密 |
StatementHandler | SQL 语句处理器 | SQL 改写、打印慢 SQL |
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class PerformanceMonitorPlugin implements Interceptor {
private long slowQueryThreshold; // 慢查询阈值(毫秒)
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 执行原方法
return invocation.proceed();
} finally {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > slowQueryThreshold) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
System.out.printf(
"[SLOW SQL] %s | 耗时: %dms | SQL: %s%n",
ms.getId(),
elapsed,
getSql(ms, parameter)
);
}
}
}
@Override
public Object plugin(Object target) {
// 使用 Plugin.wrap 创建代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.slowQueryThreshold = Long.parseLong(
properties.getProperty("slowQueryThreshold", "1000")
);
}
// 辅助方法:解析实际 SQL
private String getSql(MappedStatement ms, Object parameter) {
BoundSql boundSql = ms.getBoundSql(parameter);
return boundSql.getSql();
}
}
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="com.example.plugin.PerformanceMonitorPlugin">
<property name="slowQueryThreshold" value="500"/>
</plugin>
</plugins>
或使用 Spring Boot 配置:
@Configuration
public class MyBatisConfig {
@Bean
public PerformanceMonitorPlugin performanceMonitorPlugin() {
PerformanceMonitorPlugin plugin = new PerformanceMonitorPlugin();
Properties props = new Properties();
props.setProperty("slowQueryThreshold", "500");
plugin.setProperties(props);
return plugin;
}
}
多个插件按配置顺序形成拦截器链,层层嵌套。
PageHelper 是 MyBatis 最著名的插件之一,其核心原理就是拦截 Executor.query() 方法,在执行查询前:
SELECT COUNT(*) 获取总数LIMIT offset, pageSize// 简化版分页插件核心逻辑
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取分页参数
PageInfo pageInfo = PageContextHolder.getPageInfo();
if (pageInfo != null) {
// 2. 改写 SQL 为 COUNT 查询
String countSql = "SELECT COUNT(*) FROM (" + originalSql + ") tmp";
long total = executeCount(countSql);
// 3. 添加 LIMIT 子句
String pageSql = originalSql + " LIMIT " +
pageInfo.getOffset() + ", " + pageInfo.getPageSize();
// 4. 执行分页查询
List result = executeQuery(pageSql);
// 5. 封装分页结果
return new PageResult<>(result, total, pageInfo);
}
return invocation.proceed();
}
@Signature 的拦截范围,避免性能损耗MyBatis 提供了两级缓存体系,用于减少数据库访问次数,提升查询性能。
特点:
SqlSession 拥有自己的缓存工作原理:
// 一级缓存命中场景
SqlSession session = sqlSessionFactory.openSession();
// 第一次查询:访问数据库
User user1 = session.selectOne("getUserById", 1);
// 第二次查询:命中缓存,不访问数据库
User user2 = session.selectOne("getUserById", 1);
System.out.println(user1 == user2); // true,同一对象
session.close(); // 缓存清空
缓存失效条件:
| 操作 | 效果 |
|---|---|
执行 INSERT/UPDATE/DELETE | 自动清空当前 session 缓存 |
调用 session.clearCache() | 手动清空缓存 |
session.close() | 缓存销毁 |
flushCache="true" | 强制刷新缓存 |
特点:
Serializable配置步骤:
Step 1: 开启二级缓存全局配置
<!-- mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
Step 2: Mapper XML 中声明缓存
<!-- UserMapper.xml -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="false"
blocking="true"
/>
<!-- 或使用自定义缓存实现 -->
<cache type="com.example.cache.RedisCache"/>
缓存策略对比:
| 策略 | 说明 | 适用场景 |
|---|---|---|
LRU | 最近最少使用 | 默认策略,适合大多数场景 |
FIFO | 先进先出 | 数据访问具有时间局部性 |
SOFT | 软引用 | 内存紧张时自动回收 |
WEAK | 弱引用 | 更激进的内存回收策略 |
Step 3: POJO 实现 Serializable
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
// ... getters/setters
}
Step 4: 指定 statement 使用缓存
<select id="getUserById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 禁用某个查询的缓存 -->
<select id="getLatestData" resultType="Data" useCache="false">
SELECT * FROM data ORDER BY create_time DESC LIMIT 10
</select>
┌─────────────────┐
│ SqlSession 1 │
│ ┌───────────┐ │
│ │ 一级缓存 │ │
│ └─────┬─────┘ │
└────────┼────────┘
│ ① 查询
▼
┌─────────┐ ② 未命中
│ 二级缓存 │
└────┬────┘
│ ③ 未命中
▼
┌─────────┐
│ 数据库 │
└─────────┘
│ ④ 返回结果
▼ ⑤ 回填缓存
[一级 ← 二级 ← DB]
生产环境通常使用 Redis 作为二级缓存,实现集群共享:
public class RedisCache implements Cache {
private final String id;
private static JedisPool jedisPool;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
try (Jedis jedis = jedisPool.getResource()) {
String cacheKey = id + ":" + key.hashCode();
jedis.setex(cacheKey.getBytes(), 3600, serialize(value));
}
}
@Override
public Object getObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
String cacheKey = id + ":" + key.hashCode();
byte[] data = jedis.get(cacheKey.getBytes());
return data != null ? deserialize(data) : null;
}
}
@Override
public Object removeObject(Object key) {
Object old = getObject(key);
try (Jedis jedis = jedisPool.getResource()) {
jedis.del((id + ":" + key.hashCode()).getBytes());
}
return old;
}
@Override
public void clear() {
try (Jedis jedis = jedisPool.getResource()) {
Set<String> keys = jedis.keys(id + ":*");
for (String key : keys) {
jedis.del(key);
}
}
}
// 序列化/反序列化方法省略...
}
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class CacheWarmupPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
// 数据更新后触发缓存预热
if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
warmUpCache(ms);
}
return result;
}
private void warmUpCache(MappedStatement ms) {
// 异步加载热点数据到缓存
// ...
}
}
| 特性 | 插件机制 | 一级缓存 | 二级缓存 |
|---|---|---|---|
| 作用域 | 全局拦截 | SqlSession | Mapper namespace |
| 默认状态 | 需开发配置 | 自动开启 | 需手动开启 |
| 主要用途 | 功能扩展、监控 | 减少重复查询 | 跨 session 共享 |
| 线程安全 | 需注意 | Session 隔离 | 需保证 |
掌握 MyBatis 的插件与缓存机制,能让你在实际项目中:
希望本文能帮助你在 MyBatis 的使用上更进一步!
参考链接: