本篇主要内容涉及mybatis,重点有三个
- mybatis插件开发
- mybatis与spring的整合原理和部分源码
- mybatis管理所有sql,并对所有sql加limit 1
交代一下版本,mybatis使用mybatis为springboot开发的starter
- springboot:2.4.3
- mybatis-spring-boot-starter:2.3.0
第一部分:插件开发
参考链接:MyBatis的插件开发_mybatis插件开发_文大奇Quiin的博客-CSDN博客
这直接是从参考链接里面复制过来的,参考链接里面已经把插件开发讲清楚了,只是有一点是错误的,上面的代码就是为了给所有的sql加上limit 1,经过我的试验在我的mybatis的版本下根本行不通,这就是写这篇文章的初衷,既是为了搞明白为什么行不通,也是为了怎么做才能监控所有sql并且给想要的sql加上limit 1,这个点还是很有价值的,所以研究了一下
为什么上述代码加limit 1,行不通?
查看第一张截图中的33行代码,这个获取boundSql的地方是关键,查看具体实现,截图如下
可以看到BoundSql是通过属性sqlSource调用方法拿到的,这个sqlSource很关键,查看一下它的实现
可以看到一共有4个实现类,其中 DynamicSqlSource、RawSqlSource和StaticSqlSource,是在我调试过程中实际出现的,这边埋个点,这个sqlSource使用了装饰器设计模式,一共有两种装饰方案,分别为动态sql和非动态sql提供能力支持。虽然有4个sqlSource,但是其实经过调试之后发现无论是动态还是非动态最后使用的都是StaticSqlSource,那我们来看StaticSqlSource是怎么获取BoundSql的
看到这大家应该明白了,为啥加了limit 1不会生效,因为每次获取BoundSql都是new了一个新的对象
如何给所有sql加上limit 1
这里先讲一下上面埋的点,也就是sqlSource的两种装饰方案
- 动态sql:DynamicSqlSource
- 非动态sql:RawSqlSource->StaticSqlSource
通过调试找到了XMLScriptBuilder类的parseScriptNode方法,直接上图
明白了这两种装饰方案,就能写代码了
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
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.scripting.defaults.RawSqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.scripting.xmltags.MixedSqlNode;
import org.apache.ibatis.scripting.xmltags.SqlNode;
import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Field;
import java.util.List;
/**
* @author dfg
*/
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class MybatisPluginTest implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// mapper的全限定名
System.out.println(mappedStatement.getId());
BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
String sql = boundSql.getSql();
if (mappedStatement.getSqlSource() instanceof RawSqlSource) {
// 非动态sql
RawSqlSource rawSqlSource = (RawSqlSource) mappedStatement.getSqlSource();
// 获取到rawSqlSource的sqlSource,这是为了获取StaticSqlSource的属性sql
Field rawSqlSourceField = rawSqlSource.getClass().getDeclaredField("sqlSource");
rawSqlSourceField.setAccessible(true);
// 拿到staticSqlSource
StaticSqlSource staticSqlSource = (StaticSqlSource) rawSqlSourceField.get(rawSqlSource);
// 获取属性sql
Field field = staticSqlSource.getClass().getDeclaredField("sql");
field.setAccessible(true);
// 给属性sql加上limit 1
field.set(staticSqlSource, sql + " limit 1");
} else {
// 动态sql
DynamicSqlSource dynamicSqlSource = (DynamicSqlSource) mappedStatement.getSqlSource();
Field rootSqlNodeField = dynamicSqlSource.getClass().getDeclaredField("rootSqlNode");
rootSqlNodeField.setAccessible(true);
// 获取到混合的sqlNode
MixedSqlNode mixedSqlNode = (MixedSqlNode) rootSqlNodeField.get(dynamicSqlSource);
// 拿到contents属性
Field mixedSqlNodeField = mixedSqlNode.getClass().getDeclaredField("contents");
mixedSqlNodeField.setAccessible(true);
// contents属性是个list列表
List<SqlNode> sqlNodes = (List<SqlNode>) mixedSqlNodeField.get(mixedSqlNode);
// 在列表中尾部加一个静态的sqlNode
sqlNodes.add(new StaticTextSqlNode("limit 1"));
}
return invocation.proceed();
}
}
注解已经写明白了,功能其实已经实现了,但是还没有结束,因为通过插件是每一次sql调用都会被执行一次,每次都加limit 1,肯定是不行的
第二部分:mybatis在spring环境下的初始化
先来看MybatisAutoConfiguration这个类
这个类中通过@Bean这个注解,把SqlSessionFactory交给spring管理,所以mybatis的初始化就是完善SqlSessionFactory类的过程
最重要的方法就是这个getObject方法,再来看SqlSessionFactoryBean类
afterPropertiesSet函数是关键
找到buildSqlSessionFactory
因为是通过xml写的sql所以直接看xml的Mapper解析器,来看XMLMapperBuilder
找到configurationElement
找到buildStatementFromContext,这个方法就是提取出xml配置文件里面select|insert|update|delete这4个标签的内容
找到buildStatementFromContext
找到parseStatementNode
在找到这个位置,调试基本上就已经结束了,这里就是构造sqlSource这个最重要属性的地方,在这个函数里面就有前边提到的关于sqlSouce的两种装饰方式 ,最终把sqlSource跟MappedStatement关联起来,然后在SqlSessionFactory类里面会维护一个关于MappedStatement的map结构,截个图给大家看一下
这个map结构在后面真正调用sql的时候会根据mapper的全限定名获取Mapperstatement类,拿到这个类之后里面有sqlSource,也就是真正的sql存放的地方
放一张断点的截图
第三部分:管理所有sql,并添加limit 1
在第一部分就已经说明了,通过插件的方式是无法达到目的的,在第二部分也了解了初始化的原理,那就只需要获取spring上下文中的sqlSessionFactory修改其下的sqlSource就能达到目的了
import lombok.SneakyThrows;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.scripting.xmltags.MixedSqlNode;
import org.apache.ibatis.scripting.xmltags.SqlNode;
import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.List;
/**
* mybatis在spring初始化完成之后的监听器
* @author dfg
*/
@org.springframework.context.annotation.Configuration
public class MybatisApplicationListener implements ApplicationListener<ApplicationEvent> {
@SneakyThrows
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent) {
ContextRefreshedEvent contextRefreshedEvent = (ContextRefreshedEvent) event;
SqlSessionFactory sqlSessionFactory = contextRefreshedEvent.getApplicationContext().getBean(SqlSessionFactory.class);
Configuration configuration = sqlSessionFactory.getConfiguration();
for (MappedStatement mappedStatement : new HashSet<>(configuration.getMappedStatements())) {
if (mappedStatement.getSqlSource() instanceof RawSqlSource) {
RawSqlSource rawSqlSource = (RawSqlSource) mappedStatement.getSqlSource();
Field rawSqlSourceField = rawSqlSource.getClass().getDeclaredField("sqlSource");
rawSqlSourceField.setAccessible(true);
StaticSqlSource staticSqlSource = (StaticSqlSource) rawSqlSourceField.get(rawSqlSource);
Field field = staticSqlSource.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(staticSqlSource, field.get(staticSqlSource).toString() + " limit 1");
} else {
DynamicSqlSource dynamicSqlSource = (DynamicSqlSource) mappedStatement.getSqlSource();
Field rootSqlNodeField = dynamicSqlSource.getClass().getDeclaredField("rootSqlNode");
rootSqlNodeField.setAccessible(true);
MixedSqlNode mixedSqlNode = (MixedSqlNode) rootSqlNodeField.get(dynamicSqlSource);
Field mixedSqlNodeField = mixedSqlNode.getClass().getDeclaredField("contents");
mixedSqlNodeField.setAccessible(true);
List<SqlNode> sqlNodes = (List<SqlNode>) mixedSqlNodeField.get(mixedSqlNode);
sqlNodes.add(new StaticTextSqlNode("limit 1"));
}
}
System.out.println("init ok");
}
}
}
代码跟之前差不多,只是添加方式是通过监听spring的初始化完成事件(ContextRefreshedEvent)搞定的