概览
MyBatis的SQL输出日志语句和参数是分开打印的,我们调试时需要将语句和参数再次拼接才能进行测试执行。当遇到长SQL和参数较多时,还原一个SQL往往需要十几分钟的时间。有很多优秀(懒)的程序员通过编写集成开发环境插件的方式来优化SQL还原拼接的过程,例如Idea平台下的: mybatis-log-plugin(收费)、mybatis-log-free(免费)。
最近接到的项目有很多长SQL并且参数的数量也很多,在做SQL优化时需要花很多时间在SQL拼接上,想到前同事使用MyBatis框架插件机制来打印完整的SQL日志,尝试自己实现一下,同时也是为了加强对MyBatis框架的了解。
MyBatis Plugins
MyBatis的插件机制更准确的说是拦截器,可以用来拦截执行SQL语句执行之前或之后的操作,从而实现对 SQL 执行过程的干预。例如:参数加解密、权限判断、统一字段处理、添加分页、记录SQL执行时间等功能。实现MyBatis插件非常简单,写一个实现Interceptor接口的类,并通过@Intercepts、@Signature注解描述自己拦截的行为即可。
示例-1
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Signature;
// 拦截所有的 update 方法调用
@Intercepts(
{@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})
})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// TODO 执行之前的业务处理(自定义)
Object returnObject = invocation.proceed();
// TODO 执行之后的业务处理(自定义)
return returnObject;
}
/**
* 设置插件参数,内容可以在mybatis-config.xml中设置
*/
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
Invocation
Invocation象包含了被拦截方法的所有必要信息,Target(被调用的对象),Method(被调用的方法),Arguments(传递给方法的参数列表)。proceed 方法用于继续执行被拦截的方法。如果不调用 proceed 方法,被拦截的方法将不会被执行。
MappedStatement
MappedStatement是MyBatis中的一个核心概念和内部类,它是MyBatis执行SQL语句的关键对象。每个MappedStatement对象代表了一个已经映射的SQL语句,包括了执行此SQL语句所需的所有信息,如SQL语句文本、参数映射、结果映射、缓存策略等
MappedStatement对象由MyBatis的内部机制自动创建并管理的。当用户定义了一个Mapper接口中的方法或者XML映射文件中的SQL语句时,MyBatis会根据这些定义生成相应的MappedStatement对象,并将其存储在Configuration对象的内部映射表中。
Configuration
Configuration创建和管理MappedStatement对象,是MyBatis的核心配置对象,包含了所有映射信息、类型别名、类型处理器、对象工厂等配置。
BoundSql
BoundSql代表一个已绑定参数的SQL语句。由SqlSource创建,SqlSource是MyBatis中用于解析SQL语句并创建BoundSql的抽象类,当执行一个MappedStatement时,MyBatis会根据MappedStatement中的SqlSource创建一个BoundSql实例。BoundSql包含了SQL语句文本、参数列表以及参数映射等信息。
MetaObject
MetaObject是一个重要工具类,它主要用于简化JavaBean的操作,提供了一种统一的方式来访问和操作对象的属性。
完整示例
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import java.text.DateFormat;
import java.util.*;
import java.util.regex.Matcher;
@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})
})
public class MyBatisLogPlugins implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long end = System.currentTimeMillis();
printSql(invocation, start, end);
}
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
/**
* 打印完整SQL和执行耗时
*
* @param invocation
* @param start
* @param end
*/
private void printSql(Invocation invocation, long start, long end) {
// 获取参数
Object parameter = null;
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
}
// 获取最终的SQL
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
BoundSql boundSql = ms.getBoundSql(parameter);
// 获取节点的配置
Configuration configuration = ms.getConfiguration();
// 替换SQL中的多余的空格
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
// 获取参数
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
// String、Date等基本类型的处理
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(adornParamValue(parameterObject)));
} else {
// JavaBean、Collection、Map等复杂入参对象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(adornParamValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
// 该分支是动态sql
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(adornParamValue(obj)));
} else {
//打印出缺失,提醒该参数缺失并防止错位
sql = sql.replaceFirst("\\?", "missing");
}
}
}
}
Object target = invocation.getTarget();
System.out.println(ms.getId() + " -> 耗时:" + (end - start) + " -> 完整SQL:" + sql);
}
/**
* 装饰参数值
*
* @param obj
* @return
*/
private static String adornParamValue(Object obj) {
String value;
if (obj instanceof String) {
value = "'" + obj.toString() + "'";
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
value = "'" + formatter.format(new Date()) + "'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "";
}
}
return value;
}
}
参考资料
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » 写一个MyBatis的plugins,记录完整SQL日志
发表评论 取消回复