自动配置

对应 MybatisPlusAutoConfiguration 配置类

  1. 创建 SqlSessionFactory 类型的 bean,加载各类插件(sql注入、id生成等)

image.png

  1. 创建 SqlSessionTemplate 类型的 bean,实现 sqlSeesion 接口的 crud 方法,这部分接口每次调用都会使用 SqlSessionFactory 的 openSession() 方法创建 sqlSession,所以真正的方法体在 DefaultSqlSession 类中

image.png
image.png
image.png
image.png
image.png

  1. 创建默认的 AutoConfiguredMapperScannerRegistrar 扫描类,默认扫描启动类所属包路径下的带有 @Mapper 注解的文件作为 bean(如果配置了 @MappeScan 注解,则 @Mapper 注解失效)

image.png
image.png

Mapper 创建过程

在日常使用中,我们一般直接创建一个 mapper 接口,继承 BaseMappe 接口之后,便拥有了 curd 的功能,可以直接在 service 中依赖注入这个 bean 使用
image.png
image.png
但是默认情况下,spring 扫描注册 bean 的时候是会自动排除掉接口类型的,而这些 mapper 作为接口,且没有方法实现体,那么最终被注册到 spring 容器中的 bean 就应该是 mapper 的代理对象

mapper 扫描注册的两种方式

  1. 直接在 mapper 上添加 @Mapper 注解

image.png

  1. 在配置类上添加 @MapperScan 注解,指定 mapper 路径,当这里指定之后,其他路径下使用 @Mapper 注解将会失效

image.png

Mapper 注册过程

以 @MapperScan注解为例说明,当我们使用这个注解指定扫描路径之后,会将 MapperScannerRegistrar 注册为 bean(从而使得自动的扫描 @Mapper 注解失效),并调用对用方法
image.png
MapperScannerRegistrar 类会加载 @MapperScan 注解中指定的路径等信息
image.png
这里先通过 BeanDefinitionBuilder 创建了一个 MapperScannerConfigurer 类型的 bean
image.png
而 MapperScannerConfigurer 这个 bean 在创建之前会调用其 postProcessBeanDefinitionRegistry 方法,并自定义了一个扫描器,用于扫描 mapper 并进行处理(这部分可以参考:https://lxjblog.gitee.io/2023/08/24/spring%E6%BA%90%E7%A0%81/?t=1712563878288
image.png
上方调用了 ClassPathMapperScanner 父类的 scan() 方法,不过还是会调用自身的 doScan() 方法
image.png
并且 ClassPathMapperScanner 重写的判断条件,这里只需要扫描独立的接口类型(不是静态内部类)
image.png
先调用父类(也就是扫描器 ClassPathBeanDefinitionScanner)的 doScan() 方法,用于将指定路径的文件扫描为 bean
image.png
获取扫描到的结果,取其对应的 beanDefinition,因为这个时候扫描的都是接口本身,所以需要使用 setBeanClass() 方法指定其实现类,而 getConstructorArgumentValues() 用于指定实现类构造方法的参数
image.png
image.png
image.png
同时 MapperFactoryBean 继承了 DaoSupport,所以会调用 checkDaoConfig() 方法
image.png
所以在这里会将 mapper 的接口类型添加到 configuration 中
image.png
image.png
最后通过 MapperFactoryBean 的 getObject() 方法获取实例
image.png
而这个 mapper 会由 MybatisConfiguration 进行获取,也就是获取到上方添加的 mapper 内容
image.png
image.png
image.png
最终生成 mapper 的代理类对象
image.png

MybatisConfiguration 添加 mapper 的过程

这是由 mybatis-plus 创建的一个配置类,继承于 Configuration,但是添加了 mybatis-plus 的一些特性,只需要在自动配置类 MybatisPlusAutoConfiguration 中创建 SqlSessionFactory 的时候指定 configuration 为 MybatisConfiguration 类型即可
image.png
在添加 mapper 的时候,会先将代理类添加到 knownMappers 这个 map 中,避免重复添加,然后调用 parser.parse() 方法,用于生成对应的 sql 语句
image.png
遍历 mapper 的所有方法,并生成对应的 sql 语句,通过 parseStatement() 和 parserInjector() 方法将 sql 语句添加到 configuration 中

@Override
public void parse() { 
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) { 
        // 加载 xml 文件
        loadXmlResource();
        configuration.addLoadedResource(resource);
        String mapperName = type.getName();
        assistant.setCurrentNamespace(mapperName);
        // 是否配置二级缓存
        parseCache();
        parseCacheRef();
        InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);

        // 遍历当前 mapper 的所有方法
        for (Method method : type.ge    tMethods()) { 
            if (!canHaveStatement(method)) { 
                continue;
            } 
            // 解析方法上的注解内容
            if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
                && method.getAnnotation(ResultMap.class) == null) { 
                parseResultMap(method);
            } 
            try { 
                InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);

                // 解析生成 sql 语句,并添加到 configuration 中
                parseStatement(method);
            }  catch (IncompleteElementException e) { 
                configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
            } 
        } 

        // 获取 sql 注入器内容(mybatis-plus 中的 BaseMapper 对应的 crud 实现)
        try { 
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) { 

                // 解析生成 sql 语句,并添加到 configuration 中
                parserInjector();
            } 
        }  catch (IncompleteElementException e) { 
            configuration.addIncompleteMethod(new InjectorResolver(this));
        } 
    } 
    parsePendingMethods();
} 

