背景
由于现在的工作变成了带别的小伙子一起做项目,就导致,整个项目中的代码不再全部都是自己熟悉的,可能主要是熟悉其中的部分代码。
但是最终项目上线,作为技术责任人,线上出任何问题,我都有责任(不管是不是我的代码)。其中,慢sql就是其中的一个风险点,解决这个风险的办法,一般就是建索引。建索引的前提是熟悉代码,熟悉代码中的sql语句是怎么写的,查询条件是怎么构造的,那么,我们在不完全掌控所有代码的情况下,怎么解决这个问题呢?
我以前的方式是,使用阿里的druid数据库连接池,这个连接池自带一个web页面,上面可以看到执行了哪些sql,我就根据sql去建立索引。
由于目前的项目中,主要使用spring boot自带的HikariCP连接池,之前研究过一次,发现这个连接池各方面也还挺不错的,也就没有把它换成druid的想法,那,我们怎么来实现sql记录的工作呢?
想必你猜到了,就是用mybatis的拦截器,拦截器拦截到sql后,就记录到某处,可以是db、可以是redis,都行,记录下来后,再去分析如何建索引就行了。
今天这一篇,会先讲下mybatis(mybatis-plus)的大致的主流程代码(初始化、执行sql)。spring boot版本2.7,mybatis版本大致如下:

mybatis mapper初始化过程
MapperScan注解处理器
趁着这次写文章,把代码流程看了下,这里也记录下。
一般来说,现在都是spring boot集成mybaits或mybatis plus,在main类中,会注解:
import org.mybatis.spring.annotation.MapperScan;
@MapperScan({"com.xxx.platform.mapper"})
@@SpringBootApplication
public class AdminBootstrap {
MapperScan定义如下:
shell
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan
其中的@Import(MapperScannerRegistrar.class),会来解析MapperScan注解:

这里解析了MapperScan注解后,会注册一个类型为MapperScannerConfigurer的bean。
MapperScannerConfigurer
shell
package org.mybatis.spring.mapper;
public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor
这个类的介绍是:
shell
searches recursively starting from a base package for interfaces and registers them as MapperFactoryBean.
递归搜索base package包名下的接口,并把他们注册为bean(工厂bean,类型为MapperFactoryBean)
它是在什么时机来做这个事呢,它实现了BeanDefinitionRegistryPostProcessor.,这个后置处理器是在没有任何bean开始创建前,允许大家注册更多的bean definition进去,或者对已有的beandefinition进行修改。
它的逻辑就是扫描指定包下的mapper接口,注册为bean:

注意这个ClassPathMapperScanner,它是继承了spring自带的扫描器ClassPathBeanDefinitionScanner,做了一点定制化的事,比如,某个包名下的类假设有100个,但其实不是所有的类都是我们的mapper,我们这里就可以自己定义如何识别,比如实现了某个markerInterface才算:
A ClassPathBeanDefinitionScanner that registers Mappers by basePackage, annotationClass, or markerInterface.
简单来说,对于一个简单的mapper接口:

在扫描成bean definition后,定义如下:
bean class为工厂bean类型,要获取具体的bean,还需要调用getObject方法来生产。
shell
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>{
}
这个bean中有几个主要的属性:
1、mapper class:
private Class<T> mapperInterface;
这个属性就是对应的业务的mapper类,如我这里的com.xxx.platform.mapper.EntityBusinessDetailInfoMapper
2、SqlSessionTemplate
由于该类型继承了SqlSessionDaoSupport,而SqlSessionDaoSupport中有如下定义:
shell
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSessionTemplate sqlSessionTemplate;
这个SqlSessionTemplate是什么呢,其实里面封装了SqlSessionFactory:
java
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
java
protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
MapperFactoryBean的创建
启动过程中,由于我们的mapper一般被autowired到其他的bean中,此时,就需要先完成mapper bean的创建。
我们前面说了,mapper bean的实际类型为MapperFactoryBean,所以实际的创建也很简单,new一个MapperFactoryBean就行了。
new完后,spring会帮我们注入属性,如上面的mapperInterface、SqlSessionTemplate;注入SqlSessionTemplate是通过方法setSqlSessionFactory完成的(set方法默认会被认为是属性注入)。
此时,就会去spring bean中查找SqlSessionFactory类型的bean。
SqlSessionFactory bean的创建
在使用了mybatis plus的starter情况下,默认就会注册SqlSessionFactory类型的bean:
shell
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory

这里还标红了一处,这就是后续要说的mybatis拦截器:
java
import org.apache.ibatis.plugin.Interceptor;
private final Interceptor[] interceptors;
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
在完成上述的SqlSessionFactory创建后,被注入到MapperFactoryBean中:


最终也就完成了SqlSessionTemplate的创建,这个SqlSessionTemplate是如下mybatis-spring.jar中的,说白了,就是spring去集成mybatis时,封装了一层,用户只需要使用SqlSessionTemplate即可:


MapperFactoryBean.getObject

java
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
这里其实就是调用:

这里的getConfiguration,也是调用底层mybatis的sqlSessionFactory的configuration:
java
public Configuration getConfiguration() {
return this.sqlSessionFactory.getConfiguration();
}
而在下述调用getMapper时:
java
org.mybatis.spring.SqlSessionTemplate#getMapper
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
上面可以看到,传下面方法的第二个入参时,把当前对象this传入了,诶,当前不是SqlSessionTemplate吗?
仔细一看,原来是实现了SqlSession接口的:
public class SqlSessionTemplate implements SqlSession
在系统没使用mybatis-plus的情况下,是会执行如下方法:
java
org.apache.ibatis.session.Configuration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
由于我这边集成的是mybatis-plus,实际执行了如下方法:
java
com.baomidou.mybatisplus.core.MybatisConfiguration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mybatisMapperRegistry.getMapper(type, sqlSession);
}
其实,也就是mybatis-plus,把原来mybatis的configuration换成了自己的MybatisConfiguration(继承了原来的),道理还是相通的:

