MyBatis拦截器实现SQL打印
2021-02-08 21:25:27 1282
mybatis有自带的sql打印, 但只会出现在抛异常的时候, 或者配置日志输出, 但是输出的日志较为冗长
像这样
### Error querying database. Cause: java.lang.ArithmeticException: / by zero
### The error may exist in file [/usr/local/userMapper.xml]
### The error may involve com.xxx.xxx.UserMapper.query_COUNT
### The error occurred while handling results
### SQL: SELECT count(0) FROM (SELECT id, username FROM user) table_count
### Cause: java.lang.ArithmeticException: / by zero
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) ~[mybatis-3.5.6.jar:3.5.6]
翻了下源码, 找到了这个方法, 贴一下
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
其中ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
顾名思义, 对异常进行包装
点进方法
public static RuntimeException wrapException(String message, Exception e) {
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}
关键在于ErrorContext.instance()
的toString()
该方法的部分代码
// sql
if (sql != null) {
description.append(LINE_SEPARATOR);
description.append("### SQL: ");
description.append(sql.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').trim());
}
且通过ErrorContext.instance()
这个方法也能知道, 该类维护了一个ThreadLocal, 保证sql是线程安全的
于是就有了如下拦截器代码
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
}
)
@Log4j2
public class MybatisSqlLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object proceed = invocation.proceed();
try {
Field field = ErrorContext.class.getDeclaredField("sql");
field.setAccessible(true);
Object sql = field.get(ErrorContext.instance());
if (sql != null) {
System.out.println("mybatis sql: " + sql.toString());
}
} catch (Exception e) {
log.error("sql打印失败", e);
}
return proceed;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
执行查询, sql打印, 完成
如果想将参数也填充进去
可以这么写
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
}
)
@Log4j2
public class MybatisSqlLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object proceed = invocation.proceed();
// 获取xml中的一个select/update/insert/delete节点,是一条SQL语句
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
Object parameter = null;
// 获取参数,if语句成立,表示sql语句有参数,参数格式是map形式
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
}
// BoundSql就是封装myBatis最终产生的sql类
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
// 获取节点的配置
Configuration configuration = mappedStatement.getConfiguration();
// 获取到最终的sql语句
String sql = showSql(configuration, boundSql);
System.out.println("mybatis sql: " + sql);
return proceed;
}
/**
* 如果参数是String,则添加单引号,
* 如果是日期,则转换为时间格式器并加单引号;
* 对参数是null和不是null的情况作了处理
*/
private static String getParameterValue(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;
}
/**
* 进行?的替换
*
* @param configuration
* @param boundSql
* @return
*/
public String showSql(Configuration configuration, BoundSql boundSql) throws NoSuchFieldException, IllegalAccessException {
// 获取参数
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// sql语句中多个空格都用一个空格代替
String sql = getRealSql().replaceAll("[\\s]+", " ");
if (parameterObject != null) {
// 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 如果根据parameterObject.getClass()可以找到对应的类型,则替换
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(parameterObject)));
} else {
// MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对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(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
// 该分支是动态sql
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(getParameterValue(obj)));
} else {
// 打印出缺失,提醒该参数缺失并防止错位
sql = sql.replaceFirst("\\?", "缺失");
}
}
}
}
return sql;
}
/**
* 获取真实sql, mybatis将其存在ThreadLocal中
* @return
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
private String getRealSql() throws IllegalAccessException, NoSuchFieldException {
Field field = ErrorContext.class.getDeclaredField("sql");
field.setAccessible(true);
Object sql = field.get(ErrorContext.instance());
if (sql != null) {
return sql.toString();
}
return "";
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}