惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
GbyAI
GbyAI
T
Tailwind CSS Blog
Know Your Adversary
Know Your Adversary
小众软件
小众软件
Security Latest
Security Latest
博客园 - 聂微东
P
Privacy International News Feed
L
Lohrmann on Cybersecurity
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
阮一峰的网络日志
阮一峰的网络日志
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 叶小钗
Last Week in AI
Last Week in AI
The Register - Security
The Register - Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Vulnerabilities – Threatpost
Jina AI
Jina AI
AWS News Blog
AWS News Blog
L
LINUX DO - 热门话题
D
Darknet – Hacking Tools, Hacker News & Cyber Security
NISL@THU
NISL@THU
Spread Privacy
Spread Privacy
Google DeepMind News
Google DeepMind News
P
Palo Alto Networks Blog
S
Schneier on Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
The Exploit Database - CXSecurity.com
月光博客
月光博客
C
CERT Recently Published Vulnerability Notes
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
量子位
K
Kaspersky official blog
T
Threatpost
Latest news
Latest news
The Cloudflare Blog
MyScale Blog
MyScale Blog
C
Cisco Blogs
T
Tor Project blog
S
Securelist
Cisco Talos Blog
Cisco Talos Blog
The GitHub Blog
The GitHub Blog
A
Arctic Wolf
T
Threat Research - Cisco Blogs
H
Help Net Security
C
CXSECURITY Database RSS Feed - CXSecurity.com

博客园 - 三国梦回

spring boot 项目中oracle datasource设置schema spring cloud项目中,在bootstrap.yml中指定了active的profile,结果不生效 线上服务重启后,从nacos取不到配置了,怎么回事 nginx location没学好,把自己坑了一把 技术问题记录20260125 最近遇到的两个技术问题记录 linux服务器文件上传失败 线上遇到的redis和数据库数据未同步问题、redisson内部实现问题 复杂业务系统线上问题排查过程 nacos中配了一个数字,springboot取回来怎么变了 一个java空指针异常的解决过程 简单记录下最近2个月完成的线上系统迁移工作 centos停服,迁移centos7.3系统到新搭建的openEuler 端口telnet不通排查过程 https证书中的subject alternative name字段作用及如何生成含该字段的证书 linux中如何判断一个rpm是手动安装还是通过yum安装的 对接服务升级后仅支持tls1.2,jdk1.7默认使用tls1.0,导致调用失败 网络抓包文件太大,如何切分 分页查询不加排序有问题,加了排序怎么还有问题 利用mybatis拦截器记录sql,辅助我们建立索引(二) sql server版本太老,java客户端连接失败问题定位
利用mybatis拦截器记录sql,辅助我们建立索引(一)
三国梦回 · 2025-01-11 · via 博客园 - 三国梦回

背景

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

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