一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - mybatis 插件: 打印 sql 及其執行時間實現方法

mybatis 插件: 打印 sql 及其執行時間實現方法

2020-11-24 14:53Java教程網 Java教程

下面小編就為大家帶來一篇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<ParameterMapping> 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<ParameterMapping> 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"的屬性,這里主要是為了處理<foreach>循環時傳入List這種參數的占位符替換
    // 例如select * from xxx where id in <foreach collection="list">...</foreach>
    if (isStrictMap(parameterObjectClass)) {
     StrictMap<Collection<?>> strictMap = (StrictMap<Collection<?>>)parameterObject;
      
     if (isList(strictMap.get("list").getClass())) {
      sql = handleListParameter(sql, strictMap.get("list"));
     }
    } else if (isMap(parameterObjectClass)) {
     // 如果參數是Map則直接強轉,通過map.get(key)方法獲取真正的屬性值
     // 這里主要是為了處理<insert>、<delete>、<update>、<select>時傳入parameterType為map的場景
     Map<?, ?> paramMap = (Map<?, ?>) parameterObject;
     sql = handleMapParameter(sql, paramMap, parameterMappingList);
    } else {
     // 通用場景,比如傳的是一個自定義的對象或者八種基本數據類型之一或者String
     sql = handleCommonParameter(sql, parameterMappingList, parameterObjectClass, parameterObject);
    }
   }
  } catch (Exception e) {
   // 占位符替換過程中出現異常,則返回沒有替換過占位符但是格式美化過的sql,這樣至少保證sql語句比BoundSql中的sql更好看
   return sqlWithoutReplacePlaceholder;
  }
   
  return sql;
 }
  
 /**
  * 美化Sql
  */
 private String beautifySql(String sql) {
  sql = sql.replace("\n", "").replace("\t", "").replace(" ", " ").replace("( ", "(").replace(" )", ")").replace(" ,", ",");
   
  return sql;
 }
  
 /**
  * 處理參數為List的場景
  */
 private String handleListParameter(String sql, Collection<?> col) {
  if (col != null && col.size() != 0) {
   for (Object obj : col) {
    String value = null;
    Class<?> objClass = obj.getClass();
     
    // 只處理基本數據類型、基本數據類型的包裝類、String這三種
    // 如果是復合類型也是可以的,不過復雜點且這種場景較少,寫代碼的時候要判斷一下要拿到的是復合類型中的哪個屬性
    if (isPrimitiveOrPrimitiveWrapper(objClass)) {
     value = obj.toString();
    } else if (objClass.isAssignableFrom(String.class)) {
     value = "\"" + obj.toString() + "\"";
    }
     
    sql = sql.replaceFirst("\\?", value);
   }
  }
   
  return sql;
 }
  
 /**
  * 處理參數為Map的場景
  */
 private String handleMapParameter(String sql, Map<?, ?> paramMap, List<ParameterMapping> parameterMappingList) {
  for (ParameterMapping parameterMapping : parameterMappingList) {
   Object propertyName = parameterMapping.getProperty();
   Object propertyValue = paramMap.get(propertyName);
   if (propertyValue != null) {
    if (propertyValue.getClass().isAssignableFrom(String.class)) {
     propertyValue = "\"" + propertyValue + "\"";
    }
 
    sql = sql.replaceFirst("\\?", propertyValue.toString());
   }
  }
   
  return sql;
 }
  
 /**
  * 處理通用的場景
  */
 private String handleCommonParameter(String sql, List<ParameterMapping> parameterMappingList, Class<?> parameterObjectClass,
   Object parameterObject) throws Exception {
  for (ParameterMapping parameterMapping : parameterMappingList) {
   String propertyValue = null;
   // 基本數據類型或者基本數據類型的包裝類,直接toString即可獲取其真正的參數值,其余直接取paramterMapping中的property屬性即可
   if (isPrimitiveOrPrimitiveWrapper(parameterObjectClass)) {
    propertyValue = parameterObject.toString();
   } else {
    String propertyName = parameterMapping.getProperty();
     
    Field field = parameterObjectClass.getDeclaredField(propertyName);
    // 要獲取Field中的屬性值,這里必須將私有屬性的accessible設置為true
    field.setAccessible(true);
    propertyValue = String.valueOf(field.get(parameterObject));
    if (parameterMapping.getJavaType().isAssignableFrom(String.class)) {
     propertyValue = "\"" + propertyValue + "\"";
    }
   }
 
   sql = sql.replaceFirst("\\?", propertyValue);
  }
   
  return sql;
 }
  
 /**
  * 是否基本數據類型或者基本數據類型的包裝類
  */
 private boolean isPrimitiveOrPrimitiveWrapper(Class<?> parameterObjectClass) {
  return parameterObjectClass.isPrimitive() ||
    (parameterObjectClass.isAssignableFrom(Byte.class) || parameterObjectClass.isAssignableFrom(Short.class) ||
      parameterObjectClass.isAssignableFrom(Integer.class) || parameterObjectClass.isAssignableFrom(Long.class) ||
      parameterObjectClass.isAssignableFrom(Double.class) || parameterObjectClass.isAssignableFrom(Float.class) ||
      parameterObjectClass.isAssignableFrom(Character.class) || parameterObjectClass.isAssignableFrom(Boolean.class));
 }
  
 /**
  * 是否DefaultSqlSession的內部類StrictMap
  */
 private boolean isStrictMap(Class<?> parameterObjectClass) {
  return parameterObjectClass.isAssignableFrom(StrictMap.class);
 }
  
 /**
  * 是否List的實現類
  */
 private boolean isList(Class<?> clazz) {
  Class<?>[] interfaceClasses = clazz.getInterfaces();
  for (Class<?> interfaceClass : interfaceClasses) {
   if (interfaceClass.isAssignableFrom(List.class)) {
    return true;
   }
  }
   
  return false;
 }
  
 /**
  * 是否Map的實現類
  */
 private boolean isMap(Class<?> parameterObjectClass) {
  Class<?>[] interfaceClasses = parameterObjectClass.getInterfaces();
  for (Class<?> interfaceClass : interfaceClasses) {
   if (interfaceClass.isAssignableFrom(Map.class)) {
    return true;
   }
  }
   
  return false;
 }
  
}

