首页 > 数据库 >利用mybatis拦截器记录sql,辅助我们建立索引(一)

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

时间:2025-01-11 17:15:59浏览次数:1  
标签:mapper 拦截器 SqlSessionTemplate spring bean sql mybatis public

背景

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

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

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注解:

image-20250111124507024

这里解析了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:

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

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

image-20250111131826573

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

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

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

这里其实就是调用:

image-20250111141447278

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

image-20250111142838054

我们继续看上面的方法:

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

mybatisMapperRegistry.getMapper(type, sqlSession)

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

image-20250111143736011

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

image-20250111143842128

newInstance如下:

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

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

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

image-20250111152412046

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

这个executor类型有三种:

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

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

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

image-20250111155721614

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

image-20250111155833386

创建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

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

标签:mapper,拦截器,SqlSessionTemplate,spring,bean,sql,mybatis,public
From: https://www.cnblogs.com/grey-wolf/p/18665853

相关文章

  • MyBatis-plus
    MyBatis-Plus(上篇)简介:MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生特性:无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑损耗小:启动即会自动注入基本CURD,性能基本无损耗,直接面向对象操作强......
  • MySQL如何对用户资源进行限制
    MySQL提供了对每个用户的资源限制管理MAX_QUERIES_PER_HOUR: 一个用户在一个小时内可以执行查询的次数(基本包含所有语句)MAX_UPDATES_PER_HOUR:一个用户在一个小时内可以执行修改的次数(仅包含修改数据库或表的语句)MAX_CONNECTIONS_PER_HOUR:允许用户每小时连接的次数MAX_U......
  • MySQL主要的SQL_Mode值详解
    ANSI更改语法和行为,使其更符合标准SQL。STRICT_TRANS_TABLESTRADITIONAL使MySQL的行为象“传统”SQL数据库系统。该模式的简单描述是当在列中插入不正确的值时“给出错误而不是警告”等同STRICT_TRANS_TABLES、STRICT_ALL_TABLES、NO_ZERO_IN_DATE、NO_ZERO_DATE......
  • SQL Server性能优化(3)使用SQL Server Profiler查询性能瓶颈
    关于SQLServerProfiler的使用,网上已经有很多教程,比如这一篇文章:SQLServerProfiler:使用方法和指标说明。微软官方文档:https://msdn.microsoft.com/zh-cn/library/ms179428(v=sql.105).aspx有更详细的介绍。经过使用Profiler进行监视,得到监视结果。=========================......
  • SQL Server性能优化(2)获取基本信息
    以下常用的SQL语句有利于我们分析数据库的基本信息,然后根据查询的结果进行优化。1.查看索引碎片   无论何时对基础数据执行插入、更新或删除操作,SQLServer数据库引擎都会自动维护索引。随着时间的推移,这些修改可能会导致索引中的信息分散在数据库中(含有碎片)。当索引包含......
  • MySQL账号被锁定
    #创建一个用户mysql>createuserkeme@'localhost'identifiedby'123456';#给一个只读权限mysql>grantselecton*.*tokeme@'localhost';#可以从本地登录[root@mysql-150~]#mysql-ukeme-p123456#把keme@'localhost'给lock......
  • HANA常用的功能性sql
    1、SQL去掉字符串左侧0replace(ltrim(replace(“WERKS”,‘0’,’‘)),’',‘0’);2、exists用法–exists判断引导的子句是否有结果集返回,有返回结果集条件成立,反之,不成立;–判断A表对应字段的数据在C表中是否存在EXISTS(SELECT1FROMTABLE01ASCWHEREA.SAP_DATE......
  • 升级MySQL
    本章介绍升级MySQL安装的步骤。升级是一个常见的过程,当您在同一MySQL版本系列中获取bug修复或主要MySQL版本之间的重要功能时。您首先在一些测试系统上执行此过程以确保一切顺利进行,然后在生产系统上执行此过程。在下面的讨论中,必须使用具有管理权限的MySQL帐户运行的MySQL命......
  • 免费送源码:Java+ springboot+MySQL springboot开放实验室管理系统 计算机毕业设计原创
    摘要随着社会的发展,社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。本文以实际运用为开发背景,运用软件工程原理和开发方法,它主要是使用动态网页开发技术java作为系统的开发语言,MySQL作为后台数据库。整个开发过程首先对开放实验......
  • (免费送源码)计算机毕业设计原创定制:Java+ssm+MySQL springboot家政服务平台管理系统
     摘  要在社会快速发展的影响下,家政迅速发展,大大增加了家政服务信息管理的数量、多样性、质量等等的要求,使家政的管理和运营比过去十年更加困难。依照这一现实为基础,设计一个快捷而又方便的家政服务平台管理系统是一项十分重要并且有价值的事情。对于传统的家政服务信息管......