你是否想过在MyBatis执行SQL时自动添加分页功能?或者统一加密数据库敏感字段?MyBatis插件就是实现这些黑科技的终极武器!本文将带你彻底掌握插件开发的核心原理与实战技巧!
场景 | 实现效果 | 代表插件 |
---|---|---|
SQL分页 | 自动添加LIMIT/OFFSET | PageHelper |
字段加解密 | 出入参自动加解密 | CryptoInterceptor |
SQL性能监控 | 记录慢查询并告警 | SlowQueryInterceptor |
多租户隔离 | 自动添加租户ID过滤条件 | TenantInterceptor |
审计日志 | 记录数据变更日志 | AuditLogInterceptor |
关键机制:
拦截对象 | 作用 | 可拦截方法 |
---|---|---|
Executor | SQL执行器 | update, query, commit等 |
ParameterHandler | 参数处理器 | setParameters |
ResultSetHandler | 结果集处理器 | handleResultSets |
StatementHandler | SQL语句处理器 | prepare, parameterize等 |
@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 SqlTimerInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(SqlTimerInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取SQL信息
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
String sqlId = ms.getId();
// 2. 记录开始时间
long start = System.currentTimeMillis();
try {
// 3. 执行原方法
return invocation.proceed();
} finally {
// 4. 计算耗时
long time = System.currentTimeMillis() - start;
log.info("SQL执行耗时:{}ms - {}", time, sqlId);
// 5. 慢查询告警
if (time > 1000) {
log.warn("⚠️ 慢SQL警告:{} 执行耗时 {}ms", sqlId, time);
}
}
}
@Override
public Object plugin(Object target) {
// 创建代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 接收配置文件参数
String threshold = properties.getProperty("slowThreshold", "1000");
log.info("慢查询阈值:{}ms", threshold);
}
}
<plugins>
<plugin interceptor="com.example.SqlTimerInterceptor">
<property name="slowThreshold" value="500"/>
plugin>
plugins>
2023-08-20 14:30:22 INFO SqlTimerInterceptor: SQL执行耗时:45ms - com.example.UserDao.findById
2023-08-20 14:31:05 WARN SqlTimerInterceptor: ⚠️ 慢SQL警告:com.example.OrderDao.findComplexOrders 执行耗时 1203ms
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取参数对象
Object parameter = invocation.getArgs()[1];
// 获取参数处理器
MetaObject metaParam = SystemMetaObject.forObject(parameter);
String userName = (String) metaParam.getValue("userName");
// ... 业务逻辑
}
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
})
public class SqlModifierInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
// 修改原始SQL
String newSql = boundSql.getSql() + " LIMIT 10";
// 通过反射修改SQL
MetaObject metaSql = SystemMetaObject.forObject(boundSql);
metaSql.setValue("sql", newSql);
return invocation.proceed();
}
}
<plugins>
<plugin interceptor="com.example.PluginA"/>
<plugin interceptor="com.example.PluginB"/>
<plugin interceptor="com.example.PluginC"/>
plugins>
规则 | 正确示例 | 错误示例 | 后果 |
---|---|---|---|
不要修改Invocation参数 | 只读不写 | 修改args数组内容 | 破坏内部状态 |
必须调用invocation.proceed() | 在拦截器中调用proceed | 忘记调用proceed | SQL无法执行 |
谨慎修改目标对象 | 通过代理机制安全访问 | 直接修改原生对象属性 | 导致不可预知错误 |
@Override
public Object plugin(Object target) {
// 精确拦截:仅拦截Executor类型
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target; // 其他类型直接返回
}
@Intercepts({
@Signature(type = ParameterHandler.class,
method = "setParameters",
args = {PreparedStatement.class}),
@Signature(type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
})
public class CryptoInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 加密:参数设置时
if (invocation.getTarget() instanceof ParameterHandler) {
encryptParameters(invocation);
}
// 解密:结果集处理时
else if (invocation.getTarget() instanceof ResultSetHandler) {
Object result = invocation.proceed();
return decryptResult(result);
}
return invocation.proceed();
}
private void encryptParameters(Invocation invocation) {
// 获取参数对象并加密敏感字段
ParameterHandler ph = (ParameterHandler) invocation.getTarget();
MetaObject metaParam = SystemMetaObject.forObject(ph.getParameterObject());
if (metaParam.hasGetter("password")) {
String pwd = (String) metaParam.getValue("password");
metaParam.setValue("password", AES.encrypt(pwd));
}
}
private Object decryptResult(Object result) {
// 解密结果集中的敏感字段
if (result instanceof List) {
((List<?>) result).forEach(item -> {
if (item instanceof User) {
User user = (User) item;
user.setPassword(AES.decrypt(user.getPassword()));
}
});
}
return result;
}
}
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
})
public class TenantInterceptor implements Interceptor {
private final ThreadLocal<Long> tenantId = new ThreadLocal<>();
public void setTenantId(Long id) {
tenantId.set(id);
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 仅处理SELECT语句
if (sql.trim().toUpperCase().startsWith("SELECT")) {
Long tid = tenantId.get();
if (tid != null) {
// 添加租户过滤条件
String newSql = sql + " AND tenant_id = " + tid;
// 通过反射修改SQL
MetaObject metaSql = SystemMetaObject.forObject(boundSql);
metaSql.setValue("sql", newSql);
}
}
return invocation.proceed();
}
}
// 通过Spring配置管理插件
@Configuration
public class MyBatisPluginConfig {
@Bean
public SqlTimerInterceptor sqlTimerInterceptor() {
SqlTimerInterceptor interceptor = new SqlTimerInterceptor();
Properties props = new Properties();
props.setProperty("slowThreshold", "500");
interceptor.setProperties(props);
return interceptor;
}
@Bean
public TenantInterceptor tenantInterceptor() {
return new TenantInterceptor();
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.addInterceptor(sqlTimerInterceptor());
configuration.addInterceptor(tenantInterceptor());
};
}
}
问题:多次调用Plugin.wrap()
导致多层嵌套代理
解决:
@Override
public Object plugin(Object target) {
// 避免重复代理
if (target instanceof Interceptor) {
return target;
}
return Plugin.wrap(target, this);
}
问题:插件中使用了共享可变状态
解决:
// 错误:共享变量
private int queryCount;
// 正确:使用ThreadLocal
private ThreadLocal<Integer> queryCount = ThreadLocal.withInitial(() -> 0);
问题:在错误拦截点修改SQL
对比:
操作 | 最佳拦截点 | 错误选择 |
---|---|---|
修改SQL语句 | StatementHandler.prepare | Executor.query |
修改参数 | ParameterHandler.setParameters | StatementHandler.parameterize |
修改返回结果 | ResultSetHandler.handleResultSets | 无替代方案 |
终极建议:阅读优秀开源插件源码(如PageHelper)是提升的最佳途径!
插件能力自测:
现在就开始你的第一个插件开发吧! 尝试实现一个简单的SQL日志美化插件~ ✨