利用mybatis拦截器记录sql,辅助我们建立索引(一)

背景#

由于现在的工作变成了带别的小伙子一起做项目,就导致,整个项目中的代码不再全部都是自己熟悉的,可能主要是熟悉其中的部分代码。

但是最终项目上线,作为技术责任人,线上出任何问题,我都有责任(不管是不是我的代码)。其中,慢sql就是其中的一个风险点,解决这个风险的办法,一般就是建索引。建索引的前提是熟悉代码,熟悉代码中的sql语句是怎么写的,查询条件是怎么构造的,那么,我们在不完全掌控所有代码的情况下,怎么解决这个问题呢?

我以前的方式是,使用阿里的druid数据库连接池,这个连接池自带一个web页面,上面可以看到执行了哪些sql,我就根据sql去建立索引。

由于目前的项目中,主要使用spring boot自带的HikariCP连接池,之前研究过一次,发现这个连接池各方面也还挺不错的,也就没有把它换成druid的想法,那,我们怎么来实现sql记录的工作呢?

想必你猜到了,就是用mybatis的拦截器,拦截器拦截到sql后,就记录到某处,可以是db、可以是redis,都行,记录下来后,再去分析如何建索引就行了。

今天这一篇,会先讲下mybatis(mybatis-plus)的大致的主流程代码(初始化、执行sql)。spring boot版本2.7,mybatis版本大致如下:

image-20250111162136395

mybatis mapper初始化过程#

MapperScan注解处理器#

趁着这次写文章,把代码流程看了下,这里也记录下。

一般来说,现在都是spring boot集成mybaits或mybatis plus,在main类中,会注解:

Copy
import org.mybatis.spring.annotation.MapperScan; @MapperScan({"com.xxx.platform.mapper"}) @@SpringBootApplication public class AdminBootstrap {

MapperScan定义如下:

Copy
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(MapperScannerRegistrar.class) @Repeatable(MapperScans.class) public @interface MapperScan

其中的@Import(MapperScannerRegistrar.class),会来解析MapperScan注解:

image-20250111124507024

这里解析了MapperScan注解后,会注册一个类型为MapperScannerConfigurer的bean。

MapperScannerConfigurer#

Copy
package org.mybatis.spring.mapper; public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor

这个类的介绍是:

Copy
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:

image-20250111125256257

注意这个ClassPathMapperScanner,它是继承了spring自带的扫描器ClassPathBeanDefinitionScanner,做了一点定制化的事,比如,某个包名下的类假设有100个,但其实不是所有的类都是我们的mapper,我们这里就可以自己定义如何识别,比如实现了某个markerInterface才算:

A ClassPathBeanDefinitionScanner that registers Mappers by basePackage, annotationClass, or markerInterface.

简单来说,对于一个简单的mapper接口:

image-20250111130024871

在扫描成bean definition后,定义如下:

bean class为工厂bean类型,要获取具体的bean,还需要调用getObject方法来生产。

Copy
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>{ }

这个bean中有几个主要的属性:

1、mapper class:

Copy
private Class<T> mapperInterface;

这个属性就是对应的业务的mapper类,如我这里的com.xxx.platform.mapper.EntityBusinessDetailInfoMapper

2、SqlSessionTemplate

由于该类型继承了SqlSessionDaoSupport,而SqlSessionDaoSupport中有如下定义:

Copy
public abstract class SqlSessionDaoSupport extends DaoSupport { private SqlSessionTemplate sqlSessionTemplate;

这个SqlSessionTemplate是什么呢,其实里面封装了SqlSessionFactory:

Copy
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory); }
Copy
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:

Copy
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory

image-20250111131826573

这里还标红了一处,这就是后续要说的mybatis拦截器:

Copy
import org.apache.ibatis.plugin.Interceptor; private final Interceptor[] interceptors; if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); }

在完成上述的SqlSessionFactory创建后,被注入到MapperFactoryBean中:

image-20250111135451418

image-20250111135532729

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

image-20250111135747700

image-20250111135644460

MapperFactoryBean.getObject#

image-20250111140047364

Copy
public SqlSession getSqlSession() { return this.sqlSessionTemplate; }

