故事背景

大家还记得那些年的 P6spy,这个也是打印完整可直接执行 SQL 的利器,不过总感觉,我个人使用下来还是觉得太重,配置驱动,引入 Jar,啥啥啥的,今天无聊下给大家整了一个类似这个功能的,可以看看~

拦截器(核心)

import com.chinadaas.platform.dsp.pipeline.annotation.SqlFormat;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
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.type.TypeHandlerRegistry;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.*;
import java.util.regex.Matcher;

/**
* @author Lux Sun
* @date 2022/6/27
*/
@Slf4j
@Intercepts({
@Signature(type = org.apache.ibatis.executor.Executor.class, method = "update", args = {
MappedStatement.class, Object.class
})
})
public class SqlInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// 获取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];
System.out.println("parameter = " + parameter);
}
// 获取到节点的id, 即sql语句的id
String sqlId = mappedStatement.getId();
System.out.println("sqlId = " + sqlId);
// BoundSql就是封装myBatis最终产生的sql类
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
// 获取节点的配置
Configuration configuration = mappedStatement.getConfiguration();
// 获取到最终的sql语句
String sql = getSql(configuration, boundSql, sqlId);
System.out.println("sql = " + sql);
} catch (Exception e) {
e.printStackTrace();
}
// 执行完上面的任务后,不改变原有的sql执行过程
return invocation.proceed();
}

// 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
private static String getSql(Configuration configuration, BoundSql boundSql, String sqlId)
{
String sql = showSql(configuration, boundSql);
StringBuilder str = new StringBuilder(100);
str.append(sqlId);
str.append(":");
str.append(sql);
return str.toString();
}

// 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号; 对参数是null和不是null的情况作了处理
private static String getParameterValue(Object obj) {
String value = null;
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;
}

// 进行?的替换
private 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.isNotEmpty(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;
}

@Override
public Object plugin(Object target) {
log.info("SqlInterceptor plugin");
return Interceptor.super.plugin(target);
}

@Override
public void setProperties(Properties properties) {
log.info("SqlInterceptor setProperties");
Interceptor.super.setProperties(properties);
}
}

Ps:这边拦截只是针对 Update 操作,这个在上面自己配上 @Signature 即可,这个很简单就不再描述,对应填写的参数看下都会的!

注册拦截器

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
// 用 mybatis 的这里会有点区别, mybatis 用的是 SqlSessionFactoryBean
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
sqlSessionFactoryBean.setMapperLocations((new PathMatchingResourcePatternResolver()).getResources("classpath:mapper/*.xml"));

// 注册 SQL 拦截器
sqlSessionFactoryBean.setConfiguration(new MybatisConfiguration());
sqlSessionFactoryBean.getConfiguration().addInterceptor(new SqlInterceptor());

return sqlSessionFactoryBean.getObject();
}