MyBatis 自定义拦截器
# MyBatis 自定义拦截器
前言
在开发过程中,我们经常需要监控 SQL 语句的执行情况,包括完整的 SQL 语句(带参数值而非预编译占位符)以及执行时间,以便于分析和优化数据库操作。MyBatis 提供了强大的拦截器机制,通过自定义拦截器,我们可以实现这些功能,从而提高开发效率和应用性能。本文将详细介绍如何利用 MyBatis 拦截器实现 SQL 语句的精确监控与分析。
# 1. MyBatis 拦截器基础介绍
# 1.1 拦截器的工作原理
MyBatis 拦截器基于 Java 的动态代理机制,通过实现 Interceptor
接口,可以拦截 MyBatis 执行流程中的关键方法,如 SQL 语句的执行、参数的处理等。拦截器可以在这些方法执行前后添加自定义逻辑,例如日志记录、性能监控等。
# 1.2 可拦截的目标对象与方法
MyBatis 允许拦截以下对象的方法:
对象类型 | 说明 | 常用拦截方法 |
---|---|---|
Executor | 执行器,负责调度 StatementHandler | update(), query(), commit(), rollback() |
ParameterHandler | 参数处理器,设置预编译参数 | setParameters() |
ResultSetHandler | 结果集处理器,处理查询结果 | handleResultSets() |
StatementHandler | 语句处理器,创建 Statement 对象 | prepare(), parameterize(), batch(), update(), query() |
# 1.3 拦截器的应用场景
- SQL 语句记录与分析
- 执行时间统计
- 性能监控
- 分页处理
- 数据权限控制
- 缓存管理
# 2. 自定义 SQL 监控拦截器实现步骤
# 2.1 拦截器整体设计
我们将创建一个拦截器,它能够:
- 拦截 SQL 执行前后的时间点
- 获取并解析预编译 SQL 语句
- 获取并填充 SQL 参数值
- 组装完整的可执行 SQL 语句
- 计算执行时间
- 格式化并输出日志
# 2.2 基本实现框架
要实现 MyBatis 拦截器,需要完成以下步骤:
- 实现
org.apache.ibatis.plugin.Interceptor
接口 - 添加
@Intercepts
注解,指定拦截点 - 实现
intercept()
方法,添加自定义逻辑 - 实现
plugin()
方法,生成代理对象 - 实现
setProperties()
方法,接收配置参数 - 在 MyBatis 配置中注册拦截器
# 3. 完整拦截器代码实现
# 3.1 基础拦截器实现
package com.example.mybatis.interceptor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.Statement;
import java.util.*;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
/**
* MyBatis SQL 执行监控拦截器
* 用于打印完整SQL语句及其执行时间
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class SqlMonitorInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlMonitorInterceptor.class);
// 是否开启监控
private boolean enableSqlLog = true;
// 是否显示执行时间
private boolean showExecuteTime = true;
// 超过此时间的SQL将被标记为慢查询(单位:毫秒)
private long slowSqlThreshold = 1000;
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (!enableSqlLog) {
return invocation.proceed();
}
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
long startTime = System.currentTimeMillis();
try {
// 执行原始方法
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 打印SQL信息
printSqlInfo(statementHandler, costTime);
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 打印异常SQL信息
printErrorSqlInfo(statementHandler, costTime, e);
throw e;
}
}
/**
* 打印SQL信息
*/
private void printSqlInfo(StatementHandler statementHandler, long costTime) {
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 获取配置信息
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
Configuration configuration = (Configuration) metaObject.getValue("delegate.configuration");
// 组装完整SQL
String completeSql = getCompleteSql(configuration, boundSql);
// 打印日志
StringBuilder logBuilder = new StringBuilder("\n=========================== SQL执行详情 ===========================\n");
logBuilder.append("| 执行SQL:").append(formatSql(completeSql)).append("\n");
if (showExecuteTime) {
logBuilder.append("| 执行时间:").append(costTime).append("ms");
// 标记慢查询
if (costTime > slowSql如果 showExecuteTime) {
logBuilder.append("| 执行时间:").append(costTime).append("ms");
// 标记慢查询
if (costTime > slowSqlThreshold) {
logBuilder.append(" [慢查询]");
}
logBuilder.append("\n");
}
logBuilder.append("==================================================================");
logger.info(logBuilder.toString());
}
/**
* 打印异常SQL信息
*/
private void printErrorSqlInfo(StatementHandler statementHandler, long costTime, Exception e) {
BoundSql boundSql = statementHandler.getBoundSql();
// 获取配置信息
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
Configuration configuration = (Configuration) metaObject.getValue("delegate.configuration");
// 组装完整SQL
String completeSql = getCompleteSql(configuration, boundSql);
// 打印日志
StringBuilder logBuilder = new StringBuilder("\n====================== SQL执行异常详情 ======================\n");
logBuilder.append("| 异常SQL:").append(formatSql(completeSql)).append("\n");
logBuilder.append("| 执行时间:").append(costTime).append("ms\n");
logBuilder.append("| 异常信息:").append(e.getMessage()).append("\n");
logBuilder.append("==============================================================");
logger.error(logBuilder.toString());
}
/**
* 获取完整的SQL语句(替换占位符)
*/
private String getCompleteSql(Configuration configuration, BoundSql boundSql) {
// 获取SQL语句
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 没有参数,直接返回
if (parameterMappings == null || parameterMappings.isEmpty()) {
return sql;
}
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 替换参数值
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMapping.Mode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
// 获取参数值
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 格式化参数值
String paramValue = formatParameterValue(value);
// 替换第一个问号
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValue));
}
}
return sql;
}
/**
* 格式化参数值
*/
private String formatParameterValue(Object value) {
if (value == null) {
return "NULL";
}
if (value instanceof String) {
return "'" + value.toString().replaceAll("'", "''") + "'";
}
if (value instanceof Date) {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
return "'" + dateFormat.format(value) + "'";
}
if (value instanceof Boolean || value instanceof Number) {
return value.toString();
}
return "'" + value.toString() + "'";
}
/**
* 格式化SQL语句
*/
private String formatSql(String sql) {
return sql.replaceAll("[\\s]+", " ");
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
if (properties != null) {
String enableSqlLog = properties.getProperty("enableSqlLog");
String showExecuteTime = properties.getProperty("showExecuteTime");
String slowSqlThreshold = properties.getProperty("slowSqlThreshold");
if (enableSqlLog != null) {
this.enableSqlLog = Boolean.parseBoolean(enableSqlLog);
}
if (showExecuteTime != null) {
this.showExecuteTime = Boolean.parseBoolean(showExecuteTime);
}
if (slowSqlThreshold != null) {
try {
this.slowSqlThreshold = Long.parseLong(slowSqlThreshold);
} catch (NumberFormatException e) {
// 使用默认值
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# 3.2 增强版拦截器(支持更多功能)
package com.example.mybatis.interceptor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.Statement;
import java.text.DateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
/**
* 增强版 MyBatis SQL 监控拦截器
* 支持完整SQL打印、执行时间统计、慢查询识别、SQL分类统计等功能
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class EnhancedSqlMonitorInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(EnhancedSqlMonitorInterceptor.class);
// 配置参数
private boolean enableSqlLog = true; // 是否开启SQL日志
private boolean showExecuteTime = true; // 是否显示执行时间
private boolean showSqlType = true; // 是否显示SQL类型
private boolean showMapperInfo = true; // 是否显示Mapper信息
private boolean enableStatistics = true; // 是否启用统计功能
private long slowSqlThreshold = 1000; // 慢查询阈值(毫秒)
private boolean prettyFormat = false; // 是否美化SQL输出格式
// SQL执行统计
private final Map<String, AtomicLong> sqlCountMap = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> sqlTimeMap = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> slowSqlCountMap = new ConcurrentHashMap<>();
// 线程本地变量,用于存储SQL开始执行时间
private final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();
private final ThreadLocal<String> sqlIdThreadLocal = new ThreadLocal<>();
private final ThreadLocal<SqlCommandType> sqlTypeThreadLocal = new ThreadLocal<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (!enableSqlLog) {
return invocation.proceed();
}
String methodName = invocation.getMethod().getName();
if ("prepare".equals(methodName)) {
// 在prepare阶段记录开始时间和SQL信息
return handlePrepare(invocation);
} else {
// 在query/update/batch阶段记录结束时间并打印日志
return handleExecute(invocation, methodName);
}
}
/**
* 处理SQL准备阶段
*/
private Object handlePrepare(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 记录开始时间
startTimeThreadLocal.set(System.currentTimeMillis());
// 获取MappedStatement信息
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String sqlId = mappedStatement.getId();
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
// 保存SQL信息到ThreadLocal
sqlIdThreadLocal.set(sqlId);
sqlTypeThreadLocal.set(sqlCommandType);
return invocation.proceed();
}
/**
* 处理SQL执行阶段
*/
private Object handleExecute(Invocation invocation, String methodName) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
try {
// 执行原始方法
Object result = invocation.proceed();
// 获取执行时间
long startTime = startTimeThreadLocal.get();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 打印SQL信息
printSqlInfo(statementHandler, costTime, false);
// 统计SQL执行情况
if (enableStatistics) {
collectStatistics(costTime);
}
return result;
} catch (Exception e) {
// 获取执行时间
long startTime = startTimeThreadLocal.get();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 打印异常SQL信息
printSqlInfo(statementHandler, costTime, true);
throw e;
} finally {
// 清理ThreadLocal
startTimeThreadLocal.remove();
sqlIdThreadLocal.remove();
sqlTypeThreadLocal.remove();
}
}
/**
* 打印SQL信息
*/
private void printSqlInfo(StatementHandler statementHandler, long costTime, boolean isError) {
BoundSql boundSql = statementHandler.getBoundSql();
// 获取配置信息
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
Configuration configuration = (Configuration) metaObject.getValue("delegate.configuration");
// 组装完整SQL
String completeSql = getCompleteSql(configuration, boundSql);
// 获取SQL信息
String sqlId = sqlIdThreadLocal.get();
SqlCommandType sqlType = sqlTypeThreadLocal.get();
// 打印日志
StringBuilder logBuilder = new StringBuilder();
if (isError) {
logBuilder.append("\n====================== SQL执行异常详情 ======================\n");
} else {
logBuilder.append("\n=========================== SQL执行详情 ===========================\n");
}
// 显示Mapper信息
if (showMapperInfo && sqlId != null) {
logBuilder.append("| Mapper方法:").append(sqlId).append("\n");
}
// 显示SQL类型
if (showSqlType && sqlType != null) {
logBuilder.append("| SQL类型:").append(sqlType).append("\n");
}
// 显示SQL语句
if (prettyFormat) {
logBuilder.append("| 执行SQL:\n").append(formatSqlPretty(completeSql)).append("\n");
} else {
logBuilder.append("| 执行SQL:").append(formatSql(completeSql)).append("\n");
}
// 显示执行时间
if (showExecuteTime) {
logBuilder.append("| 执行时间:").append(costTime).append("ms");
// 标记慢查询
if (costTime > slowSqlThreshold) {
logBuilder.append(" [慢查询]");
}
logBuilder.append("\n");
}
logBuilder.append("==================================================================");
if (isError) {
logger.error(logBuilder.toString());
} else {
logger.info(logBuilder.toString());
}
}
/**
* 收集SQL执行统计信息
*/
private void collectStatistics(long costTime) {
String sqlId = sqlIdThreadLocal.get();
SqlCommandType sqlType = sqlTypeThreadLocal.get();
if (sqlId != null) {
// 统计SQL执行次数
sqlCountMap.computeIfAbsent(sqlId, k -> new AtomicLong(0)).incrementAndGet();
// 统计SQL执行时间
sqlTimeMap.computeIfAbsent(sqlId, k -> new AtomicLong(0)).addAndGet(costTime);
// 统计慢查询
if (costTime > slowSqlThreshold) {
slowSqlCountMap.computeIfAbsent(sqlId, k -> new AtomicLong(0)).incrementAndGet();
}
}
}
/**
* 获取完整的SQL语句(替换占位符)
*/
private String getCompleteSql(Configuration configuration, BoundSql boundSql) {
// 获取SQL语句
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 没有参数,直接返回
if (parameterMappings == null || parameterMappings.isEmpty()) {
return sql;
}
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 替换参数值
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMapping.Mode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
// 获取参数值
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 格式化参数值
String paramValue = formatParameterValue(value);
// 替换第一个问号
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValue));
}
}
return sql;
}
/**
* 格式化参数值
*/
private String formatParameterValue(Object value) {
if (value == null) {
return "NULL";
}
if (value instanceof String) {
return "'" + value.toString().replaceAll("'", "''") + "'";
}
if (value instanceof Date) {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
return "'" + dateFormat.format(value) + "'";
}
if (value instanceof Boolean || value instanceof Number) {
return value.toString();
}
return "'" + value.toString() + "'";
}
/**
* 格式化SQL语句(简单格式)
*/
private String formatSql(String sql) {
return sql.replaceAll("[\\s]+", " ");
}
/**
* 格式化SQL语句(美化格式)
*/
private String formatSqlPretty(String sql) {
// 简单的SQL美化
sql = sql.replaceAll("(?i)SELECT", "\nSELECT")
.replaceAll("(?i)FROM", "\nFROM")
.replaceAll("(?i)WHERE", "\nWHERE")
.replaceAll("(?i)AND", "\n AND")
.replaceAll("(?i)OR", "\n OR")
.replaceAll("(?i)LEFT JOIN", "\nLEFT JOIN")
.replaceAll("(?i)RIGHT JOIN", "\nRIGHT JOIN")
.replaceAll("(?i)INNER JOIN", "\nINNER JOIN")
.replaceAll("(?i)ORDER BY", "\nORDER BY")
.replaceAll("(?i)GROUP BY", "\nGROUP BY")
.replaceAll("(?i)HAVING", "\nHAVING")
.replaceAll("(?i)LIMIT", "\nLIMIT")
.replaceAll("(?i)UNION", "\nUNION\n");
return sql;
}
/**
* 获取SQL执行统计信息
*/
public Map<String, Map<String, Object>> getStatistics() {
if (!enableStatistics) {
return Collections.emptyMap();
}
Map<String, Map<String, Object>> result = new HashMap<>();
for (Map.Entry<String, AtomicLong> entry : sqlCountMap.entrySet()) {
String sqlId = entry.getKey();
long count = entry.getValue().get();
long totalTime = sqlTimeMap.getOrDefault(sqlId, new AtomicLong(0)).get();
long slowCount = slowSqlCountMap.getOrDefault(sqlId, new AtomicLong(0)).get();
Map<String, Object> sqlStats = new HashMap<>();
sqlStats.put("count", count);
sqlStats.put("totalTime", totalTime);
sqlStats.put("avgTime", count > 0 ? totalTime / count : 0);
sqlStats.put("slowCount", slowCount);
result.put(sqlId, sqlStats);
}
return result;
}
/**
* 重置统计信息
*/
public void resetStatistics() {
sqlCountMap.clear();
sqlTimeMap.clear();
slowSqlCountMap.clear();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
if (properties != null) {
String enableSqlLog = properties.getProperty("enableSqlLog");
String showExecuteTime = properties.getProperty("showExecuteTime");
String showSqlType = properties.getProperty("showSqlType");
String showMapperInfo = properties.getProperty("showMapperInfo");
String enableStatistics = properties.getProperty("enableStatistics");
String slowSqlThreshold = properties.getProperty("slowSqlThreshold");
String prettyFormat = properties.getProperty("prettyFormat");
if (enableSqlLog != null) {
this.enableSqlLog = Boolean.parseBoolean(enableSqlLog);
}
if (showExecuteTime != null) {
this.showExecuteTime = Boolean.parseBoolean(showExecuteTime);
}
if (showSqlType != null) {
this.showSqlType = Boolean.parseBoolean(showSqlType);
}
if (showMapperInfo != null) {
this.showMapperInfo = Boolean.parseBoolean(showMapperInfo);
}
if (enableStatistics != null) {
this.enableStatistics = Boolean.parseBoolean(enableStatistics);
}
if (slowSqlThreshold != null) {
try {
this.slowSqlThreshold = Long.parseLong(slowSqlThreshold);
} catch (NumberFormatException e) {
// 使用默认值
}
}
if (prettyFormat != null) {
this.prettyFormat = Boolean.parseBoolean(prettyFormat);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# 4. 拦截器配置与使用
# 4.1 在 MyBatis 配置文件中注册拦截器
在 mybatis-config.xml
中配置:
<configuration>
<!-- 其他配置 -->
<plugins>
<!-- 基础版SQL监控拦截器 -->
<plugin interceptor="com.example.mybatis.interceptor.SqlMonitorInterceptor">
<property name="enableSqlLog" value="true"/>
<property name="showExecuteTime" value="true"/>
<property name="slowSqlThreshold" value="1000"/>
</plugin>
<!-- 或者使用增强版拦截器 -->
<!--
<plugin interceptor="com.example.mybatis.interceptor.EnhancedSqlMonitorInterceptor">
<property name="enableSqlLog" value="true"/>
<property name="showExecuteTime" value="true"/>
<property name="showSqlType" value="true"/>
<property name="showMapperInfo" value="true"/>
<property name="enableStatistics" value="true"/>
<property name="slowSqlThreshold" value="1000"/>
<property name="prettyFormat" value="true"/>
</plugin>
-->
</plugins>
</configuration>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 4.2 在 Spring Boot 中配置拦截器
在 Spring Boot 项目中,可以通过 @Configuration
类注册拦截器:
package com.example.config;
import com.example.mybatis.interceptor.SqlMonitorInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class MyBatisConfig {
@Bean
public SqlMonitorInterceptor sqlMonitorInterceptor() {
SqlMonitorInterceptor interceptor = new SqlMonitorInterceptor();
Properties properties = new Properties();
properties.setProperty("enableSqlLog", "true");
properties.setProperty("showExecuteTime", "true");
properties.setProperty("slowSqlThreshold", "1000");
interceptor.setProperties(properties);
return interceptor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 4.3 在 Spring XML 配置中注册拦截器
在 Spring XML 配置文件中:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
<property name="plugins">
<array>
<bean class="com.example.mybatis.interceptor.SqlMonitorInterceptor">
<property name="properties">
<props>
<prop key="enableSqlLog">true</prop>
<prop key="showExecuteTime">true</prop>
<prop key="slowSqlThreshold">1000</prop>
</props>
</property>
</bean>
</array>
</property>
</bean>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5. 拦截器输出效果展示
# 5.1 基础版拦截器输出示
=========================== SQL执行详情 ===========================
| 执行SQL:SELECT id, username, email, create_time, update_time FROM t_user WHERE id = 1 AND status = 'ACTIVE'
| 执行时间:15ms
==================================================================
2
3
4
# 5.2 增强版拦截器输出示例(普通格式)
=========================== SQL执行详情 ===========================
| Mapper方法:com.example.mapper.UserMapper.getUserById
| SQL类型:SELECT
| 执行SQL:SELECT id, username, email, create_time, update_time FROM t_user WHERE id = 1 AND status = 'ACTIVE'
| 执行时间:15ms
==================================================================
2
3
4
5
6
# 5.3 增强版拦截器输出示例(美化格式)
=========================== SQL执行详情 ===========================
| Mapper方法:com.example.mapper.UserMapper.getUserById
| SQL类型:SELECT
| 执行SQL:
SELECT id, username, email, create_time, update_time
FROM t_user
WHERE id = 1
AND status = 'ACTIVE'
| 执行时间:15ms
==================================================================
2
3
4
5
6
7
8
9
10
# 5.4 慢查询标记示例
=========================== SQL执行详情 ===========================
| Mapper方法:com.example.mapper.OrderMapper.getOrdersWithItems
| SQL类型:SELECT
| 执行SQL:SELECT o.id, o.order_no, o.user_id, o.create_time, i.id as item_id, i.product_id, i.quantity, i.price FROM t_order o LEFT JOIN t_order_item i ON o.id = i.order_id WHERE o.user_id = 1 ORDER BY o.create_time DESC
| 执行时间:1245ms [慢查询]
==================================================================
2
3
4
5
6
# 5.5 异常SQL输出示例
====================== SQL执行异常详情 ======================
| Mapper方法:com.example.mapper.UserMapper.updateUser
| SQL类型:UPDATE
| 执行SQL:UPDATE t_user SET username = 'admin', email = 'admin@example.com', update_time = '2023-08-18 15:30:45' WHERE id = 999
| 执行时间:35ms
| 异常信息:Duplicate entry 'admin' for key 'uk_username'
==============================================================
2
3
4
5
6
7
# 6. 拦截器高级应用场景
# 6.1 SQL 执行统计与分析
增强版拦截器提供了统计功能,可以收集 SQL 执行次数、总执行时间、平均执行时间和慢查询次数等信息。这些统计数据可以通过 API 获取,或者定期输出到日志中,用于分析系统性能瓶颈。
// 获取统计信息
@RestController
@RequestMapping("/admin/sql")
public class SqlMonitorController {
@Autowired
private EnhancedSqlMonitorInterceptor sqlMonitorInterceptor;
@GetMapping("/statistics")
public Map<String, Map<String, Object>> getStatistics() {
return sqlMonitorInterceptor.getStatistics();
}
@PostMapping("/reset")
public void resetStatistics() {
sqlMonitorInterceptor.resetStatistics();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6.2 与监控系统集成
拦截器可以与 Prometheus、Grafana 等监控系统集成,实时监控 SQL 执行情况:
// 在拦截器中添加 Prometheus 指标收集
private static final Counter SQL_COUNTER = Counter.build()
.name("mybatis_sql_count")
.help("MyBatis SQL execution count")
.labelNames("sqlId", "sqlType")
.register();
private static final Histogram SQL_EXECUTION_TIME = Histogram.build()
.name("mybatis_sql_execution_time")
.help("MyBatis SQL execution time in milliseconds")
.labelNames("sqlId", "sqlType")
.buckets(10, 50, 100, 500, 1000, 5000)
.register();
// 在统计方法中添加
SQL_COUNTER.labels(sqlId, sqlType.toString()).inc();
SQL_EXECUTION_TIME.labels(sqlId, sqlType.toString()).observe(costTime);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 6.3 自动识别并优化慢查询
拦截器可以自动识别慢查询,并提供优化建议:
// 在拦截器中添加慢查询分析功能
private void analyzeSqlPerformance(String sql, long costTime, String sqlId) {
if (costTime > slowSqlThreshold) {
StringBuilder analysis = new StringBuilder("\n================ 慢查询分析 ================\n");
analysis.append("| SQL ID: ").append(sqlId).append("\n");
analysis.append("| 执行时间: ").append(costTime).append("ms\n");
analysis.append("| 可能的原因:\n");
// 检查是否包含 SELECT *
if (sql.toUpperCase().contains("SELECT *")) {
analysis.append("| - 使用了 SELECT *,建议只查询需要的列\n");
}
// 检查是否缺少 WHERE 条件
if (!sql.toUpperCase().contains("WHERE")) {
analysis.append("| - 缺少 WHERE 条件,可能导致全表扫描\n");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 6.4 多环境配置与日志级别控制
在不同环境中,可能需要不同的 SQL 监控策略。比如,开发环境需要详细的 SQL 信息,而生产环境可能只需要记录慢查询。
// 环境感知配置
@Configuration
public class SqlMonitorConfiguration {
@Bean
@Profile({"dev", "test"})
public SqlMonitorInterceptor devSqlMonitorInterceptor() {
SqlMonitorInterceptor interceptor = new SqlMonitorInterceptor();
Properties properties = new Properties();
properties.setProperty("enableSqlLog", "true");
properties.setProperty("showExecuteTime", "true");
properties.setProperty("showSqlType", "true");
properties.setProperty("showMapperInfo", "true");
properties.setProperty("prettyFormat", "true");
properties.setProperty("slowSqlThreshold", "500");
interceptor.setProperties(properties);
return interceptor;
}
@Bean
@Profile("prod")
public SqlMonitorInterceptor prodSqlMonitorInterceptor() {
SqlMonitorInterceptor interceptor = new SqlMonitorInterceptor();
Properties properties = new Properties();
properties.setProperty("enableSqlLog", "false"); // 默认不开启完整日志
properties.setProperty("enableSlowSqlLog", "true"); // 只记录慢查询
properties.setProperty("slowSqlThreshold", "1000");
interceptor.setProperties(properties);
return interceptor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
通过运行时配置控制日志级别:
@RestController
@RequestMapping("/admin/sql")
public class SqlMonitorController {
@Autowired
private EnhancedSqlMonitorInterceptor sqlMonitorInterceptor;
@PostMapping("/config")
public void updateConfig(@RequestBody Map<String, String> config) {
Properties properties = new Properties();
properties.putAll(config);
sqlMonitorInterceptor.setProperties(properties);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.5 结合 ELK 实现 SQL 日志集中分析
将 SQL 监控日志集成到 ELK(Elasticsearch + Logstash + Kibana)系统中,可以实现更强大的日志分析和可视化功能。
日志配置(logback.xml):
<appender name="SQL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/sql.log</file>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>sqlId</includeMdcKeyName>
<includeMdcKeyName>sqlType</includeMdcKeyName>
<includeMdcKeyName>executionTime</includeMdcKeyName>
<includeMdcKeyName>isSlowSql</includeMdcKeyName>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/sql.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
<logger name="com.example.mybatis.interceptor.SqlMonitorInterceptor" level="INFO" additivity="false">
<appender-ref ref="SQL_FILE" />
</logger>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在拦截器中添加 MDC 信息:
import org.slf4j.MDC;
// 在日志输出前设置MDC
private void setLogMDC(String sqlId, SqlCommandType sqlType, long executionTime, boolean isSlowSql) {
MDC.put("sqlId", sqlId);
MDC.put("sqlType", sqlType.toString());
MDC.put("executionTime", String.valueOf(executionTime));
MDC.put("isSlowSql", String.valueOf(isSlowSql));
}
// 在日志输出后清理MDC
private void clearLogMDC() {
MDC.remove("sqlId");
MDC.remove("sqlType");
MDC.remove("executionTime");
MDC.remove("isSlowSql");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7. 最佳实践与注意事项
# 7.1 性能影响控制
SQL 监控拦截器虽然有助于调试和性能分析,但也会带来一定的性能开销。为了减少这种影响,可以采取以下措施:
- 按需启用:在生产环境中,考虑只对特定模块或特定类型的 SQL 启用监控
- 选择性记录:只记录慢查询或异常 SQL,而不是所有 SQL
- 异步处理:将日志记录和统计操作放在异步线程中执行
- 采样记录:在高并发场景下,考虑采用采样方式记录 SQL,如每 100 次执行中随机记录 1 次
// 采样记录示例
private final Random random = new Random();
private final int samplingRate = 100; // 采样率:1/100
private boolean shouldSample() {
return random.nextInt(samplingRate) == 0;
}
// 在日志记录方法中使用
if (!enableSqlLog && !(costTime > slowSqlThreshold) && !shouldSample()) {
return; // 不记录日志
}
2
3
4
5
6
7
8
9
10
11
12
# 7.2 敏感信息保护
SQL 语句中可能包含敏感信息,如密码、身份证号、手机号等。在记录完整 SQL 时,应当对这些信息进行脱敏处理。
/**
* 脱敏处理敏感参数
*/
private String maskSensitiveInfo(String sql) {
// 脱敏密码
sql = sql.replaceAll("(?i)(password\\s*=\\s*)'[^']*'", "$1'******'");
sql = sql.replaceAll("(?i)(passwd\\s*=\\s*)'[^']*'", "$1'******'");
// 脱敏手机号
sql = sql.replaceAll("(1[3-9]\\d{9})", maskPhoneNumber("$1"));
// 脱敏身份证号
sql = sql.replaceAll("(\\d{6})(\\d{8})(\\w{4})", "$1********$3");
return sql;
}
private String maskPhoneNumber(String phone) {
if (phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 7.3 日志存储与清理策略
SQL 日志可能会占用大量存储空间,特别是在高并发系统中。应制定合理的日志存储和清理策略:
- 日志轮转:使用日志轮转机制,按日期或大小自动拆分日志文件
- 清理策略:定期清理过期日志,只保留最近 N 天的日志
- 压缩存储:对历史日志进行压缩存储,减少空间占用
- 分析提取:定期对日志进行分析,提取重要统计信息后,可以删除原始日志
# 7.4 与应用监控系统的集成策略
将 SQL 监控与应用监控系统(如 Prometheus、Grafana、Skywalking 等)集成时,应当考虑:
- 指标精简:只收集关键指标,避免指标爆炸
- 标签合理化:为指标添加合适的标签(如 SQL 类型、模块名等),便于筛选和聚合
- 聚合上报:对于高频执行的 SQL,考虑先在应用内聚合,再定期上报,减少监控系统压力
- 告警配置:设置合理的告警阈值,避免误报和漏报