mybatis 插件: 打印 sql 及其执行时间实现方法

Plugins

摘一段来自MyBatis官方文档的文字。

MyBatis允许你在某一点拦截已映射语句执行的调用。默认情况下,MyBatis允许使用插件来拦截方法调用:

Executor(update、query、flushStatements、commint、rollback、getTransaction、close、isClosed)

ParameterHandler(getParameterObject、setParameters)

ResultSetHandler(handleResultSets、handleOutputParameters)

StatementHandler(prepare、parameterize、batch、update、query)

这些类中方法的详情可以通过查看每个方法的签名来发现,而且它们的源代码存在于MyBatis发行包中。你应该理解你所覆盖方法的行为,假设你所做的要比监视调用要多。如果你尝试修改或覆盖一个给定的方法,你可能会打破MyBatis的核心。这是低层次的类和方法,要谨慎使用插件。

插件示例:打印每条SQL语句及其执行时间

以下通过代码来演示一下如何使用MyBatis的插件,要演示的场景是:打印每条真正执行的SQL语句及其执行的时间。这是一个非常有用的需求,MyBatis本身的日志可以记录SQL,但是有以下几个问题:

MyBatis日志打印出来的SQL日志,参数都被占位符”?”替换,无法知道真正执行的SQL语句中的参数是什么

MyBatis日志打印出来的SQL日志,有大量的换行符,通常一句SQL语句要通过十几行显示,阅读体验非常差

无法记录SQL执行时间,有SQL执行时间就可以精准定位到执行时间比较慢的SQL

写MyBatis插件非常简单,只需要实现Interceptor接口即可,我这里将我的Interceptor命名为SqlCostInterceptor:
?
1
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

/**

  • 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 SqlCostInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();

long startTime = System.currentTimeMillis();
StatementHandler statementHandler = (StatementHandler)target;
try {
return invocation.proceed();
} finally {
long endTime = System.currentTimeMillis();
long sqlCost = endTime - startTime;

BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();
List parameterMappingList = boundSql.getParameterMappings();

// 格式化Sql语句,去除换行符,替换参数
sql = formatSql(sql, parameterObject, parameterMappingList);

System.out.println(“SQL:[” + sql + “]执行耗时[” + sqlCost + “ms]”);
}
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {

}

@SuppressWarnings(“unchecked”)
private String formatSql(String sql, Object parameterObject, List parameterMappingList) {
// 输入sql字符串空判断
if (sql == null || sql.length() == 0) {
return “”;
}

// 美化sql
sql = beautifySql(sql);

// 不传参数的场景,直接把Sql美化一下返回出去
if (parameterObject == null || parameterMappingList == null || parameterMappingList.size() == 0) {
return sql;
}

// 定义一个没有替换过占位符的sql,用于出异常时返回
String sqlWithoutReplacePlaceholder = sql;

try {
if (parameterMappingList != null) {
Class parameterObjectClass = parameterObject.getClass();

// 如果参数是StrictMap且Value类型为Collection,获取key="list"的属性,这里主要是为了处理循环时传入List这种参数的占位符替换
// 例如select * from xxx where id in ...
if (isStrictMap(parameterObjectClass)) {
 StrictMap> strictMap = (StrictMap>)parameterObject;
   
 if (isList(strictMap.get("list").getClass())) {
  sql = handleListParameter(sql, strictMap.get("list"));
 }
} else if (isMap(parameterObjectClass)) {
 // 如果参数是Map则直接强转,通过map.get(key)方法获取真正的属性值
 // 这里主要是为了处理