我们继续看上面的方法:
shell
com.baomidou.mybatisplus.core.MybatisConfiguration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mybatisMapperRegistry.getMapper(type, sqlSession);
}
mybatisMapperRegistry也被换成了mybatis-plus的com.baomidou.mybatisplus.core.MybatisMapperRegistry

这个类,看名字就能猜到,里面是注册了所有的mapper类型。

所以,如下代码也就是从上述的map中,根据mapper的class类型,获取到一个MybatisMapperProxyFactory对象。
java
mybatisMapperRegistry.getMapper(type, sqlSession)
这个MybatisMapperProxyFactory也是从mybatis中扩展来的:

获取到MybatisMapperProxyFactory后,接下来就是调用它的如下newInstance方法:

newInstance如下:
java
public T newInstance(SqlSession sqlSession) {
final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
上述方法,先是new了一个MybatisMapperProxy对象,传入了sqlSession、mapperInterface等。

接下来,如下2处代码,会生成一个mapper接口的jdk动态代理,代理的invocationHandler就是创建的MybatisMapperProxy对象:
java
public T newInstance(SqlSession sqlSession) {
final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
// 2
return newInstance(mapperProxy);
}
protected T newInstance(MybatisMapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
到此为止,mapper接口的动态代理就算是生成了。
过程总结
简单来说,在MapperScan的处理过程中,在指定包名下扫到了n个mapper.java,注册为bean,bean类型为MapperFactoryBean。
在创建完MapperFactoryBean后,初始化的过程中,要注入属性,属性中包括SqlSessionFactory 等,此时就会先去spring中查找SqlSessionFactory bean的definition,然后实例化、初始化,完成后,放到spring中。
接下来,SqlSessionFactory 被注入到MapperFactoryBean 中,工厂bean就算创建完成。
接下来,调用MapperFactoryBean 工厂bean的getObject方法,生成每个mapper接口对应的bean。
此处最终会创建一个动态代理对象,invocationHandler类型为:MybatisMapperProxy。
我们下图可以看到,一个具体的mapper,它是一个动态代理类型,其中包含一个MybatisMapperProxy类型的属性:

mapper执行过程
sqlSessionProxy
在执行mapper中的业务方法的过程中,由于mapper这个动态代理对象中的invocationHandler是MybatisMapperProxy(mybatis-plus包中),所以自然是先在mybatis-plus包中的类溜达了一会,然后,还是开始调用spring-mybatis jar包中的SqlSessionTemplate来实现底层逻辑:

在SqlSessionTemplate执行方法时,没想到,还要交给另一个对象sqlSessionProxy来执行:

这个sqlSessionProxy是一个jdk动态代理对象,代理了SqlSession接口(SqlSessionTemplate是实现了该接口的)中的方法:

为什么要代理给这样一个对象呢?

这里意思是,主要的考虑就是获取SqlSession要结合spring的事务来获取,比如,开启事务的时候,底层需要保证一直使用同一个数据库连接,在同一个连接上进行sql操作、事务开启和回滚,所以,一般开启事务后,第一个sql获取到数据库连接(对应到上层就是一个session)后,存储到线程局部变量中;后续都一直从线程局部变量中获取。
如下:

sessionFactory.openSession
由于我们是第一次调用,此时没有会话存储在线程局部变量中,因此需要新建一个session。
此时,就调用到了mybatis这一层。

java
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}

上述看到,会先创建一个Executor,再创建一个DefaultSqlSession。
这个executor类型有三种:
java
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
这几种具体类型,我也并没有深入了解,可以看出,BATCH是批量操作相关的,应该是提高性能。
在创建完成后,会尝试调用org.apache.ibatis.plugin.InterceptorChain#pluginAll,试图对Executor进行jdk动态代理,代理后,调用方法时,都会先进入拦截器链,在拦截器链中执行完成后,才会继续原有的方法执行:


此处我们先不深入拦截器链的创建。
session执行sql
创建statementHandler
获取完成session后,会继续如下处理,进行方法调用:



如下获取到对应的statement,传入参数:

下图中,获取到boundSql后,其中就包含了完整sql(已完成parameter的拼接):

接下来,会执行到如下代码:
java
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 1
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 2
stmt = prepareStatement(handler, ms.getStatementLog());
// 3
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
1处,创建statementHandler
java
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 1.1
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 1.2
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
上图的1.1处,如下,判断是预编译语句还是普通语句或者是存储过程:

在new PreparedStatementHandler的过程中,还会创建parameterHandler/resultSetHandler

创建parameterHandler
java
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 尝试进行拦截器链代理
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
这里创建具体的ParameterHandler,并进行拦截器链代理。
创建resultsetHandler
java
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
创建结果集handler,并进行拦截器链代理。
2处,使用statementHandler创建statement


statement执行

至此,执行过程基本结束了。
拦截器链作用的部分
在上述源码过程中,有4处,拦截器链对这些生成的对象进行了代理,代理后,这些对象的方法在执行时,就会先进入拦截器。

这几个接口中的方法,都有可能被拦截,具体取决于,拦截器中配置了要拦截哪些方法:




总结
mybatis和spring的代码,结合得还是很紧密,有时候会弄混其中的边界,今天也算是简单理了下。
druid、HikariCP这些,算是底层,是datasource一层,mybatis要依赖这一层;
mybatis对外:SqlSessionFactory、SqlSession;
spring呢,使用sqlSessionTemplate(mybatis-spring-xxx.jar)去封装了mybatis的上述两个概念,主要是综合考虑了spring的事务。
mybatis-plus呢,替换了mybatis原本的SqlSessionFactory(其他方面的还没太研究),另一方面,继续封装,上层只需要使用mybatis-plus即可。