parseStatement() 方法用于获取 mapper 方法上的注解等内容解析为 sql 语句,通过调用 assistant.addMappedStatement() 方法将 sql 添加到 mappedStatements 这个 map 中,以 类名+方法名 作为 key,sql 作为 value
image.png
image.png
而在 parserInjector() 方法中,则是获取 sqlInjector 注入器
image.png
image.png
获取 BaseMapper 中默认的方法进行注入
image.png
image.png
一次调用 Insert 等类的 inject 方法,生成 sql 语句并添加到 configuration 中
image.png
image.png
image.png

MybatisConfiguration 获取 mapper 的过程

image.png
创建通过代理对象创建实例
image.png
实际上就是创建了一个 MybatisMapperProxy** **类型的代理对象
image.png
所以每次调用 mapper 的方法的时候,就会经过 MybatisMapperProxy 代理对象的 invoke() 方法
image.png
方法调用的时候都会先从缓存中取结果,没有则查询数据库
image.png
而 MybatisMapperMethod 类的 invoke() 方法是调用 MybatisMapperMethod 类的 execute() 方法
image.png
这里就会去判断方法的操作类型
image.png
例如查询列表的时候就会调用 executeForMany() 方法
image.png
这时候就会使用 sqlSession 进行数据库操作
image.png
而当前容器中注册的是 SqlSessionTemplate 类型的,所以会调用 sqlSessionProxy 代理类的方法
image.png
image.png
image.png
而 getSqlSession() 方法获取到的是 DefaultSqlSession 类型的 sqlSession
image.png
image.png
image.png
所以在这里的 mapper 就可以获取到上面添加的 sql 表达式了
image.png
通过 getBoundSql() 方法就可以获取 sql 语句了
image.png

创建小结

1、Mapper 在注册过程中会先创建 MapperScannerConfigurer 这个 bean
image.png
2、这个 bean 自定义扫描器,也可以配置扫描规则,mybatis 配置只扫描独立的接口
image.png
3、ClassPathMapperScanner 将扫描出来的 GenericBeanDefinition 设置实现类为 MapperFactoryBean
image.png
4、所以每个扫描出来的 mapper 是从 MybatisConfiguration 类的 mybatisMapperRegistry 中获取的
image.png
image.png
image.png
5、所以实际的代理对象是这个 MybatisMapperProxy
image.png
6、所以每次调用 mapper 的方法的时候,都会执行 MybatisMapperMethod 的 execute 方法
image.png
image.png

Mybatis 缓存

Mybatis之一二级缓存和拦截器

一级缓存

注意:mybatis 中使用的是 sqlSession 代理对象进行查询操作,所以调用任何查询方法之前都会经历下方的代理,先获取 sqlSession,所以如果没开启事务的话,你的每次查询都是使用不同的 DefaultSqlSession 进行查询,所以不会有一级缓存
image.png
接下来是方法试探
1、直接查看 DefaultSqlSession 的 selectList 查询方法,缓存是在 executor 中执行的
image.png
image.png
2、在 MybatisCachingExecutor 中执行 query 操作,key 中包含如下信息
image.png
3、如果存在缓存,则直接返回结果,不存在则查询数据库
image.png
4、其中还会判断是否需要分页 pageOptional,返回分页对象
image.png
重点来啦
开启事务进行测试:
image.png
结果发现这个 cache 一直为 null,所以问题来啦,mybatis 的一级缓存不是在这里实现的,盲猜这里是实现二级缓存的,因为这个 cache 是存在在 MappedStatement 中的,应该所有线程都可以访问
image.png
但是日志中确实只查询了一次数据库
image.png

所以实际的缓存是在 BaseExecutor 中实现的
image.png
当 localCache 缓存中不存在时,则调用 queryFromDatabase 方法进行数据库查询并将结果存放到缓存中
image.png
并且当 MappedStatement 的 flushCacheRequired 为 true 时则清空缓存
image.png
image.png
image.png

二级缓存

