背景

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

但是最终项目上线,作为技术责任人,线上出任何问题,我都有责任(不管是不是我的代码)。其中,慢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. 为什么在http协议中使用base64编码方式传输二进制文件

    相关: 图解 Base64 实现原理并使用 js 实现一个简单的 Base64 编码器 常用加密方法之Base64编解码及代码实现 一直都知道在http协议中使用base64的方式传递二进制文件,虽然 ...

  2. 一款WPF开发的B站视频下载开源项目

    更多开源项目请查看:一个专注推荐优秀.Net开源项目的榜单 今天给推荐一款C#开发的.界面简洁的哔哩哔哩视频下载工具. 项目简介 这是一款基于WPF开发的,B站下载工具,操作界面简洁,支持多线程下载. ...

  3. Java基础语法闪过——纯小白

    Java语法突击 笔者因为学校奇葩选课原因,需要学习Java,考试所迫和大伙一起交流复习下基础的语法内容,大家都一把拿下考试 观前提醒:本文整理的有些仓促了,简单几分钟看看Java有什么内容还好,如果 ...

  4. pnpm 是如何颠覆 npm 和 yarn 的?

    今天研究了一下 pnpm 的机制,发现它确实很强大,甚至可以说对 yarn 和 npm 形成了降维打击 . 我们从包管理工具的发展历史,一起看下到底好在哪里? npm2 在 npm 3.0 版本之前, ...

  5. 关于xml文件解析时'&'不能被解析的问题

    Bug情况:在解析xml文件的时候,&字符解析错误 解决方式:将符号进行转义

  6. 【分块】LibreOJ 6281 数列分块入门5

    前言 对一个 int 类型的非负整数进行开方下取整,最多只会开方四次大小就不会再发生变化.一个大于 \(0\) 的正整数开方下取整最后的结果比如是 \(1\),而 \(1\) 开方的结果仍然会是 \( ...

  7. IOS多线程之NSOperation(3)

    IOS多线程之NSOperation(3) 操作优先级和服务质量 可以通过QueuePriority属性来设置operation在队列中的执行优先级 public enum QueuePriority ...

  8. Dart代码混淆

    Dart代码混淆 代码混淆是修改应用程序的二进制文件以使其更难被人类理解的过程.混淆会在编译后的 Dart 代码中隐藏函数和类名称,将每个符号替换为另一个符号. Flutter 的代码混淆仅适用于re ...

  9. ajax请求与前后端交互的数据编码格式

    目录 一.Ajax AJAX简介 应用场景 AJAX的优点 语法实现 二.数据编码格式(Content-Type) 写在前面 form表单 几种数据编码格式介绍 三.ajax携带文件数据 四.ajax ...

  10. KTL 用C++14写公式的K线工具 - 0.9.3版

    K,K线,Candle蜡烛图. T,技术分析,工具平台 L,公式Language语言使用c++14,Lite小巧简易. 项目仓库:https://github.com/bbqz007/KTL 国内仓库 ...