说明
日常开发中,有些时候会有要获取到MyBatis执行时的完整SQL的需求。在此记录一下我自己的实现方式。
提示
其实也可以用MyBatis拦截器来实现,但是我的拦截器用于实现结果集解密和通用Mapper了,所以使用切面来实现。
首先定义切入点表达式,并选择 Around
:
@Around("execution(* com.test.mapper.*.*(..))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
// .......
}
提示
由于我把MyBatis各种信息封装到一个类中,这个类构造器执行完毕后就持有了所有信息,现在单独拆分代码解释。
import cn.hutool.core.util.ReflectUtil;
// ------------------------------------------------
Signature signature = pjp.getSignature();
// 接口
// 因为拦的是Mapper,所以一定是MapperStatement
Class<?> mapperInterface = signature.getDeclaringType();
// 方法名
String methodName = signature.getName();
// 获取当前执行的方法,我记得我之前用的切面是可以直接获取方法对象的
// 但是现在用的版本获取不到,所以才用这个方式获取方法对象
// 因为是Mapper方法,所以通常在一个接口中只会存在一个名字的方法,标签内不允许定义重复名称
Method method = ReflectUtil.getMethodByName(mapperInterface, methodName);
import org.apache.ibatis.mapping.MappedStatement;
// ------------------------------------------------
// MappedStatement (接口方法的SQL定义,包含映射、SQL等元素)
// 这里其实就是 org.apache.ibatis.session.Configuration#getMappedStatement()方法
MappedStatement mappedStatement = MyBatisUtil.getMappedStatement(mapperInterface, methodName);
// 获取参数对象,需要用这个对象获取SQL。
Object paramObj = MyBatisUtil.getNamedParams(method, pjp.getArgs());
提示
> 上面代码中获取的 paramObj 对象,其实就是MyBatis对我们在Mapper方法形参加的 @Param 注解的处理结果。
> 如果我们只有一个参数:String city
,并且加上了 @Param("city") 注解的情况下,
> 那么 paramObj 对象会是一个 MapperMethod.ParamMap
类型的对象,
> 里面大概存储着两个键值对:city -> 参数值
param_1 = 参数值
> 如果我们只有一个参数,但是是对象:UserEntity entity
,那么这时的 paramObj 就是这个 UserEntity
对象。
我们在下面代码会用
MappedStatement#getBoundSql()
方法获取SQL脚本,这样可能会导致一个问题出现。
如果你有定义过
SqlProvider
,也就是类似这样的定义:@SelectProvider(type = BaseProvider.class, method = "getUser")
它会调用 BaseProvider 类的 getUser 方法,用于获取SQL脚本,默认情况下这样是没有问题的。
但是如果你在 BaseProvider 类中有对 参数对象(例如上面提到的 UserEntity 对象) 做出赋值的操作,
这样就会导致一个问题:重复赋值。
这其实是因为
MappedStatement#getBoundSql()
方法的原因,再加上方法参数在MyBatis中一直流转,
由于MyBatis本身会调用一次这个方法,在你实现了SqlProvider
的前提下,这个调用其实是调用了SqlProvider
的方法。
如果你在
SqlProvider
的方法(例如上面提到的 BaseProvider.getUser)中有赋值动作,
比方说entity.setName("test" + entity.getName)
,那么我们的程序中还会调用一次,
那么entity.setName()
就被执行了两次,这样参数就乱了。
解决方案
最简单的方法是用 paramObj
参数对象深拷贝一份对象出来,这样互不干扰。我们只需要一份拷贝对象即可实现逻辑。
// 因为参数对象类型不同,拷贝逻辑也需灵活变动。
// 使用不同的拷贝方式
Object paramObject;
// 多属性或单独非对象属性
if (paramObj instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap<Object> paramMap = (MapperMethod.ParamMap<Object>) paramObj;
// 新Map
MapperMethod.ParamMap<Object> newParamMap = new MapperMethod.ParamMap<>();
// Json根据层级深拷贝
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
Object value = entry.getValue();
// 过滤空
if (Objects.isNull(value)) {
newParamMap.put(entry.getKey(), null);
continue;
}
// 数组(由于Json序列化数组再反序列化回来时是一个JsonArray对象,所以不使用Json做深拷贝)
if (ArrayUtil.isArray(value)) {
Object[] valueArray = ((Object[]) value).clone();
newParamMap.put(entry.getKey(), valueArray);
} else {
// 非数组
newParamMap.put(entry.getKey(), JsonUtil.clone(value));
}
}
paramObject = newParamMap;
} else {
// 可能是对象,直接拷贝
paramObject = (Objects.isNull(paramObj)) ? null : JsonUtil.clone(paramObj);
}
这样拷贝出的对象我们就可以直接使用,不会出现这种类似的问题。
BoundSql boundSql = MyBatisUtil.getBoundSql(mappedStatement, paramObject);
这样获取出的SQL并不完整,因为默认使用预编译,所以其中可能还充斥着
?
占位符
所以我们还需要对boundSql
对象做一道处理:把?
占位符顺序替换为我们正常的参数
String fullSql = MyBatisUtil.getFullSql(boundSql, paramObject);
import cn.hutool.core.stream.StreamUtil;
import com.test.ParameterValue;
import org.apache.ibatis.builder.SqlSourceBuilder;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.reflection.ArrayUtil;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Method;
import java.sql.Array;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.*;
/**
* @author wcp
* @since 2023/5/24
*/
public class MyBatisUtil {
private static Configuration configuration;
/**
* 获取{@link MappedStatement}
*
* @param mapperInterface 接口Class
* @param methodName 方法名
* @return {@link MappedStatement}
*/
public static MappedStatement getMappedStatement(Class<?> mapperInterface, String methodName) {
return getConfiguration().getMappedStatement(mapperInterface.getName() + "." + methodName);
}
/**
* 获取 MyBatis定义的参数
* <pre>
* 就是@Param("city") String city,把@Param的值当作key
* </pre>
*
* @param method 方法对象
* @param args 方法参数
* @return 参数,可用来获取SQL
*/
public static Object getNamedParams(Method method, Object[] args) {
ParamNameResolver paramNameResolver = new ParamNameResolver(getConfiguration(), method);
return paramNameResolver.getNamedParams(args);
}
/**
* 获取SQL封装对象(需要携带参数对象,因为{@link BoundSql}对象是需要通过参数获取完整SQL)
*
* @param mappedStatement 可以理解为mapper方法
* @param namedParams 被包装过的参数
* @return SQL脚本
*/
public static BoundSql getBoundSql(MappedStatement mappedStatement, Object namedParams) {
// 获取SQL及参数包装对象
return mappedStatement.getBoundSql(namedParams);
}
/**
* 获取完整Sql(按照 MyBatis底层源码操作)
*
* @param boundSql Sql包装对象
* @param parameterObject Mapper方法入参
* @return 完整SQL脚本
*/
public static String getFullSql(BoundSql boundSql, Object parameterObject) {
// 获取带有占位符的Sql
String sql = boundSql.getSql();
// 去除换行等空白
sql = SqlSourceBuilder.removeExtraWhitespaces(sql);
// 获取参数
Map<Integer, ParameterValue> parameterMap = getParameterMap(boundSql, parameterObject);
// 替换?为 {}(简单方案)
sql = sql.replaceAll("\\?", "{}");
// 按顺序排列
int size = parameterMap.size();
List<String> valueList = new ArrayList<>(size);
for (int i = 1; i <= size; i++) {
ParameterValue parameterValue = parameterMap.get(i);
valueList.add(objectValueString(parameterValue.getValue(), parameterValue.getJdbcType()));
}
return StrUtil.format(sql, valueList.toArray());
}
/**
* 获取参数 Map(key: 参数序号;value: 参数值)
*
* @param boundSql Sql包装对象
* @param parameterObject Sql完整参数
* @return 参数 Map
*/
public static Map<Integer, ParameterValue> getParameterMap(BoundSql boundSql, Object parameterObject) {
// 参数 Map(key -> 序号,value值)
Map<Integer, ParameterValue> parameterMap = new HashMap<>();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
JdbcType jdbcType = parameterMapping.getJdbcType();
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
parameterMap.put(i + 1, new ParameterValue(propertyName, value, jdbcType));
}
}
}
return parameterMap;
}
/**
* 获取对象的toString结果
*
* @param value 对象
* @return String
*/
public static String objectValueString(Object value, JdbcType jdbcType) {
if (Objects.isNull(value)) {
return "null";
}
if (value instanceof Array) {
try {
return ArrayUtil.toString(((Array) value).getArray());
} catch (SQLException e) {
return value.toString();
}
}
// 通过获取JdbcType做特殊处理,并通过其类型判断是否加单引号展示
return typeToString(transformJdbcType(value, jdbcType));
}
/**
* 通过JdbcType做特殊处理(需要根据JdbcType转换成不同类型的值)
*
* @param value 参数值
* @param jdbcType 数据库类型
* @return 转换后的值
*/
public static Object transformJdbcType(Object value, JdbcType jdbcType) {
if (Objects.isNull(value)) {
return "null";
}
Object result = value;
if (Objects.isNull(jdbcType)) {
if (value instanceof Date) {
Date date = (Date) value;
result = new Timestamp(date.getTime());
}
} else {
switch (jdbcType) {
case DATE:
Date date = (Date) value;
result = new java.sql.Date(date.getTime());
break;
case TIME:
Date time = (Date) value;
result = new Time(time.getTime());
break;
// todo 还有的自己补充
}
}
return result;
}
/**
* 根据值的不同类型判断是否加单引号
*
* @param value 值
* @return 转换后的值
*/
private static String typeToString(Object value) {
if ((value instanceof String) || (value instanceof Date)) {
return "'" + value + "'";
} else {
return value.toString();
}
}
/**
* 获取 MyBatis 全局配置对象(多数据源情况可能)
*
* @return 全局配置
*/
public static synchronized Configuration getConfiguration() {
if (Objects.isNull(configuration)) {
// 暂时是单数据源可以这样做
SqlSessionFactory factory = SpringUtil.getBean(SqlSessionFactory.class);
configuration = factory.getConfiguration();
}
return configuration;
}
}
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
@UtilityClass
public class JsonUtil {
public static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 深拷贝
*
* @param obj 对象
* @param <T> 参数化类型
* @return 完全不一致的对象
*/
@SneakyThrows
@SuppressWarnings("unchecked")
public static <T> T clone(T obj) {
if (Objects.isNull(obj)) {
return null;
}
return (T) toJavaBean(toJson(obj), obj.getClass());
}
/**
* Json反序列化为Java对象
*
* @param json Json
* @param clz 对象类型
* @param <T> 对象的参数化类型
* @return Java对象
*/
public static <T> T toJavaBean(String json, Class<T> clz) {
return toJavaBean(json, clz, false);
}
/**
* Json反序列化为Java对象
*
* @param json Json
* @param clz 对象类型
* @param ignoreCase 反序列化时是否忽略Json Key的大小写
* @param <T> 对象的参数化类型
* @return Java对象
*/
@SneakyThrows
public static <T> T toJavaBean(String json, Class<T> clz, boolean ignoreCase) {
// fastJson可以忽略大小写差异进行反序列化,而Jackson比较严格
return (ignoreCase) ? JSON.parseObject(json).toJavaObject(clz) : objectMapper.readValue(json, clz);
}
}
import lombok.Getter;
import org.apache.ibatis.type.JdbcType;
import java.util.Objects;
@Getter
public class ParameterValue {
private final String propertyName;
private final Object value;
private final String type;
private final JdbcType jdbcType;
public ParameterValue(String propertyName, Object value, JdbcType jdbcType) {
this.propertyName = propertyName;
this.value = value;
this.type = (Objects.isNull(value)) ? "" : value.getClass().getSimpleName();
this.jdbcType = jdbcType;
}
}