背景
在使用 MyBatisPlus 时,在面对复杂的动态 SQL 或多个参数的情况下,SQL 语句和其绑定的参数可能不容易直接查看,导致排查问题时非常麻烦。每次遇到 SQL 执行异常,开发人员不得不手动查看 SQL 文件或在代码中打印参数,增加了排查和修复的时间。
解决方法
写一个拦截器,所有 SQL 查询和更新的过程都会自动记录,并且每个占位符 ? 都会被替换为实际的参数值。这样,开发者可以通过日志清晰地看到完整的 SQL 语句和对应的参数,极大地方便了 SQL 调试和错误排查。
项目中使用
自定义拦截器->注册拦截器
自定义拦截器:
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
public class MybatisPlusAllSqlLog implements InnerInterceptor {
// 记录 SQL 日志的日志器
public static final Logger log = LoggerFactory.getLogger("sys-sql");
/**
* 查询前的拦截
*
* @param executor Executor实例
* @param ms MappedStatement实例
* @param parameter 查询参数
* @param rowBounds 分页信息
* @param resultHandler 结果处理器
* @param boundSql BoundSql实例
* @throws SQLException SQL异常
*/
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
logInfo(boundSql, ms, parameter);
}
/**
* 更新前的拦截
*
* @param executor Executor实例
* @param ms MappedStatement实例
* @param parameter 更新参数
* @throws SQLException SQL异常
*/
@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
logInfo(boundSql, ms, parameter);
}
/**
* 日志打印的核心逻辑
*
* @param boundSql BoundSql实例
* @param ms MappedStatement实例
* @param parameter 参数对象
*/
private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) {
try {
log.info("parameter = {}", parameter); // 使用占位符方式更为安全且性能优越
String sqlId = ms.getId(); // 获取SQL的ID
log.info("sqlId = {}", sqlId);
Configuration configuration = ms.getConfiguration(); // 获取配置对象
String sql = getSql(configuration, boundSql, sqlId); // 获取最终的SQL语句
log.info("完整的sql: {}", sql);
} catch (Exception e) {
log.error("异常: {}", e.getLocalizedMessage(), e); // 捕获异常并打印
}
}
/**
* 封装SQL语句信息,返回SQL节点id和对应的SQL语句
*
* @param configuration 配置对象
* @param boundSql BoundSql实例
* @param sqlId SQL节点ID
* @return 完整的SQL信息
*/
public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
return sqlId + ":" + showSql(configuration, boundSql); // 返回完整的sql信息
}
/**
* 替换SQL语句中的占位符?为实际的参数值
*
* @param configuration 配置对象
* @param boundSql BoundSql实例
* @return 替换后的SQL语句
*/
public static String showSql(Configuration configuration, BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject(); // 获取参数对象
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 获取SQL中所有的参数映射
String sql = boundSql.getSql().replaceAll("[\\s]+", " "); // 清除SQL中的多余空格
// 如果存在参数且参数对象不为空,则替换占位符
if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); // 获取类型处理器注册器
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject); // 获取MetaObject,用于获取参数属性
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表示形式(例如:字符串加引号,日期加引号等)
*
* @param obj 参数对象
* @return 转换后的参数值
*/
private static String getParameterValue(Object obj) {
if (obj == null) {
return "NULL"; // 处理null情况
}
if (obj instanceof String) {
return "'" + obj.toString() + "'"; // 字符串加单引号
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
return "'" + formatter.format(obj) + "'"; // 日期格式化后加单引号
} else {
return obj.toString(); // 其他类型直接调用toString()
}
}
}
注册拦截器:
@Configuration
public class MybatisConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new MybatisPlusAllSqlLog());
return mybatisPlusInterceptor;
}
}
自定义拦截器主要方法和逻辑
MybatisPlusAllSqlLog 是一个自定义的 MyBatis Plus 拦截器,它实现了 InnerInterceptor 接口。它的主要功能是拦截所有 MyBatis Plus 的 SQL 执行操作。
-
beforeQuery 和 beforeUpdate 方法:
- 在查询和更新操作之前调用,拦截 SQL 执行过程。
- 使用
logInfo
方法打印 SQL 相关信息。
-
logInfo 方法:
- 获取 SQL 的 ID (
sqlId
),这有助于定位 SQL 语句对应的 Mapper 方法。 - 使用
getSql
方法获取最终执行的 SQL 语句,并将其打印出来。
- 获取 SQL 的 ID (
-
getSql 和 showSql 方法:
getSql
:封装 SQL 的sqlId
和实际的 SQL 语句,输出完整的 SQL 信息。showSql
:替换 SQL 语句中的占位符?
为实际的参数值。首先检查参数是否为空,然后根据参数类型(如String
、Date
)替换为合适的格式。
-
getParameterValue 方法:
- 将参数对象转换为相应的 SQL 格式,例如将字符串加上单引号,将日期格式化并加上单引号。
- 处理
null
的情况,输出"NULL"
。
自定义拦截器关键点
- 日志打印:使用
Logger
打印 SQL 日志,包括 SQL ID、SQL 语句和参数。 - 替换占位符:在日志中打印替换后的 SQL 语句,使其更容易理解执行的实际操作。
- 适配不同类型的参数:根据参数类型(如字符串、日期等)生成适当的 SQL 字符串表示,确保日志清晰且无歧义。
效果
❤觉得有用的可以留个关注~~❤