分析一下這段代碼(這個是改良過的版本,主要是增加了對select * from xxx where id in <foreach collection=”list”>…</foreach>這種寫法占位符替換為真正參數的支持)。

首先是注解@Intercepts與@Signature,這兩個注解是必須的,因為Plugin的wrap方法會取這兩個注解里面參數。@Intercepts中可以定義多個@Signature,一個@Signature表示符合如下條件的方法才會被攔截:

接口必須是type定義的類型

方法名必須和method一致

方法形參的Class類型必須和args定義Class類型順序一致

接著的一個問題是:有四個接口可以攔截,為什么使用StatementHandler去攔截?根據名字來看ParameterHandler和ResultSetHandler,前者處理參數,后者處理結果是不可能使用的,剩下的就是Executor和StatementHandler了。攔截StatementHandler的原因是而不是用Executor的原因是:

Executor的update與query方法可能用到MyBatis的一二級緩存從而導致統計的并不是真正的SQL執行時間

StatementHandler的update與query方法無論如何都會統計到PreparedStatement的execute方法執行時間,盡管也有一定誤差(誤差主要來自會將處理結果的時間也算上),但是相差不大

接著講一下setProperties方法,可以將一些配置屬性配置在<plugin></plugin>的子標簽<property />中,所有的配置屬性會在形參Properties中,setProperties方法可以拿到配置的屬性進行需要的處理。

接著講一下plugin方法,這里是為目標接口生成代理,不需要也沒必要自己去寫生成代理的方法,MyBatis的Plugin類已經為我們提供了wrap方法(當然如果自己有自己的邏輯也可以在Plugin.wrap方法前后加入,但是最終一定要使用Plugin.wrap方法生成代理),看一下該方法的實現:

