假如我们想实现多租户,或者在某些SQL后面自动拼接查询条件。在开发过程中大部分场景可能都是一个查询写一个SQL去处理,我们如果想修改最终SQL可以通过修改各个mapper.xml中的SQL来处理。但实际过程中我们可能穿插着ORM和SQL的混合使用,隐藏在代码中不容易被发现,还有假如项目中有很多很多的SQL我们不可能一一的去修改解决。这个时候我们就需要通过mybatis拦截SQL并且最终修改SQL。
maven依赖
org.mybatis
mybatis-spring
2.0.4
org.springframework
spring-core
5.2.4.RELEASE
compile
mysql
mysql-connector-java
8.0.26
com.github.jsqlparser
jsqlparser
4.2
org.mybatis
mybatis-spring
2.0.4
com.baomidou
mybatis-plus
2.3.3
org.mybatis
mybatis
3.5.4
com.alibaba
druid-spring-boot-starter
1.2.6
org.projectlombok
lombok
org.projectlombok
lombok
配置部分
这部分是传统mybatis的xml配置,如果是Springboot项目或者使用JavaConfig配置的请查看官方文档配置方式。无非就是Springboot封装了mybatis-xxx-stater包将部分配置都转为了参数控制以及部分autoconfig,大同小异这里不做过多讨论。
整个拦截调用链路流程图:
[图片上传失败...(image-fbe423-1656999539717)]
① mybatis在这一层包装了StatementHandler返回代理对象,下一步调用prepare的时候会先调用增强拦截器。
Configuration.newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 对statementhandler进行代理
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
InterceptorChain.pluginAll
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
Interceptor.plugin
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
包装成代理对象
Plugin.wrap
public static Object wrap(Object target, Interceptor interceptor) {
Map, Set> signatureMap = getSignatureMap(interceptor);
Class> type = target.getClass();
// 获取target对象的所有接口类型
Class>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 学习过JDK动态代理的同学对这段代码肯定很熟,这里是生成一个代理对象
return Proxy.newProxyInstance(
type.getClassLoader(), // 类加载器
interfaces, // JDK动态代理必须要有接口
new Plugin(target, interceptor, signatureMap));
}
return target;
}
如果大家觉得这篇文章写的还可以请关注我,我后续会出
mybatis
的源码解析。
调用代码
这部分代码负责调用mybatis,如果使用springboot这部分就是你的rest接口。
import com.allens.mybatis.model.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TestMybatis {
public static void main(String[] args) throws IOException {
// 读取配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 通过SqlSessionFactoryBuilder创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 获取到SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 调用Mapper中的指定方法 com.wyh.mapper.UserMapper.queryAll是statementId
Map map = new HashMap<>();
map.put("pageSize", 1);
map.put("desc", "desc");
map.put("name", "Allens");
List userList = sqlSession.selectList("com.allens.mybatis.mappers.UserMapper.selectUsers", map);
System.out.println("++++++++++++++++++++++");
userList.forEach(System.out::println);
}
}
拦截器的代码实现
这使用了Druid的SQLParser进行解析SQL,如果不想使用druid可以使用
sqlparser
包进行sql解析。不管用什么样的工具把SQL修改掉就行了,形式不限。
如果想使用sqlparser进行解析sql可以看我的下一篇文章
/**
* MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
*
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
* ParameterHandler (getParameterObject, setParameters)
* ResultSetHandler (handleResultSets, handleOutputParameters)
* StatementHandler (prepare, parameterize, batch, update, query)
* 这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
*
* 通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可
*/
//@Intercepts({@Signature(
// type= Executor.class,
// method = "query",
// args = {StatementHandler.class, Object.class, RowBounds.class, ResultHandler.class})})
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class SQLInterceptor implements Interceptor {
private Properties properties = new Properties();
public List getInsertPropertiesName(List
可以看到上面使用了反射去设置boundsql.sql,有可能有同学会问这样会不会触发JVM优化修改不了这个
final String
属性。这里我下一个结论是可以的,为了解释这个问题我写了端代码帮助理解:
package base;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class FinalPropertyModify {
private final String a = "123";
private final String ab;
public FinalPropertyModify(String ab) {
this.ab = ab;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
// 直接赋值初始化
FinalPropertyModify finalPropertyModify = new FinalPropertyModify("123");
Field a = FinalPropertyModify.class.getDeclaredField("a");
a.setAccessible(true);
a.set(finalPropertyModify, "12345");
// 构造函数初始化
Field ab = FinalPropertyModify.class.getDeclaredField("ab");
ab.setAccessible(true);
ab.set(finalPropertyModify, "123456");
System.out.println("==================反射获取==================");
System.out.println(a.get(finalPropertyModify));
System.out.println(ab.get(finalPropertyModify));
System.out.println("==================直接获取==================");
System.out.println(finalPropertyModify.a);
System.out.println(finalPropertyModify.ab);
}
}
最终输出结果为:
[图片上传失败...(image-87672a-1656999539717)]
可以看到如果是直接赋值进行初始化final属性的话,会被JVM给优化掉,如果使用的是构造函数进行初始化属性是不是触发JVM优化的。我们再看一下boundsql类的属性定义,很显然sql
属性是在构造函数中进行初始化的。我们可以大胆的去modify sql,但一定要注意不能修改成错误的SQL和一定要考虑安全问题,mybatis没有提供sql的修改方法也是考虑这一点,可能会不安全。
public class BoundSql {
// 我们要修改的SQL属性
private final String sql;
private final List parameterMappings;
private final Object parameterObject;
private final Map additionalParameters;
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public String getSql() {
return sql;
}
public List getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
UserMapper.xml
很简单的一个SQL
测试
运行成功:
[图片上传失败...(image-5524a2-1656999539717)]