目录
[🎯 先说说我被慢SQL"折磨"的经历](#🎯 先说说我被慢SQL"折磨"的经历)
[✨ 摘要](#✨ 摘要)
[1. 插件不是魔法:先理解MyBatis的拦截机制](#1. 插件不是魔法:先理解MyBatis的拦截机制)
[1.1 MyBatis能拦截什么?](#1.1 MyBatis能拦截什么?)
[1.2 插件的工作原理:责任链模式](#1.2 插件的工作原理:责任链模式)
[2. 手把手:写你的第一个插件](#2. 手把手:写你的第一个插件)
[2.1 需求分析:我们要监控什么?](#2.1 需求分析:我们要监控什么?)
[2.2 项目结构设计](#2.2 项目结构设计)
[2.3 基础插件实现](#2.3 基础插件实现)
[3. 企业级SQL监控插件完整实现](#3. 企业级SQL监控插件完整实现)
[3.1 核心数据结构设计](#3.1 核心数据结构设计)
[3.2 完整插件实现](#3.2 完整插件实现)
[4. 高级功能:SQL统计与告警](#4. 高级功能:SQL统计与告警)
[4.1 SQL统计收集器](#4.1 SQL统计收集器)
[4.2 慢SQL告警器](#4.2 慢SQL告警器)
[5. 插件配置与使用](#5. 插件配置与使用)
[5.1 MyBatis配置](#5.1 MyBatis配置)
[5.2 Spring Boot配置](#5.2 Spring Boot配置)
[6. 性能测试与优化](#6. 性能测试与优化)
[6.1 插件性能影响测试](#6.1 插件性能影响测试)
[6.2 性能优化技巧](#6.2 性能优化技巧)
[7. 生产环境问题排查](#7. 生产环境问题排查)
[7.1 常见问题排查清单](#7.1 常见问题排查清单)
[7.2 监控指标](#7.2 监控指标)
[8. 高级功能扩展](#8. 高级功能扩展)
[8.1 SQL防注入检测](#8.1 SQL防注入检测)
[8.2 SQL执行计划分析](#8.2 SQL执行计划分析)
[9. 企业级最佳实践](#9. 企业级最佳实践)
[9.1 我的"插件开发军规"](#9.1 我的"插件开发军规")
[📜 第一条:明确职责](#📜 第一条:明确职责)
[📜 第二条:性能优先](#📜 第二条:性能优先)
[📜 第三条:配置化](#📜 第三条:配置化)
[📜 第四条:异常处理](#📜 第四条:异常处理)
[📜 第五条:兼容性](#📜 第五条:兼容性)
[9.2 生产环境部署检查清单](#9.2 生产环境部署检查清单)
[10. 最后的话](#10. 最后的话)
[📚 推荐阅读](#📚 推荐阅读)
🎯 先说说我被慢SQL"折磨"的经历
去年我们团队负责的支付系统,突然在双11前出现性能问题。用户反馈支付要等十几秒,DBA说数据库CPU都90%了,但就是不知道哪个SQL有问题。我们加了各种日志,还是定位不到慢SQL。
最后没办法,我花了一晚上写了个MyBatis插件,第二天就找到了罪魁祸首:一个被错误使用的联表查询,全表扫描了上百万数据。修复后,响应时间从15秒降到200毫秒。
但事情没完。上线后监控显示,有0.1%的请求还是很慢。又是排查了三天,发现是因为有人用${}做了动态排序,导致大量硬解析。这次我直接把插件升级,增加了SQL防注入检测。
这两次经历让我明白:不懂MyBatis插件开发,就等于开飞机没有仪表盘,出事了都不知道原因。
✨ 摘要
MyBatis插件(Plugin)是其框架扩展性的核心。本文深入剖析插件实现原理,手把手教你开发企业级SQL监控插件。从拦截器接口、责任链模式,到动态代理实现,完整展示SQL执行耗时监控、慢SQL告警、SQL防注入检测等功能的实现。通过性能压测数据和生产环境案例,提供插件开发的最佳实践和故障排查指南。
1. 插件不是魔法:先理解MyBatis的拦截机制

1.1 MyBatis能拦截什么?
很多人以为插件只能拦截SQL执行,太天真了!MyBatis允许你拦截四大核心组件:

图1:MyBatis可拦截的四大组件
每个组件的作用:
| 组件 | 拦截点 | 能干什么 | 实际用途 |
|---|---|---|---|
| Executor | update/query | 控制缓存、事务 | SQL重试、读写分离 |
| StatementHandler | prepare/parameterize/query | 修改SQL、设置超时 | SQL监控、分页 |
| ParameterHandler | setParameters | 参数处理 | 参数加密、脱敏 |
| ResultSetHandler | handleResultSets | 结果集处理 | 数据脱敏、格式化 |
1.2 插件的工作原理:责任链模式
这是理解插件开发的关键。MyBatis用责任链模式实现插件:
java
// 简化版的插件执行流程
public class Plugin implements InvocationHandler {
private final Object target; // 被代理的对象
private final Interceptor interceptor; // 拦截器
private final Map<Class<?>, Set<Method>> signatureMap; // 方法签名映射
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 检查是否是需要拦截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 2. 执行拦截器逻辑
return interceptor.intercept(new Invocation(target, method, args));
}
// 3. 否则直接执行原方法
return method.invoke(target, args);
}
}
代码清单1:插件代理的核心逻辑
用图来表示更清楚:

图2:插件责任链执行流程
关键点:每个被拦截的对象都被层层代理,就像洋葱一样,一层包一层。
2. 手把手:写你的第一个插件
2.1 需求分析:我们要监控什么?
在动手前,先想清楚需求。一个生产级的SQL监控插件应该监控:
-
执行时间:每个SQL的执行耗时
-
SQL语句:实际执行的SQL(带参数)
-
参数信息:SQL绑定的参数
-
调用位置:哪个Mapper的哪个方法
-
结果大小:返回了多少条数据
-
慢SQL告警:超过阈值要告警
2.2 项目结构设计
先看项目结构,好的结构是成功的一半:
sql-monitor-plugin/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── yourcompany/
│ │ │ └── mybatis/
│ │ │ └── plugin/
│ │ │ ├── SqlMonitorPlugin.java # 主插件
│ │ │ ├── SlowSqlAlarmer.java # 慢SQL告警
│ │ │ ├── SqlStatsCollector.java # SQL统计收集
│ │ │ ├── SqlContextHolder.java # SQL上下文
│ │ │ ├── model/
│ │ │ │ ├── SqlExecutionInfo.java # SQL执行信息
│ │ │ │ └── SlowSqlAlert.java # 慢SQL告警信息
│ │ │ └── util/
│ │ │ ├── SqlFormatter.java # SQL格式化
│ │ │ └── StackTraceUtil.java # 堆栈工具
│ │ └── resources/
│ │ └── META-INF/
│ │ └── com.yourcompany.mybatis.properties # 配置文件
│ └── test/
└── pom.xml
2.3 基础插件实现
先写个最简单的插件,打印SQL执行时间:
java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "query",
args = {Statement.class, ResultHandler.class}
),
@Signature(
type = StatementHandler.class,
method = "update",
args = {Statement.class}
)
})
@Slf4j
public class SimpleSqlMonitorPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 执行原方法
return invocation.proceed();
} finally {
long costTime = System.currentTimeMillis() - startTime;
// 获取StatementHandler
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
// 打印日志
log.info("SQL执行耗时: {}ms, SQL: {}", costTime, boundSql.getSql());
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以读取配置
}
}
代码清单2:最简单的SQL监控插件
但这个插件有问题:
-
没区分是query还是update
-
没获取参数值
-
没处理批量操作
-
没考虑性能影响
3. 企业级SQL监控插件完整实现
3.1 核心数据结构设计
先定义数据结构,好的数据结构是成功的一半:
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SqlExecutionInfo {
// 基本信息
private String sqlId; // Mapper方法全限定名
private String sql; // 原始SQL
private String realSql; // 实际SQL(带参数)
private SqlCommandType commandType; // 操作类型:SELECT/INSERT等
// 执行信息
private long startTime; // 开始时间
private long endTime; // 结束时间
private long costTime; // 耗时(ms)
// 参数信息
private Object parameters; // 参数对象
private Map<String, Object> paramMap; // 参数Map
// 结果信息
private Object result; // 执行结果
private int resultSize; // 结果集大小
private boolean success; // 是否成功
private Throwable exception; // 异常信息
// 上下文信息
private String mapperInterface; // Mapper接口
private String mapperMethod; // Mapper方法
private String stackTrace; // 调用堆栈
private String dataSource; // 数据源
private String transactionId; // 事务ID
// 性能指标
private long fetchSize; // 获取行数
private long updateCount; // 更新行数
private long connectionAcquireTime; // 获取连接耗时
public enum SqlCommandType {
SELECT, INSERT, UPDATE, DELETE, UNKNOWN
}
}
代码清单3:SQL执行信息实体类
3.2 完整插件实现
现在实现完整的企业级插件:
java
@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}
),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class}
)
})
@Component
@Slf4j
public class SqlMonitorPlugin implements Interceptor {
// 配置
private long slowSqlThreshold = 1000; // 慢SQL阈值(ms),默认1秒
private boolean enableStackTrace = true; // 是否收集堆栈
private boolean enableAlert = true; // 是否开启告警
private int maxStackTraceDepth = 5; // 最大堆栈深度
// 统计收集器
private final SqlStatsCollector statsCollector = new SqlStatsCollector();
// 告警器
private final SlowSqlAlarmer alarmer = new SlowSqlAlarmer();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 创建执行信息
SqlExecutionInfo executionInfo = createExecutionInfo(invocation);
Object result = null;
Throwable exception = null;
try {
// 2. 执行原方法
result = invocation.proceed();
// 3. 记录结果信息
recordResultInfo(executionInfo, result);
executionInfo.setSuccess(true);
return result;
} catch (Throwable t) {
// 4. 记录异常
exception = t;
executionInfo.setException(t);
executionInfo.setSuccess(false);
throw t;
} finally {
// 5. 计算耗时
executionInfo.setEndTime(System.currentTimeMillis());
executionInfo.setCostTime(
executionInfo.getEndTime() - executionInfo.getStartTime()
);
// 6. 收集统计信息
statsCollector.collect(executionInfo);
// 7. 记录日志
logExecution(executionInfo);
// 8. 慢SQL告警
if (executionInfo.getCostTime() > slowSqlThreshold) {
triggerSlowSqlAlert(executionInfo);
}
}
}
private SqlExecutionInfo createExecutionInfo(Invocation invocation) {
SqlExecutionInfo info = new SqlExecutionInfo();
info.setStartTime(System.currentTimeMillis());
// 获取MappedStatement
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 设置基本信息
info.setSqlId(ms.getId());
info.setCommandType(ms.getSqlCommandType());
// 获取BoundSql
BoundSql boundSql = ms.getBoundSql(parameter);
info.setSql(boundSql.getSql());
info.setParameters(parameter);
// 解析参数
parseParameters(info, boundSql, parameter);
// 获取调用堆栈
if (enableStackTrace) {
info.setStackTrace(getStackTrace());
}
// 解析Mapper信息
parseMapperInfo(info, ms);
return info;
}
private void parseParameters(SqlExecutionInfo info, BoundSql boundSql, Object parameter) {
try {
// 如果是Map类型
if (parameter instanceof Map) {
info.setParamMap((Map<String, Object>) parameter);
}
// 如果是单个参数
else if (parameter != null) {
Map<String, Object> paramMap = new HashMap<>();
// 获取参数名称
Object paramObj = boundSql.getParameterObject();
if (paramObj != null) {
// 如果是@Param注解的参数
if (paramObj instanceof Map) {
paramMap.putAll((Map<String, Object>) paramObj);
}
// 如果是实体对象
else {
// 通过反射获取属性值
BeanInfo beanInfo = Introspector.getBeanInfo(paramObj.getClass());
PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor prop : props) {
if (!"class".equals(prop.getName())) {
Method getter = prop.getReadMethod();
if (getter != null) {
Object value = getter.invoke(paramObj);
paramMap.put(prop.getName(), value);
}
}
}
}
}
info.setParamMap(paramMap);
}
// 生成实际SQL(用于调试)
info.setRealSql(generateRealSql(boundSql));
} catch (Exception e) {
log.warn("解析SQL参数失败", e);
}
}
private String generateRealSql(BoundSql boundSql) {
String sql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty() || parameterObject == null) {
return sql;
}
// 这里简化处理,实际应该用TypeHandler处理类型转换
try {
for (ParameterMapping mapping : parameterMappings) {
String property = mapping.getProperty();
Object value = getParameterValue(property, parameterObject);
// 简单的字符串替换(仅用于日志,不要用于实际执行)
if (value instanceof String) {
value = "'" + value + "'";
}
sql = sql.replaceFirst("\\?", value.toString());
}
} catch (Exception e) {
// 生成失败返回原始SQL
}
return sql;
}
private Object getParameterValue(String property, Object parameterObject) {
if (parameterObject instanceof Map) {
return ((Map<?, ?>) parameterObject).get(property);
} else {
// 反射获取属性值
try {
BeanInfo beanInfo = Introspector.getBeanInfo(parameterObject.getClass());
PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor prop : props) {
if (prop.getName().equals(property)) {
Method getter = prop.getReadMethod();
if (getter != null) {
return getter.invoke(parameterObject);
}
}
}
} catch (Exception e) {
// ignore
}
}
return "?";
}
private void parseMapperInfo(SqlExecutionInfo info, MappedStatement ms) {
String sqlId = ms.getId();
int lastDotIndex = sqlId.lastIndexOf(".");
if (lastDotIndex > 0) {
info.setMapperInterface(sqlId.substring(0, lastDotIndex));
info.setMapperMethod(sqlId.substring(lastDotIndex + 1));
}
}
private void recordResultInfo(SqlExecutionInfo info, Object result) {
if (result instanceof List) {
info.setResultSize(((List<?>) result).size());
} else if (result instanceof Collection) {
info.setResultSize(((Collection<?>) result).size());
} else if (result != null) {
info.setResultSize(1);
}
info.setResult(result);
}
private String getStackTrace() {
StringBuilder stackTrace = new StringBuilder();
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
int depth = 0;
for (StackTraceElement element : elements) {
// 过滤框架调用
if (element.getClassName().startsWith("org.apache.ibatis") ||
element.getClassName().startsWith("com.sun.proxy") ||
element.getClassName().startsWith("java.lang.Thread")) {
continue;
}
stackTrace.append(element.getClassName())
.append(".")
.append(element.getMethodName())
.append("(")
.append(element.getFileName())
.append(":")
.append(element.getLineNumber())
.append(")\n");
if (++depth >= maxStackTraceDepth) {
break;
}
}
return stackTrace.toString();
}
private void logExecution(SqlExecutionInfo info) {
if (log.isInfoEnabled()) {
String logMsg = String.format(
"SQL执行统计 - 方法: %s, 耗时: %dms, 类型: %s, 结果: %d条, SQL: %s",
info.getSqlId(),
info.getCostTime(),
info.getCommandType(),
info.getResultSize(),
info.getSql()
);
if (info.getCostTime() > slowSqlThreshold) {
log.warn("⚠️ " + logMsg);
} else {
log.info("✅ " + logMsg);
}
}
}
private void triggerSlowSqlAlert(SqlExecutionInfo info) {
if (!enableAlert) {
return;
}
SlowSqlAlert alert = new SlowSqlAlert();
alert.setSqlId(info.getSqlId());
alert.setSql(info.getSql());
alert.setCostTime(info.getCostTime());
alert.setThreshold(slowSqlThreshold);
alert.setParameters(info.getParamMap());
alert.setStackTrace(info.getStackTrace());
alert.setAlertTime(new Date());
alarmer.sendAlert(alert);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取配置
String threshold = properties.getProperty("slowSqlThreshold");
if (threshold != null) {
this.slowSqlThreshold = Long.parseLong(threshold);
}
String enableStack = properties.getProperty("enableStackTrace");
if (enableStack != null) {
this.enableStackTrace = Boolean.parseBoolean(enableStack);
}
String enableAlertProp = properties.getProperty("enableAlert");
if (enableAlertProp != null) {
this.enableAlert = Boolean.parseBoolean(enableAlertProp);
}
String maxDepth = properties.getProperty("maxStackTraceDepth");
if (maxDepth != null) {
this.maxStackTraceDepth = Integer.parseInt(maxDepth);
}
}
}
代码清单4:完整的企业级SQL监控插件
4. 高级功能:SQL统计与告警

4.1 SQL统计收集器
监控不能只记录,还要能分析。实现一个统计收集器:
java
@Component
@Slf4j
public class SqlStatsCollector {
// 使用ConcurrentHashMap保证线程安全
private final ConcurrentHashMap<String, SqlStats> statsMap = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 统计信息
@Data
public static class SqlStats {
private String sqlId;
private long totalCount; // 总执行次数
private long successCount; // 成功次数
private long errorCount; // 失败次数
private long totalCostTime; // 总耗时
private long maxCostTime; // 最大耗时
private long minCostTime = Long.MAX_VALUE; // 最小耗时
private double avgCostTime; // 平均耗时
// 耗时分布
private long[] costTimeDistribution = new long[6]; // 0-100, 100-500, 500-1000, 1000-3000, 3000-10000, >10000 ms
// 最近100次耗时(用于计算P99等)
private final LinkedList<Long> recentCosts = new LinkedList<>();
private static final int RECENT_SIZE = 100;
public synchronized void record(SqlExecutionInfo info) {
totalCount++;
if (info.isSuccess()) {
successCount++;
} else {
errorCount++;
}
long cost = info.getCostTime();
totalCostTime += cost;
if (cost > maxCostTime) {
maxCostTime = cost;
}
if (cost < minCostTime) {
minCostTime = cost;
}
avgCostTime = (double) totalCostTime / totalCount;
// 记录耗时分布
if (cost < 100) {
costTimeDistribution[0]++;
} else if (cost < 500) {
costTimeDistribution[1]++;
} else if (cost < 1000) {
costTimeDistribution[2]++;
} else if (cost < 3000) {
costTimeDistribution[3]++;
} else if (cost < 10000) {
costTimeDistribution[4]++;
} else {
costTimeDistribution[5]++;
}
// 记录最近耗时
recentCosts.add(cost);
if (recentCosts.size() > RECENT_SIZE) {
recentCosts.removeFirst();
}
}
public double getP99CostTime() {
if (recentCosts.isEmpty()) {
return 0;
}
List<Long> sorted = new ArrayList<>(recentCosts);
Collections.sort(sorted);
int index = (int) Math.ceil(0.99 * sorted.size()) - 1;
return index >= 0 ? sorted.get(index) : sorted.get(0);
}
public double getSuccessRate() {
return totalCount > 0 ? (double) successCount / totalCount * 100 : 100;
}
}
public SqlStatsCollector() {
// 每分钟输出一次统计报告
scheduler.scheduleAtFixedRate(this::printStatsReport, 1, 1, TimeUnit.MINUTES);
// 每小时清理一次旧数据
scheduler.scheduleAtFixedRate(this::cleanupOldStats, 1, 1, TimeUnit.HOURS);
}
public void collect(SqlExecutionInfo info) {
String sqlId = info.getSqlId();
statsMap.compute(sqlId, (key, stats) -> {
if (stats == null) {
stats = new SqlStats();
stats.setSqlId(sqlId);
}
stats.record(info);
return stats;
});
}
public SqlStats getStats(String sqlId) {
return statsMap.get(sqlId);
}
public Map<String, SqlStats> getAllStats() {
return new HashMap<>(statsMap);
}
private void printStatsReport() {
if (statsMap.isEmpty()) {
return;
}
log.info("======= SQL执行统计报告 =======");
log.info("统计时间: {}", new Date());
log.info("总SQL数量: {}", statsMap.size());
// 找出最慢的10个SQL
List<SqlStats> topSlow = statsMap.values().stream()
.sorted((s1, s2) -> Long.compare(s2.getMaxCostTime(), s1.getMaxCostTime()))
.limit(10)
.collect(Collectors.toList());
log.info("最慢的10个SQL:");
for (int i = 0; i < topSlow.size(); i++) {
SqlStats stats = topSlow.get(i);
log.info("{}. {} - 最大: {}ms, 平均: {:.2f}ms, 成功: {:.2f}%, 调用: {}次",
i + 1, stats.getSqlId(), stats.getMaxCostTime(),
stats.getAvgCostTime(), stats.getSuccessRate(), stats.getTotalCount());
}
// 统计总体情况
long totalExecutions = statsMap.values().stream()
.mapToLong(SqlStats::getTotalCount)
.sum();
double avgSuccessRate = statsMap.values().stream()
.mapToDouble(SqlStats::getSuccessRate)
.average()
.orElse(0);
log.info("总体统计 - 总执行: {}次, 平均成功率: {:.2f}%",
totalExecutions, avgSuccessRate);
}
private void cleanupOldStats() {
// 清理24小时无调用的统计
// 实际实现中可以添加最后调用时间字段
}
}
代码清单5:SQL统计收集器
4.2 慢SQL告警器
告警要及时,但不能太频繁:
java
@Component
@Slf4j
public class SlowSqlAlarmer {
// 告警规则
@Data
public static class AlertRule {
private String sqlPattern; // SQL模式匹配
private long threshold; // 阈值(ms)
private int interval; // 告警间隔(分钟)
private String[] receivers; // 接收人
private AlertLevel level; // 告警级别
public enum AlertLevel {
INFO, WARNING, ERROR, CRITICAL
}
}
// 告警记录
@Data
public static class AlertRecord {
private String sqlId;
private long costTime;
private long threshold;
private Date alertTime;
private int count; // 本次告警周期内的次数
}
private final List<AlertRule> rules = new ArrayList<>();
private final Map<String, AlertRecord> lastAlertMap = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public SlowSqlAlarmer() {
// 加载默认规则
loadDefaultRules();
// 每小时清理一次旧告警记录
scheduler.scheduleAtFixedRate(this::cleanupAlertRecords, 1, 1, TimeUnit.HOURS);
}
private void loadDefaultRules() {
// 规则1:所有SQL超过5秒
AlertRule rule1 = new AlertRule();
rule1.setSqlPattern(".*");
rule1.setThreshold(5000);
rule1.setInterval(5);
rule1.setLevel(AlertRule.AlertLevel.ERROR);
rule1.setReceivers(new String[]{"dba@company.com"});
// 规则2:重要业务SQL超过1秒
AlertRule rule2 = new AlertRule();
rule2.setSqlPattern(".*(User|Order|Payment).*");
rule2.setThreshold(1000);
rule2.setInterval(1);
rule2.setLevel(AlertRule.AlertLevel.WARNING);
rule2.setReceivers(new String[]{"dev@company.com"});
rules.add(rule1);
rules.add(rule2);
}
public void sendAlert(SlowSqlAlert alert) {
// 1. 匹配规则
List<AlertRule> matchedRules = matchRules(alert);
if (matchedRules.isEmpty()) {
return;
}
for (AlertRule rule : matchedRules) {
// 2. 检查是否需要告警(防骚扰)
if (shouldAlert(alert, rule)) {
// 3. 发送告警
doSendAlert(alert, rule);
// 4. 记录告警
recordAlert(alert, rule);
}
}
}
private List<AlertRule> matchRules(SlowSqlAlert alert) {
return rules.stream()
.filter(rule -> alert.getCostTime() >= rule.getThreshold())
.filter(rule -> alert.getSql().matches(rule.getSqlPattern()))
.collect(Collectors.toList());
}
private boolean shouldAlert(SlowSqlAlert alert, AlertRule rule) {
String key = alert.getSqlId() + ":" + rule.getThreshold();
AlertRecord lastRecord = lastAlertMap.get(key);
if (lastRecord == null) {
return true;
}
// 检查是否在告警间隔内
long timeDiff = System.currentTimeMillis() - lastRecord.getAlertTime().getTime();
return timeDiff > rule.getInterval() * 60 * 1000;
}
private void doSendAlert(SlowSqlAlert alert, AlertRule rule) {
String title = String.format("[%s] 慢SQL告警: %s",
rule.getLevel(), alert.getSqlId());
StringBuilder content = new StringBuilder();
content.append("SQL ID: ").append(alert.getSqlId()).append("\n");
content.append("执行耗时: ").append(alert.getCostTime()).append("ms\n");
content.append("阈值: ").append(rule.getThreshold()).append("ms\n");
content.append("实际SQL: ").append(alert.getSql()).append("\n");
content.append("参数: ").append(alert.getParameters()).append("\n");
content.append("告警时间: ").append(new Date()).append("\n");
if (alert.getStackTrace() != null) {
content.append("调用堆栈:\n").append(alert.getStackTrace());
}
// 发送邮件
sendEmail(rule.getReceivers(), title, content.toString());
// 发送企业微信/钉钉
sendChatMessage(rule.getLevel(), title, content.toString());
log.warn("发送慢SQL告警: {}, 耗时: {}ms", alert.getSqlId(), alert.getCostTime());
}
private void recordAlert(SlowSqlAlert alert, AlertRule rule) {
String key = alert.getSqlId() + ":" + rule.getThreshold();
AlertRecord record = new AlertRecord();
record.setSqlId(alert.getSqlId());
record.setCostTime(alert.getCostTime());
record.setThreshold(rule.getThreshold());
record.setAlertTime(new Date());
record.setCount(1);
lastAlertMap.put(key, record);
}
private void sendEmail(String[] receivers, String title, String content) {
// 实现邮件发送逻辑
// 可以使用JavaMail或Spring Mail
}
private void sendChatMessage(AlertRule.AlertLevel level, String title, String content) {
// 实现企业微信/钉钉消息发送
}
private void cleanupAlertRecords() {
long now = System.currentTimeMillis();
long hourMillis = 60 * 60 * 1000;
lastAlertMap.entrySet().removeIf(entry ->
now - entry.getValue().getAlertTime().getTime() > 24 * hourMillis
);
}
}
代码清单6:慢SQL告警器
5. 插件配置与使用
5.1 MyBatis配置
在mybatis-config.xml中配置插件:
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启驼峰命名转换 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 插件配置 -->
<plugins>
<plugin interceptor="com.yourcompany.mybatis.plugin.SqlMonitorPlugin">
<!-- 慢SQL阈值,单位毫秒 -->
<property name="slowSqlThreshold" value="1000"/>
<!-- 是否开启堆栈信息收集 -->
<property name="enableStackTrace" value="true"/>
<!-- 是否开启告警 -->
<property name="enableAlert" value="true"/>
<!-- 最大堆栈深度 -->
<property name="maxStackTraceDepth" value="5"/>
<!-- 慢SQL告警规则(JSON格式) -->
<property name="alertRules" value='
[
{
"sqlPattern": ".*",
"threshold": 5000,
"interval": 5,
"level": "ERROR",
"receivers": ["dba@company.com"]
},
{
"sqlPattern": ".*(User|Order|Payment).*",
"threshold": 1000,
"interval": 1,
"level": "WARNING",
"receivers": ["dev@company.com"]
}
]
'/>
</plugin>
</plugins>
<!-- 其他配置... -->
</configuration>
代码清单7:MyBatis配置插件
5.2 Spring Boot配置
在Spring Boot中配置:
XML
# application.yml
mybatis:
configuration:
# MyBatis配置
map-underscore-to-camel-case: true
default-statement-timeout: 30
# 插件配置
configuration-properties:
slowSqlThreshold: 1000
enableStackTrace: true
enableAlert: true
maxStackTraceDepth: 5
Java配置类:
java
@Configuration
public class MyBatisConfig {
@Bean
public SqlMonitorPlugin sqlMonitorPlugin() {
SqlMonitorPlugin plugin = new SqlMonitorPlugin();
Properties properties = new Properties();
properties.setProperty("slowSqlThreshold", "1000");
properties.setProperty("enableStackTrace", "true");
properties.setProperty("enableAlert", "true");
properties.setProperty("maxStackTraceDepth", "5");
plugin.setProperties(properties);
return plugin;
}
}
6. 性能测试与优化
6.1 插件性能影响测试
插件有性能开销,必须测试:
测试环境:
-
CPU: 4核
-
内存: 8GB
-
MySQL: 8.0
-
MyBatis: 3.5.7
-
测试数据: 10000次查询
测试结果:
| 插件功能 | 平均耗时(ms) | 性能影响 | 内存增加 |
|---|---|---|---|
| 无插件 | 12.5 | 基准 | 0MB |
| 基础监控 | 13.8 | +10.4% | 5MB |
| 完整监控 | 15.2 | +21.6% | 12MB |
| 监控+告警 | 16.7 | +33.6% | 18MB |
结论:插件会增加10-30%的性能开销,在可接受范围内。
6.2 性能优化技巧
优化1:异步处理
java
// 异步发送告警
private void triggerSlowSqlAlert(SqlExecutionInfo info) {
if (!enableAlert) {
return;
}
CompletableFuture.runAsync(() -> {
SlowSqlAlert alert = new SlowSqlAlert();
// 构建告警信息...
alarmer.sendAlert(alert);
});
}
优化2:采样率控制
java
// 控制采样率,避免全量监控
private boolean shouldSample() {
// 10%的采样率
return ThreadLocalRandom.current().nextInt(100) < 10;
}
// 在intercept方法中使用
if (!shouldSample() && info.getCostTime() < slowSqlThreshold) {
return invocation.proceed();
}
优化3:使用对象池
java
// 重用SqlExecutionInfo对象
private final ObjectPool<SqlExecutionInfo> infoPool = new GenericObjectPool<>(
new BasePooledObjectFactory<SqlExecutionInfo>() {
@Override
public SqlExecutionInfo create() {
return new SqlExecutionInfo();
}
@Override
public void passivateObject(PooledObject<SqlExecutionInfo> p) {
// 重置对象状态
p.getObject().reset();
}
}
);
private SqlExecutionInfo createExecutionInfo(Invocation invocation) {
SqlExecutionInfo info = null;
try {
info = infoPool.borrowObject();
// 填充数据...
return info;
} catch (Exception e) {
return new SqlExecutionInfo();
} finally {
if (info != null) {
infoPool.returnObject(info);
}
}
}
7. 生产环境问题排查
7.1 常见问题排查清单
我总结了插件开发中最常见的10个问题:
问题1:插件不生效
排查步骤:
-
检查
@Intercepts注解配置是否正确 -
检查插件是否在
mybatis-config.xml中配置 -
检查Spring Boot自动配置是否正确
-
检查插件顺序(如果有多个插件)
java
// 调试方法:在插件中加日志
@Override
public Object plugin(Object target) {
log.info("插件包装对象: {}", target.getClass().getName());
return Plugin.wrap(target, this);
}
问题2:性能下降明显
排查:
-
检查是否频繁创建对象
-
检查字符串操作是否过多
-
检查日志级别是否正确
java
// 使用JMH进行性能测试
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testPluginPerformance() {
// 测试代码
}
问题3:内存泄漏
排查:
-
检查是否有静态Map无限增长
-
检查线程局部变量是否清理
-
使用MAT分析内存快照
java
// 定期清理缓存
scheduler.scheduleAtFixedRate(() -> {
statsMap.entrySet().removeIf(entry ->
System.currentTimeMillis() - entry.getValue().getLastAccessTime() > 3600000
);
}, 1, 1, TimeUnit.HOURS);
7.2 监控指标
插件自身也要被监控:
java
@Component
public class PluginMetrics {
private final MeterRegistry meterRegistry;
// 监控指标
private final Counter totalSqlCounter;
private final Counter slowSqlCounter;
private final Timer sqlTimer;
public PluginMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.totalSqlCounter = Counter.builder("mybatis.sql.total")
.description("SQL执行总次数")
.register(meterRegistry);
this.slowSqlCounter = Counter.builder("mybatis.sql.slow")
.description("慢SQL次数")
.register(meterRegistry);
this.sqlTimer = Timer.builder("mybatis.sql.duration")
.description("SQL执行耗时")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
}
public void recordSqlExecution(long costTime, boolean isSlow) {
totalSqlCounter.increment();
sqlTimer.record(costTime, TimeUnit.MILLISECONDS);
if (isSlow) {
slowSqlCounter.increment();
}
}
}
8. 高级功能扩展
8.1 SQL防注入检测
在监控基础上,增加安全检测:
java
public class SqlInjectionDetector {
private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile(
"('|--|;|\\|/\\*|\\*/|@@|char|nchar|varchar|nvarchar|alter|begin|cast|create|cursor|declare|delete|drop|end|exec|execute|fetch|insert|kill|open|select|sys|sysobjects|syscolumns|table|update|union)"
);
public static boolean detectInjection(String sql) {
if (sql == null) {
return false;
}
// 检查是否使用${}(容易导致注入)
if (sql.contains("${")) {
return true;
}
// 检查危险关键字
return SQL_INJECTION_PATTERN.matcher(sql.toLowerCase()).find();
}
public static String sanitizeSql(String sql) {
if (sql == null) {
return null;
}
// 简单的SQL清理
return sql.replace("'", "''")
.replace("--", "")
.replace(";", "");
}
}
// 在插件中使用
private void checkSqlInjection(SqlExecutionInfo info) {
if (SqlInjectionDetector.detectInjection(info.getSql())) {
log.error("检测到可能的SQL注入: {}", info.getSql());
// 发送安全告警
sendSecurityAlert(info);
}
}
8.2 SQL执行计划分析
集成数据库执行计划分析:
java
public class ExplainAnalyzer {
public ExecutionPlan analyzeExplain(String sql, Object params) {
// 生成EXPLAIN SQL
String explainSql = "EXPLAIN " + sql;
// 执行EXPLAIN
// 解析结果
// 返回执行计划
return executionPlan;
}
@Data
public static class ExecutionPlan {
private String id;
private String selectType;
private String table;
private String type; // ALL, index, range, ref, eq_ref, const, system, NULL
private String possibleKeys;
private String key;
private int keyLen;
private String ref;
private int rows;
private String extra; // Using filesort, Using temporary
}
public boolean isSlowPlan(ExecutionPlan plan) {
// 判断是否为慢查询计划
return "ALL".equals(plan.getType()) || // 全表扫描
plan.getExtra().contains("filesort") || // 文件排序
plan.getExtra().contains("temporary"); // 临时表
}
}
9. 企业级最佳实践
9.1 我的"插件开发军规"
经过多年实践,我总结了一套插件开发最佳实践:
📜 第一条:明确职责
一个插件只做一件事,不要大而全。监控插件就只监控,不要混入业务逻辑。
📜 第二条:性能优先
插件调用非常频繁,每个操作都要考虑性能。避免在插件中做耗时的IO操作。
📜 第三条:配置化
所有参数都要可配置,避免硬编码。通过Properties传递配置。
📜 第四条:异常处理
插件异常不能影响主流程,要捕获所有异常,只记录不抛出。
📜 第五条:兼容性
考虑不同MyBatis版本、不同数据库的兼容性。使用反射时要检查方法是否存在。
9.2 生产环境部署检查清单
上线前必须检查:
-
\] 性能测试通过
-
\] 异常情况测试通过
-
\] 监控指标配置完成
-
\] 回滚方案准备就绪
MyBatis插件开发就像给汽车装行车记录仪,平时用不上,关键时刻能救命。
我见过太多团队在这上面栽跟头:有的因为插件性能问题拖垮系统,有的因为内存泄漏导致OOM,有的因为异常处理不当导致事务回滚。
记住:插件是利器,用好了能提升系统可观测性,用不好就是线上事故。理解原理,小心使用,持续优化。
📚 推荐阅读
官方文档
-
**MyBatis官方文档 - 插件** - 官方插件开发指南
-
**MyBatis拦截器原理** - 深入理解拦截机制
源码学习
-
**MyBatis插件源码** - 学习官方实现
-
**PageHelper分页插件** - 优秀插件案例
监控工具
-
**Prometheus监控** - 监控指标收集
-
**Grafana仪表板** - 监控数据展示
性能分析
-
**Arthas诊断工具** - Java应用诊断
-
**VisualVM性能分析** - JVM性能监控
最后建议 :不要直接在生产环境使用本文的代码。先在测试环境跑通,性能测试通过,再逐步灰度上线。记住:先监控,后优化;先测试,后上线。