1. 为什么我们需要一个MyBatis SQL执行监控插件
在日常开发中,我们经常遇到这样的场景:某个接口响应突然变慢,排查时发现是SQL执行效率问题,但苦于没有直接的执行日志;或者测试环境出现数据异常,却无法快速定位是哪条SQL语句导致的问题。这些问题都指向一个共同的需求——我们需要清晰地掌握MyBatis执行的每一条SQL及其耗时情况。
传统的解决方案通常是在日志配置中开启DEBUG级别,但这会带来两个明显问题:一是日志输出过于冗杂,真正需要的信息被淹没在海量日志中;二是单纯的SQL语句打印无法直观反映执行耗时,需要人工计算时间差。而一个专门的MyBatis插件可以完美解决这些痛点。
我曾在一次性能优化项目中,花费了整整两天时间通过日志拼凑SQL执行链路。正是那次经历让我下定决心开发这个插件。现在,每当团队新成员看到插件输出的清晰SQL执行报告时,都会感叹:"早知道有这个就好了!"
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 插件核心设计思路与技术选型
2.1 MyBatis插件机制解析
MyBatis提供了强大的插件(Interceptor)机制,允许我们在四大核心对象的方法执行前后插入自定义逻辑:
- Executor (update, query, flushStatements等方法)
- ParameterHandler (getParameterObject, setParameters方法)
- ResultSetHandler (handleResultSets, handleOutputParameters方法)
- StatementHandler (prepare, parameterize, batch, update, query等方法)
对于SQL监控这个需求,StatementHandler是最合适的切入点。具体来说,我们需要拦截:
- prepare()方法:获取原始SQL语句
- parameterize()方法:获取最终带参数的SQL
- query()/update()方法:计算执行耗时
2.2 耗时计算的精准实现方案
简单的System.currentTimeMillis()差值计算在大多数场景下已经足够,但在高并发或需要纳秒级精度的场景下可能不够准确。经过对比测试,我最终选择了两种方案:
- 对于常规应用:使用StopWatch(Spring Core提供)
java复制StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 执行SQL
stopWatch.stop();
long cost = stopWatch.getTotalTimeMillis();
- 对于高性能场景:使用System.nanoTime()
java复制long start = System.nanoTime();
// 执行SQL
long cost = (System.nanoTime() - start) / 1000000;
注意:避免在同一个拦截器中混用两种计时方式,这会导致时间单位不统一的问题。我在初期实现时就犯过这个错误,导致生成的报告时间单位混乱。
2.3 SQL格式化输出方案
直接打印的SQL往往是一行长长的字符串,可读性差。经过调研,我采用了以下格式化方案:
- 使用JSqlParser解析SQL结构:
java复制Statement statement = CCJSqlParserUtil.parse(sql);
String formattedSql = statement.toString();
- 对于简单SQL,也可以使用正则表达式美化:
java复制sql = sql.replaceAll("(?i)\\b(select|from|where|and|or|group by|order by|limit)\\b", "\n$1");
3. 插件完整实现步骤
3.1 基础拦截器框架搭建
首先创建基础的Interceptor实现类:
java复制@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "parameterize",
args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update",
args = {Statement.class})
})
public class SqlMonitorInterceptor implements Interceptor {
// 实现代码将在下面展开
}
3.2 SQL信息采集实现
在prepare阶段获取原始SQL:
java复制@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
StatementHandler handler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(handler);
MappedStatement mappedStatement = (MappedStatement)
metaObject.getValue("delegate.mappedStatement");
if ("prepare".equals(methodName)) {
String originalSql = handler.getBoundSql().getSql();
// 存储到ThreadLocal中
SqlContextHolder.setOriginalSql(originalSql);
}
// 其他方法处理...
}
3.3 耗时计算与报告生成
完整的query/update方法拦截实现:
java复制if ("query".equals(methodName) || "update".equals(methodName)) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
return invocation.proceed();
} finally {
stopWatch.stop();
String formattedSql = formatSql(
SqlContextHolder.getOriginalSql(),
handler.getBoundSql().getParameterObject()
);
SqlReport report = new SqlReport(
mappedStatement.getId(),
formattedSql,
stopWatch.getTotalTimeMillis()
);
log.info(report.toString());
SqlContextHolder.clear();
}
}
3.4 线程安全的上下文管理
使用ThreadLocal保存SQL执行上下文:
java复制public class SqlContextHolder {
private static final ThreadLocal<String> SQL_HOLDER = new ThreadLocal<>();
public static void setOriginalSql(String sql) {
SQL_HOLDER.set(sql);
}
public static String getOriginalSql() {
return SQL_HOLDER.get();
}
public static void clear() {
SQL_HOLDER.remove();
}
}
4. 高级功能与生产级优化
4.1 慢SQL告警机制
在实际项目中,我们通常需要关注执行时间超过阈值的SQL:
java复制long slowSqlThreshold = 500; // 500ms
if (stopWatch.getTotalTimeMillis() > slowSqlThreshold) {
log.warn("Slow SQL detected: {}ms - {}",
stopWatch.getTotalTimeMillis(),
mappedStatement.getId());
// 可以集成邮件/钉钉告警
alertService.notifySlowSql(report);
}
4.2 SQL执行统计报表
收集一段时间内的SQL执行数据,生成统计报告:
java复制public class SqlStatistics {
private static final ConcurrentHashMap<String, SqlStats> STATS =
new ConcurrentHashMap<>();
public static void record(String sqlId, long cost) {
STATS.compute(sqlId, (k, v) -> {
if (v == null) {
return new SqlStats(cost);
}
v.record(cost);
return v;
});
}
public static void printReport() {
STATS.forEach((sqlId, stats) -> {
log.info("SQL Statistics for {}: {}", sqlId, stats);
});
}
static class SqlStats {
private long count;
private long totalCost;
private long maxCost;
private long minCost = Long.MAX_VALUE;
// 统计方法实现...
}
}
4.3 与Spring Boot的优雅集成
创建自动配置类,简化插件启用:
java复制@Configuration
@ConditionalOnClass(SqlMonitorInterceptor.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class MybatisMonitorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SqlMonitorInterceptor sqlMonitorInterceptor() {
return new SqlMonitorInterceptor();
}
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(sqlMonitorInterceptor());
};
}
}
5. 生产环境中的实战经验
5.1 性能影响评估与优化
在初次上线时,我们发现插件在高并发场景下增加了约3%的CPU开销。通过以下优化将开销降至1%以内:
- 使用缓存避免重复解析相同SQL
- 采样率控制(仅记录部分请求)
- 异步化日志记录
优化后的采样率控制实现:
java复制private static final AtomicLong COUNTER = new AtomicLong();
private static final int SAMPLE_RATE = 10; // 10%
if (COUNTER.incrementAndGet() % SAMPLE_RATE == 0) {
log.info(report.toString());
}
5.2 常见问题排查指南
问题1:插件不生效
- 检查顺序:确保插件是最后一个被添加的拦截器
- 解决方案:调整拦截器添加顺序
问题2:SQL日志缺失
- 检查点:ThreadLocal是否被意外清除
- 解决方案:确保所有代码路径都调用了clear()
问题3:参数显示为null
- 原因:parameterize方法未被正确拦截
- 解决方案:检查@Signature配置
5.3 与现有监控系统的集成
我们的插件可以与APM系统如SkyWalking、Pinpoint等配合使用:
java复制// 在SQL执行前后添加Trace
ActiveSpan span = ContextManager.createLocalSpan("SQL: " + mappedStatement.getId());
try {
span.tag("sql", formattedSql);
return invocation.proceed();
} finally {
span.tag("cost", stopWatch.getTotalTimeMillis() + "ms");
span.stop();
}
6. 插件效果展示与使用建议
6.1 典型输出示例
插件生成的报告格式如下:
code复制[SQL Monitor]
ID : com.example.mapper.UserMapper.selectById
SQL :
SELECT
id, username, email
FROM
user
WHERE
id = ?
TIME: 12ms
PARAM: [1]
6.2 不同环境下的配置建议
-
开发环境:
- 全量记录所有SQL
- 启用SQL格式化
- 设置慢SQL阈值为100ms
-
测试环境:
- 采样率设置为50%
- 启用慢SQL告警
- 记录执行参数
-
生产环境:
- 采样率设置为1-5%
- 仅记录慢SQL
- 禁用详细参数记录(避免敏感信息泄露)
6.3 扩展思路
基于这个基础插件,可以进一步开发:
- SQL执行计划分析
- 自动生成最优索引建议
- SQL注入检测
- 与CI/CD集成,防止性能回退
这个插件在我们团队已经稳定运行两年多,帮助定位了无数性能问题。特别是在最近一次大促前的压测中,通过插件发现的慢SQL优化直接让系统QPS提升了40%。希望这个实现方案也能为你的团队带来效率提升