?
1
2
3
4
5
6
7
8
9
10
11
12
public static Object wrap(Object target, Interceptor interceptor) {
 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
 Class<?> type = target.getClass();
 Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
 if (interfaces.length > 0) {
  return Proxy.newProxyInstance(
   type.getClassLoader(),
   interfaces,
   new Plugin(target, interceptor, signatureMap));
 }
 return target;
}

因為這里的target一定是一個接口,因此可以放心使用JDK本身提供的Proxy類,這里相當于就是如果該接口滿足方法簽名那么就為之生成一個代理。

最后就是intercept方法了,這里就是攔截器的核心代碼了,方法的邏輯我就不解釋了,可以自己看一下,唯一要注意的一點就是無論如何最終一定要返回invocation.proceed(),保證攔截器的層層調用。

xml文件配置即效果演示

寫完了插件,只需要在config.xml文件中進行一次配置即可,非常簡單:

?
1
2
3
<plugins>
  <plugin interceptor="org.xrq.mybatis.plugin.SqlCostInterceptor" />
 </plugins>

這里每個<plugin>子標簽代表一個插件,interceptor表示攔截器的完整路徑,每個人的不同。

有了類和這段配置,就可以使用SqlCostInterceptor了,SqlCostInterceptor是通用的,但是每個人的CRUD是不同的,我打印一下我這里CRUD執行的結果:

?
1
2
3
SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "1", "[email protected]", "個人使用");]執行耗時[1ms]
SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "2", "[email protected]", "企業使用");]執行耗時[1ms]
SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "3", "[email protected]", "注冊賬號使用");]執行耗時[0ms]

看到打印了完整的SQl語句以及SQL語句執行時間。

不過要說明一點,這個插件只是一個簡單的Demo,我并沒有完整測試過,應該是無法覆蓋所有場景的,所以如果想用這段代碼片段打印真正的SQL及其執行時間的朋友,還需要在這個基礎上做修改,不過即使不改代碼,這個插件起到美化SQL的作用,去除一些換行符還是沒問題的。

至于MyBatis插件的實現原理,會在我【MyBatis源碼分析】系列文章中詳細解讀,文章地址為【MyBatis源碼分析】插件實現原理。

后記

MyBatis插件機制非常有用,用得好可以解決很多問題,不只是這里的打印SQL語句以及記錄SQL語句執行時間,分頁、分表都可以通過插件來實現。用好插件的關鍵是我開頭就列舉的,這里再列一次:

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

ParameterHandler(getParameterObject、setParameters)

ResultSetHandler(handleResultSets、handleOutputParameters)

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

只有理解這四個接口及相關方法是干什么的,才能寫出好的攔截器,開發出符合預期的功能。

以上這篇mybatis 插件: 打印 sql 及其執行時間實現方法就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持服務器之家。

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 美女靠逼的视频 | 久久久久久久久性潮 | 美女脱得一二净无内裤全身的照片 | 精品国产成a人在线观看 | 欧美日韩精品免费一区二区三区 | 97国产自拍| 亚洲精品成人 | 国产免费又粗又猛又爽视频国产 | 奇米影视888四色首页 | 国产精品思瑞在线观看 | 色综合久久中文字幕 | 高跟翘臀老师后进式视频 | 日韩激情视频在线观看 | 青草青草视频2免费观看 | 日本天堂网 | 亚洲天堂精品在线观看 | 五月色婷婷在线影院 | 四虎最新永久在线精品免费 | 四虎在线免费 | 午夜亚洲国产 | 全黄h全肉细节文在线观看 全彩成人18h漫画 | 色播影院性播影院私人影院 | 韩国一级淫片特黄特刺激 | 免费日本视频 | dasd817黑人在线播放 | 欧美成人免费观看国产 | 五月丁香啪啪. | 无码人妻丰满熟妇啪啪网不卡 | 欧亚专线欧洲m码可遇不可求 | 91香蕉在线 | 男人亚洲天堂 | 亚州综合网 | 欧美福利在线播放 | xxxxx大片在线观看 | 大杳蕉在线影院在线播放 | 爱情岛论坛亚洲品质自拍视频 | 亚洲第6页| 日韩一区在线观看 | 高清欧美videossexo免费 | 国产男人搡女人免费视频 | 日韩精品亚洲一级在线观看 |