Mybatis 的二级缓存是默认开启的(也可能是跟版本有关),同样也可以在 xml 文件中使用 标签进行定义,在所需要缓存的查询接口配置开启

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yihaomen.mybatis.dao.StudentMapper">
    <!--开启本mapper的namespace下的二级缓存-->
    <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>

    <!--可以通过设置useCache来规定这个sql是否开启缓存,ture是开启,false是关闭-->
    <select id="selectAllStudents" useCache="true">
        SELECT id, name, age FROM student
    </select>

</mapper>

不过主要还是讲 springboot 项目中的应用:
1、配置开启二级缓存(默认开启),这里也可以不配置,除非是要关闭二级缓存

mybatis-plus:
  configuration:
    cache-enabled: true

image.png
这个配置影响的是生成的 sqlsession 中对应的执行 executor 的类型,开启缓存后使用 MybatisCachingExecutor 执行器进行 sql 操作
image.png
2、在 Mapper 接口上添加 @CacheNamespace 注解,同时实体对象需要实现序列化接口,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中。序列化作用参考:知识点

@Mapper
@CacheNamespace
public interface StudentSMapper extends BaseMapper<Students> { 
} 
@Data
public class Students implements Serializable { 
    @TableId
    private Integer id;
    private String name;
    private Integer age;
    private LocalDateTime createTime;
} 

接下来是二级缓存生效过程:
1、查看创建 Mapper 的过程
image.png
image.png
直接看到 parseCache() 方法这里
image.png
2、如果当前的 Mapper 接口上添加 @CacheNamespace 注解,则会创建对应 mapper 接口的缓存,相当于这个缓存是针对这一整个 Mapper 接口的
image.png
3、useNewCache 方法中,会创建当前 mapper 的缓存,并应用注解上的参数,设置 currentCache 为这个缓存
image.png
4、而在后续的 addMappedStatement 方法中,在创建 mapper 每个方法的 statement 时,都会传入这个 cache
image.png
然后就是二级缓存的存取啦:
1、也就是在一级缓存中猜测的位置,在 MybatisCachingExecutor 类的 query 方法会先获取 MappedStatement 的 cache(在上面添加进去的)
image.png
2、接下来就会发现二级缓存中没有数据,需要进行数据库查询,并把结果 result 存到缓存中
image.png
3、但是实际的缓存是存放在 PerpetualCache(永久缓存) 类的 map 中
image.png
使用 computeIfAbsent 方法时,会把 key 作为参数参入 lambda 表达式中,所以 TransactionalCache 中的 delegate 是注解生成的 cache
image.png
image.png
4、虽然名字叫永久缓存,但我好像也看不出怎么是永久的~重启项目还是会清空缓存
image.png

二级缓存配置参数

1、查看 @CacheNamespace 注解的内容,原来 PerpetualCache 缓存是默认配置,缓存大小默认为 1024

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace { 

  /**
   * 要使用的缓存实现类型
   */
  Class<? extends Cache> implementation() default PerpetualCache.class;

  /**
   * 要使用的缓存逐出实现类型(相当于修饰器,在实现类之前调用)
   * LruCache 用于维持缓存数量
   */
  Class<? extends Cache> eviction() default LruCache.class;

  /**
   * 刷新间隔
   */
  long flushInterval() default 0;

  /**
   * 缓存大小
   */
  int size() default 1024;

  /**
   * 是否使用读/写缓存
   */
  boolean readWrite() default true;

  /**
   * 是否在请求时阻止缓存
   */
  boolean blocking() default false;

  /**
   * 属性值
   */
  Property[] properties() default { } ;

} 

2、这些配置在创建 cache 的时候有用到,所以我们可以指定任何 cache 实现类作为缓存实现类
image.png
3、查看 Decorator
image.png
调用 addDecorator 方法的时候会将 Cache 实现类添加到 builder 的 List 集合中
image.png
在调用 build 方法的时候就会遍历这个 decorators 集合,将当前 cache 作为参数构造新的 cache 实现对象(相当于在外面包了一层),最终返回的是最外层的 cache 对象
image.png
image.png
3、不过发现这里并没有提供基于 redis 的 Cache 实现类,不过我们也可以自己实现一个 redis 缓存,并配置到注解中就可以啦
image.png
4、查看默认配置中的 LruCache 缓存实现类,这个类中也创建了一个 map 用于存放 key,不同的是这个用的是 linkedHashMap,保存有序的数据,并重写了 removeEldestEntry() 方法,当容量超过设置的 size 时就会移除最旧的 key
image.png
image.png
所以每次添加缓存的时候,都会去检查 eldestKey 是否不为空,不为空则需要删除最旧的缓存数据啦
image.png

二级缓存清除方法

