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的核心。这是低层次的类和方法,要谨慎使用插件。
以下通过代码来演示一下如何使用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})})publicclassSqlCostInterceptor implementsInterceptor {@OverridepublicObject intercept(Invocation invocation) throwsThrowable {Object target = invocation.getTarget();longstartTime = System.currentTimeMillis();StatementHandler statementHandler = (StatementHandler)target;try{returninvocation.proceed();} finally{longendTime = System.currentTimeMillis();longsqlCost = 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]");}}@OverridepublicObject plugin(Object target) {returnPlugin.wrap(target, this);}@OverridepublicvoidsetProperties(Properties properties) {}@SuppressWarnings("unchecked")privateString formatSql(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) {// 输入sql字符串空判断if(sql == null|| sql.length() == 0) {return"";}// 美化sqlsql = beautifySql(sql);// 不传参数的场景,直接把Sql美化一下返回出去if(parameterObject == null|| parameterMappingList == null|| parameterMappingList.size() == 0) {returnsql;}// 定义一个没有替换过占位符的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"));}} elseif(isMap(parameterObjectClass)) {// 如果参数是Map则直接强转,通过map.get(key)方法获取真正的属性值// 这里主要是为了处理<insert>、<delete>、<update>、<select>时传入parameterType为map的场景Map<?, ?> paramMap = (Map<?, ?>) parameterObject;sql = handleMapParameter(sql, paramMap, parameterMappingList);} else{// 通用场景,比如传的是一个自定义的对象或者八种基本数据类型之一或者Stringsql = handleCommonParameter(sql, parameterMappingList, parameterObjectClass, parameterObject);}}} catch(Exception e) {// 占位符替换过程中出现异常,则返回没有替换过占位符但是格式美化过的sql,这样至少保证sql语句比BoundSql中的sql更好看returnsqlWithoutReplacePlaceholder;}returnsql;}/*** 美化Sql*/privateString beautifySql(String sql) {sql = sql.replace("\\n", "").replace("\\t", "").replace(" ", " ").replace("( ", "(").replace(" )", ")").replace(" ,", ",");returnsql;}/*** 处理参数为List的场景*/privateString 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();} elseif(objClass.isAssignableFrom(String.class)) {value = "\\""+ obj.toString() + "\\""; }sql = sql.replaceFirst("\\\\?", value);}}returnsql;}/*** 处理参数为Map的场景*/privateString 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());}}returnsql;}/*** 处理通用的场景*/privateString handleCommonParameter(String sql, List<ParameterMapping> parameterMappingList, Class<?> parameterObjectClass, Object parameterObject) throwsException {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设置为truefield.setAccessible(true);propertyValue = String.valueOf(field.get(parameterObject));if(parameterMapping.getJavaType().isAssignableFrom(String.class)) {propertyValue = "\\""+ propertyValue + "\\"";}}sql = sql.replaceFirst("\\\\?", propertyValue);}returnsql;}/*** 是否基本数据类型或者基本数据类型的包装类*/privatebooleanisPrimitiveOrPrimitiveWrapper(Class<?> parameterObjectClass) {returnparameterObjectClass.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*/privatebooleanisStrictMap(Class<?> parameterObjectClass) {returnparameterObjectClass.isAssignableFrom(StrictMap.class);}/*** 是否List的实现类*/privatebooleanisList(Class<?> clazz) {Class<?>[] interfaceClasses = clazz.getInterfaces();for(Class<?> interfaceClass : interfaceClasses) {if(interfaceClass.isAssignableFrom(List.class)) {returntrue;}}returnfalse;}/*** 是否Map的实现类*/privatebooleanisMap(Class<?> parameterObjectClass) {Class<?>[] interfaceClasses = parameterObjectClass.getInterfaces();for(Class<?> interfaceClass : interfaceClasses) {if(interfaceClass.isAssignableFrom(Map.class)) {returntrue;}}returnfalse;}} | 
分析一下这段代码(这个是改良过的版本,主要是增加了对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
						 | publicstaticObject 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) {returnProxy.newProxyInstance(type.getClassLoader(),interfaces,newPlugin(target, interceptor, signatureMap));}returntarget;} | 
因为这里的target一定是一个接口,因此可以放心使用JDK本身提供的Proxy类,这里相当于就是如果该接口满足方法签名那么就为之生成一个代理。
最后就是intercept方法了,这里就是拦截器的核心代码了,方法的逻辑我就不解释了,可以自己看一下,唯一要注意的一点就是无论如何最终一定要返回invocation.proceed(),保证拦截器的层层调用。
xml文件配置即效果演示
写完了插件,只需要在config.xml文件中进行一次配置即可,非常简单:
| 
								1
 
								2
 
								3
						 | <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", "123@sina.com", "个人使用");]执行耗时[1ms]SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "2", "123@qq.com", "企业使用");]执行耗时[1ms]SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "3", "123@sohu.com", "注册账号使用");]执行耗时[0ms] | 
不过要说明一点,这个插件只是一个简单的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 及其执行时间实现方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持快网idc。
相关文章
- 个人服务器网站搭建:如何选择合适的服务器提供商? 2025-06-10
- ASP.NET自助建站系统中如何实现多语言支持? 2025-06-10
- 64M VPS建站:如何选择最适合的网站建设平台? 2025-06-10
- ASP.NET本地开发时常见的配置错误及解决方法? 2025-06-10
- ASP.NET自助建站系统的数据库备份与恢复操作指南 2025-06-10
- 2025-07-10 怎样使用阿里云的安全工具进行服务器漏洞扫描和修复?
- 2025-07-10 怎样使用命令行工具优化Linux云服务器的Ping性能?
- 2025-07-10 怎样使用Xshell连接华为云服务器,实现高效远程管理?
- 2025-07-10 怎样利用云服务器D盘搭建稳定、高效的网站托管环境?
- 2025-07-10 怎样使用阿里云的安全组功能来增强服务器防火墙的安全性?
快网idc优惠网
QQ交流群
- 
            2025-05-24 98
- 
            2025-05-29 36
- 
            2025-05-29 37
- 
            2025-05-29 94
- 
            2025-05-29 20
 
        
 
    		 
            	 
															 
         
         
        
 
                        