前言
在日常使用Mybatis-plus
开发时,为了能获取到执行的sql
语句,通常可以在配置文件
进入如下的配置:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
通过配置MyBatis-plus
中将log-impl
的日志打印的实现为org.apache.ibatis.logging.stdout.StdOutImpl
,以实现sql
语句在控制台的打印。
此时,当我们执行如下sql
信息时:
<select id="selectByUserName" resultType="com.example.pojo.User">
select user_name userName , age from t_user where user_name = #{name}
</select>
可以看到在控制台会打印出如下内容:
不难发现,我们打印出的sql
信息其实是带有占位符
的。如果我们想在sql
工具中对sql
进行执行,则需要我们手动对占位符
进行替换,对于上述这样的sql
来说这并不是一件难事。但当sql
相关查询参数比较多的时,通过手动对sql
的占位符
进行替换显然不是一件明智的举措了。
为了解决这一问题,我们其实可以借助Mybatis
提供的拦截器
来获取真正执行的sql
信息,从而避免手动对占位符
的替换!
配置Mybatis
的拦截器
该类实现了InnerInterceptor
接口,主要用于在 MyBatis Plus 执行查询和更新操作之前记录相关的 SQL 信息,包括参数值、SQL 语句的 ID 以及完整的 SQL 语句内容等,以便于进行调试和查看数据库交互的具体情况。
package com.ssm.subject.infra.config;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.log4j.Log4j2;
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.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 org.springframework.util.CollectionUtils;
import java.sql.SQLException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
@Log4j2
public class MybatisPlusAllSqlLog implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
logInfo(boundSql, ms, parameter);
}
@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
logInfo(boundSql, ms, parameter);
}
private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) {
try {
log.info("parameter = " + parameter);
// 获取到节点的id,即sql语句的id
String sqlId = ms.getId();
log.info("sqlId = " + sqlId);
// 获取节点的配置
Configuration configuration = ms.getConfiguration();
// 获取到最终的sql语句
String sql = getSql(configuration, boundSql, sqlId);
log.info("完整的sql:{}", sql);
} catch (Exception e) {
log.error("异常:{}", e.getLocalizedMessage(), e);
}
}
// 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
return sqlId + ":" + showSql(configuration, boundSql);
}
// 进行?的替换
public static String showSql(Configuration configuration, BoundSql boundSql) {
// 获取参数
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// sql语句中多个空格都用一个空格代替
String sql = boundSql.getSql().replaceAll("[\s]+", " ");
if (!CollectionUtils.isEmpty(parameterMappings) && 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;
}
// 如果参数是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;
}
}
配置 SQL 打印拦截器
实现了Interceptor
接口,主要用于拦截 MyBatis 执行的 SQL 语句(包括更新和查询操作),并记录这些 SQL 语句的执行时间,根据执行时间的不同范围输出相应的日志信息,以便于对 SQL 执行性能进行监控和分析。
package com.ssm.subject.infra.config;
import lombok.extern.log4j.Log4j2;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
@Log4j2
@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, CacheKey.class, BoundSql.class})})
public class SqlStatementInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long timeConsuming = System.currentTimeMillis() - startTime;
log.info("执行SQL:{}ms", timeConsuming);
if (timeConsuming > 999 && timeConsuming < 5000) {
log.info("执行SQL大于1s:{}ms", timeConsuming);
} else if (timeConsuming >= 5000 && timeConsuming < 10000) {
log.info("执行SQL大于5s:{}ms", timeConsuming);
} else if (timeConsuming >= 10000) {
log.info("执行SQL大于10s:{}ms", timeConsuming);
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
注入Spring容器
@Configuration
public class MybatisConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new MybatisPlusAllSqlLog());
return mybatisPlusInterceptor;
}
}
测试
会把完整的sql打印出来
16:08:12.594 [http-nio-3000-exec-2] INFO com.ssm.subject.infra.config.MybatisPlusAllSqlLog - parameter = {arg0=[5, 6], collection=[5, 6], list=[5, 6]}
16:08:12.594 [http-nio-3000-exec-2] INFO com.ssm.subject.infra.config.MybatisPlusAllSqlLog - sqlId = com.ssm.subject.infra.basic.mapper.SubjectLabelDao.queryByLabelIds
16:08:12.594 [http-nio-3000-exec-2] INFO com.ssm.subject.infra.config.MybatisPlusAllSqlLog - 完整的sql:com.ssm.subject.infra.basic.mapper.SubjectLabelDao.queryByLabelIds:select * from subject_label where id in ( 5 , 6 )