1、在创建每个 mapper 方法的 statement 的时候,都会传入同一个 cache 对象,以及是否刷新缓存(如果这个 statement 不是 select 类型,则它每次调用都会刷新缓存)
image.png
2、所以当调用 update 类型的 statement 方法的时候就会刷新缓存啦,这里刷新的是整个 mapper 接口的缓存,而不只是这个方法的
image.png
image.png
所以在同一个 mapper 中,只要调用了其中的一个 update 类型的方法,都会刷新掉整个 namespace 命名空间(接口)的缓存

缓存 key 的计算

1、通过 mybatis 的源码可以看出 key 的类型为 CacheKey
image.png
2、而创建 key 的过程如下,通过创建一个 CacheKey 类型的对象,并不断调用对应的 update 方法,往 key 中添加内容
image.png
image.png
3、计算 CacheKey 的 hashCode 值
image.png
4、重写 hashCode 方法,方便 hashMap 对该 key 进行存取
image.png
5、而在一级缓存中是使用 PerpetualCache 类型作为 Cache 的
image.png
6、缓存内容的存取都是通过其内部的 HashMap 来实现的,这个 Map 似乎也没有限制大小
image.png

Mybatis 拦截器

用法

1、实现 Interceptor 接口,添加 @Intercepts 注解 和 @Signature 注解指定拦截的执行器和方法,并添加 @Component 注册为 bean

@Intercepts(
    { 
        // 指定拦截的类和方法(参数)
         @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} )
    } 
)
@Component
public class MyInterceptor implements Interceptor { 
    @Override
    public Object intercept(Invocation invocation) throws Throwable { 
        // 指定拦截处理方法
        return null;
    } 
} 

例如 MybatisPlusInterceptor 拦截器,但是默认是没有注册为 bean 的,如果我们需要分页功能,那还得手动将他进行注册
image.png

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { 
    // 创建 Mybatis 拦截器
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 为拦截器添加插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
} 

生效过程

1、基于前面,我们知道每次使用 mybatis 进行 curd 操作的时候都会使用 factory 创建 sqlSession,同时使用 configuration 创建执行器 executor
image.png
2、在 MybatisConfiguration 创建 executor 的时候就会调用 interceptorChain 拦截器链进行装饰
image.png
3、遍历拦截器链中的拦截器进行装饰(代理)
image.png
image.png
4、相当于就是使用 interceptor 创建了执行器的代理对象,在调用 executor 方法的时候进行拦截,执行拦截器的方法
image.png
5、在 getSignatureMap 方法就会判断拦截器上是否具有 @Intercepts 注解,并获取其拦截的类和方法
image.png
image.png
6、因为 Plugin 本身也是拦截器,代理对象的拦截器也指定了是 Plugin,所以每次调用执行器的方法的时候都会经过 Plugin 拦截,然后再判断是否调用拦截器链中的方法
image.png

拦截器链

1、拦截器链内容的添加是通过调用 Configuration 的 addInterceptor 方法
image.png
2、而这个方法在 MybatisSqlSessionFactoryBean 的 buildSqlSessionFactory 方法中调用,用于构造 sqlSessionFactory 工厂,而这里的 plugins 就是我们配置的拦截器
image.png
3、在 SpringBoot 自动创建 SqlSessionFactory 的时候就会设置上 plugins,而其中的 interceptors 则是依赖注入进来的,所以我们只需要将自己的拦截器注册到 ioc 容器中就能生效了
image.png
image.png

MyBatisPlus 拦截器

1、MyBatisPlus 提供了一个 拦截器,我们只需要将他注册为 bean,并往这个拦截器中继续添加内部即可
image.png
2、配置之后 MyBatisPlus 拦截器会在查询和更新数据库之前调用内部拦截器的方法
image.png
3、分页插件用法如下,就是注册 myBatisPlus 拦截器并添加内部拦截器

/**
 * 添加MP插件到Spring容器中
 *
 * @return
 */
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { 
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 分页插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
} 

4、同样我们只需要创建 InnerInterceptor 内部拦截器的实现类也可以添加到拦截器中
image.png
5、而 MyBatisPlus 就是通过这个 PaginationInnerInterceptor 内部拦截器进行 selectCount 和 分页 操作

分页查询

1、MyBatisPlus 在进行分页查询之前会先调用内部拦截器的方法,判断内容是否为空
image.png
2、通过 PaginationInnerInterceptor 内部拦截器查询数量
image.png
3、根据 countId 获取对应的 statement
image.png
4、如果未设置 countId,则会根据查询内容构造数量查询语句
image.png
image.png
5、countId 的用法如下,只需要在分页对象 Page 中指定即可
image.png
image.png
6、如果查询结果数量为空则直接返回 false,不再继续进行数据库查询
image.png
image.png

面试题

MyBatis常见面试题汇总(超详细回答)_mybatis面试题-CSDN博客