这里其实就是调用:

image-20250111141447278

这里的getConfiguration,也是调用底层mybatis的sqlSessionFactory的configuration:

Copy
public Configuration getConfiguration() { return this.sqlSessionFactory.getConfiguration(); }

而在下述调用getMapper时:

Copy
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的情况下,是会执行如下方法:

Copy
org.apache.ibatis.session.Configuration#getMapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }

由于我这边集成的是mybatis-plus,实际执行了如下方法:

Copy
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(继承了原来的),道理还是相通的:

image-20250111142838054

我们继续看上面的方法:

Copy
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

image-20250111143441493

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

image-20250111143606672

所以,如下代码也就是从上述的map中,根据mapper的class类型,获取到一个MybatisMapperProxyFactory对象。

Copy
mybatisMapperRegistry.getMapper(type, sqlSession)

这个MybatisMapperProxyFactory也是从mybatis中扩展来的:

image-20250111143736011

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

image-20250111143842128

newInstance如下:

Copy
public T newInstance(SqlSession sqlSession) { final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }

上述方法,先是new了一个MybatisMapperProxy对象,传入了sqlSession、mapperInterface等。

image-20250111144052653

接下来,如下2处代码,会生成一个mapper接口的jdk动态代理,代理的invocationHandler就是创建的MybatisMapperProxy对象:

Copy
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类型的属性:

image-20250111145404252

mapper执行过程#

sqlSessionProxy#

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

image-20250111150008165

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

image-20250111150738022

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

image-20250111150909453

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

image-20250111151119859

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

如下:

image-20250111151655621

sessionFactory.openSession#

由于我们是第一次调用,此时没有会话存储在线程局部变量中,因此需要新建一个session。

此时,就调用到了mybatis这一层。

image-20250111152117113

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

image-20250111152412046

上述看到,会先创建一个Executor,再创建一个DefaultSqlSession。

这个executor类型有三种:

Copy
public enum ExecutorType { SIMPLE, REUSE, BATCH }

这几种具体类型,我也并没有深入了解,可以看出,BATCH是批量操作相关的,应该是提高性能。

在创建完成后,会尝试调用org.apache.ibatis.plugin.InterceptorChain#pluginAll,试图对Executor进行jdk动态代理,代理后,调用方法时,都会先进入拦截器链,在拦截器链中执行完成后,才会继续原有的方法执行:

image-20250111152846981

image-20250111153109458

此处我们先不深入拦截器链的创建。

session执行sql#

创建statementHandler#

获取完成session后,会继续如下处理,进行方法调用:

image-20250111153607052

image-20250111153701786

image-20250111153838158

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

image-20250111153917357

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

image-20250111154922966

接下来,会执行到如下代码:

Copy
@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#

Copy
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处,如下,判断是预编译语句还是普通语句或者是存储过程:

image-20250111155721614

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

image-20250111155833386

创建parameterHandler

Copy
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

Copy
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#

image-20250111160541264

image-20250111160603385

statement执行#

image-20250111160706371

至此,执行过程基本结束了。

拦截器链作用的部分#

在上述源码过程中,有4处,拦截器链对这些生成的对象进行了代理,代理后,这些对象的方法在执行时,就会先进入拦截器。

image-20250111161027970

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

image-20250111161120295

image-20250111161151910

image-20250111161222263

image-20250111161241733

总结#

mybatis和spring的代码,结合得还是很紧密,有时候会弄混其中的边界,今天也算是简单理了下。

druid、HikariCP这些,算是底层,是datasource一层,mybatis要依赖这一层;

mybatis对外:SqlSessionFactory、SqlSession;

spring呢,使用sqlSessionTemplate(mybatis-spring-xxx.jar)去封装了mybatis的上述两个概念,主要是综合考虑了spring的事务。

mybatis-plus呢,替换了mybatis原本的SqlSessionFactory(其他方面的还没太研究),另一方面,继续封装,上层只需要使用mybatis-plus即可。




posted @   三国梦回  阅读(325)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2024-01-11 Feign源码解析4:调用过程
2021-01-11 一个7年程序员的2020之旅(从国企到鹅厂)
点击右上角即可分享
微信分享提示
CONTENTS