背景

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

但是最终项目上线,作为技术责任人,线上出任何问题,我都有责任(不管是不是我的代码)。其中,慢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定义如下:

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

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

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

MapperScannerConfigurer

package org.mybatis.spring.mapper;

public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor

这个类的介绍是:

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方法来生产。

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中有如下定义:

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSessionTemplate sqlSessionTemplate;

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

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

com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory

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

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

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

这里其实就是调用:

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

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

而在下述调用getMapper时:

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

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

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

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

我们继续看上面的方法:

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对象。

mybatisMapperRegistry.getMapper(type, sqlSession)

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

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

newInstance如下:

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对象:

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这一层。

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

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

这个executor类型有三种:

public enum ExecutorType {
SIMPLE, REUSE, BATCH
}

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

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

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

session执行sql

创建statementHandler

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

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

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

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

@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

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

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

  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即可。

利用mybatis拦截器记录sql,辅助我们建立索引(一)的更多相关文章

  1. Mybatis拦截器实现SQL性能监控

    Mybatis拦截器只能拦截四类对象,分别为:Executor.ParameterHandler.StatementHandler.ResultSetHandler,而SQL数据库的操作都是从Exec ...

  2. mybatis拦截器获取sql

    mybatis获取sql代码 package com.icourt.alpha.log.interceptor; import org.apache.ibatis.executor.Executor; ...

  3. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  4. Mybatis拦截器,修改Date类型数据。设置毫秒为0

    1:背景 Mysql自动将datetime类型的毫秒数四舍五入,比如代码中传入的Date类型的数据值为  2021.03.31 23:59:59.700     到数据库   2021.04.01 0 ...

  5. mybatis 之定义拦截器 控制台SQL的打印

    类型 先说明Mybatis中可以被拦截的类型具体有以下四种: 1.Executor:拦截执行器的方法.2.ParameterHandler:拦截参数的处理.3.ResultHandler:拦截结果集的 ...

  6. SpringMVC利用拦截器防止SQL注入

    引言 随着互联网的发展,人们在享受互联网带来的便捷的服务的时候,也面临着个人的隐私泄漏的问题.小到一个拥有用户系统的小型论坛,大到各个大型的银行机构,互联网安全问题都显得格外重要.而这些网站的背后,则 ...

  7. Mybatis拦截器介绍

    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...

  8. Mybatis拦截器介绍及分页插件

    1.1    目录 1.1 目录 1.2 前言 1.3 Interceptor接口 1.4 注册拦截器 1.5 Mybatis可拦截的方法 1.6 利用拦截器进行分页 1.2     前言 拦截器的一 ...

  9. Mybatis拦截器实现分页

    本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...

  10. 数据权限管理中心 - 基于mybatis拦截器实现

    数据权限管理中心 由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手 需求场景 第一种场景:行级数据处理 原sql: select ...

随机推荐

  1. Mysql数据库个人整理笔记

    数据类型 tinyint/smallint/int/bigint float double char/varchar date/time/datetime/timestamp DDL 数据库 crea ...

  2. 基于Java+SpringBoot+Mysql实现的古诗词平台功能设计与实现三

    一.前言介绍: 1.1 项目摘要 随着信息技术的迅猛发展和数字化时代的到来,传统文化与现代科技的融合已成为一种趋势.古诗词作为中华民族的文化瑰宝,具有深厚的历史底蕴和独特的艺术魅力.然而,在现代社会中 ...

  3. 微软憋大招:SQL Server + Copilot = 地表最强AI数据库!

    微软憋大招:SQL Server + Copilot = 地表最强AI数据库! 微软布局代码AI霸主地位 微软在人工智能领域的布局引人注目,尤其在代码生成领域,微软通过Copilot展现出了强大的竞争 ...

  4. SpringBoot进阶教程(八十三)Kaptcha

    Kaptcha是谷歌开源的一个可高度配置的比较老旧的实用验证码生成工具.它可以实现:(1)验证码的字体/大小颜色:(2)验证码内容的范围(数字,字母,中文汉字):(3)验证码图片的大小,边框,边框粗细 ...

  5. delphi 图形图像处理 Image32

    delpher 越来越少了,但不能掩盖它的优秀,很外前看到了 Image32,但发现用它的人很少,这段时间整理了它的资料,重新组合了一个DEMO,也可以说是个小工具,分享出来. Image32 关于I ...

  6. ARC127E Priority Queue

    ARC127E Priority Queue 分析性质+dp. 思路 由于每次加入的数肯定是一个 \(a\) 的排列,但这个角度不好考虑. 设 \(\{a\}\) 为最终状态的集合,其中 \(a_i& ...

  7. paramiko模块的使用

    简介: Paramiko是基于Python(2.7,3.4+)版本实现和封装了SSHv2协议,底层是用cryptography实现,我们如果希望远程登录主机或者远程下载或者上传文件到远程主机都可以使用 ...

  8. Java虚拟机类加载机制浅谈

    Java语言是一种编译后再经过解释器执行的过程, 解释器主要就是如何处理解释Class文件的二进制字节流.JVM主要包含三大核心部分:运行时数据区,类加载器和执行引擎. 虚拟机将描述类的数据从Clas ...

  9. uni-app 蓝牙扫码适配

    1.前言 蓝牙设备扫码的效率要高于手机摄像头 App需要进行对蓝牙扫码枪进行适配才能正常使用蓝牙设备枪,并兼容之前的摄像头扫码 适配的关键在于:扫码枪进行扫码时,需要对其进行事件监听,并拿到条码的值 ...

  10. Sealos AI Proxy 发布!一个平台调用所有大模型,再也不用到处找 API 了

    你是一位开发者,你需要调用各类 AI 模型,每次调用模型,都要在不同的平台间反复横跳,你大概会遇到以下问题: 获取 API Key 流程繁琐:需访问多个厂商的官网,查阅各自的使用文档,并按照规定的步骤 ...