MybatisPlus Mapper层接口实现——从Mapper扫描到Sql语句的执行

2,674 阅读8分钟

0.前言

这段时间自己练习了一个springboot项目云E办,项目架构如下图所示,集成MybatisPlus后,项目不需要进行mapper层接口实现,如下图第一个方框中的mapper包是mapper接口,第二个mapper包是xml文件。对于MP底层如何实现很感兴趣,通过多方查询及debug测试,稍稍弄懂了一些,现将自己的学习过程记录一下,欢迎各位大佬指正。

image.png

1.mapper接口的扫描

在本项目的启动类中,存在 @MapperScan注解,从此处发,开始进行讲解。@MapperScan注解上存在注解 @Import(MapperScannerRegistrar.class)。此处,@Import存在三种导入方式,分别是:

  1. 带有 @Configuration注解的配置类(4.2 版本之前只可以导入配置类,4.2版本之后 也可以导入 普通类)
  2. 实现ImportSelector
  3. 实现ImportBeanDefinitionRegistrar

这里属于第三种,即在MapperScannnerRegistrar中实现ImportBeanDefinitionRegistrar接口,从而注册MapperScannnerConfigurer

image.png

MapperScannerConfigurer类中,有一个方法postProcessBeanDefinitionRegistry(),在这个方法中,new一个ClassPathMapperScanner类的对象(父类是ClassPathBeanDefinitionScanner ),通过该对象实现对mapper包的扫描。该过程首先调用父类中的scan()方法,再调用自己类中的doScan()方法,在子类中的doScan()中又super.doScan()...(debug给我看懵了,还能这么跳的?)

-------------------------------------ClassPathMapperScanner------------------------------------------
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        ...
        processBeanDefinitions(beanDefinitions);
        ...
        return beanDefinitions;

其中,在子类doScan()方法中,调用processBeanDefinitions()方法,该方法生成mapper接口的工厂beanMapperFactoryBean,这样每次获取mapper 实例实际是通过MapperFactoryBean的实例去获取(具体的mapper实例又涉及到动态代理)。而在源码中,存在如下注解,证明mapper接口的实例是通过MapperFactoryBean得到。

-----------------------------ClassPathMapperScanner------------------------------------
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        ...
        definition.setBeanClass(this.mapperFactoryBeanClass);
        ....

// the mapper interface is the original class of the bean

// but, the actual class of the bean is MapperFactoryBean

2.mapper接口的调用

在实现mapper接口的扫描后,我们现在手中拥有mapper接口的工厂,当serviceImpl类中进行 @Autowired时,我们需要进行“加工生产”,即MapperFactoryBean.getObject()

-------------------------MapperFactoryBean-------------------------
    public T getObject() throws Exception {
        return this.getSqlSession().getMapper(this.mapperInterface);
    }
    
------------------------SqlSessionDaoSupport--------------------------
    public SqlSession getSqlSession() {
        return this.sqlSessionTemplate;
    }

------------------------SqlSessionTemplate--------------------------
    public <T> T getMapper(Class<T> type) {
        return getConfiguration().getMapper(type, this);
    }

对了,此处有一个小问题,在其他的一些博客中,我看到博主并不使用类.方法名,而是类#方法名进行表述,希望有大神在评论区解释一下。

由源码可知,getObject()方法进行一系列调用最终来到SqlSessionTemplate类中的getMapper()方法。

SqlSessionTemplate是Mybatis为了接入Spring提供的Bean,

SqlSessionManager是Mybatis不接入Spring时用于管理SqlSession的Bean

两者均是SqlSession的实现类

接着,调用两个方法分别是getConfiguration()得到Configuration(在MP中是MybatisConfiguration) 在getConfiguration方法中,用到一个属性sqlSessionFactory,该属性的获取涉及到另一篇博文,其实,建议在此处停止阅读,前往另一篇博文将其中涉及到的类、属性、方法弄明白,再继续阅读,会更加清楚、容易。MybatisConfiguration.getMapper()方法调用重要属性mybatisMapperRegistrygetMapper()方法。(记住它,就是它开启了动态代理!!!)

------------------------SqlSessionTemplate---------------------------
    public Configuration getConfiguration() {
        return this.sqlSessionFactory.getConfiguration();
    }
-----------------------MybatisConfiguration--------------------------    
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mybatisMapperRegistry.getMapper(type, sqlSession);
    }

3.MybatisPlus中的动态代理

终于,终于,我们来到了动态代理部分,我一直没弄明白动态代理和AOP的关系,所以也不敢贸然写“MP中的AOP”。对于此部分,简单绘制动态代理的关系图,便于理解。 image.png 进入MybatisMapperRegistry,找到对应的getMapper()方法,此处,全部CV过来,一步步分析。

  1. 第一句从knownMappers属性中获取mapper工厂类,并进行强转为MybatisMapperProxyFactory
  2. 接着,通过该工厂类newInstance,获取泛型T(即我们所需要的接口实例)。

进入MybatisMapperProxyFactory中,再进行newInstance()分析。通过源码可以看到,这里运用Prox类获取泛型T的动态代理对象。对newProxyInstance()中三个参数进行简要分析:

  1. mapperInsterface.getClassLoader() :被代理接口的类加载器,即泛型T的类加载器。
  2. new Class[]{mapperInterface} : 被代理接口。
  3. mapperProxy :InvocationHandler的实现类,重写了invoke方法(手动加星,重点中的重点)。 进入MyBatisMapperProxy,找到invoke()方法,如源码所示,变量method可以通俗理解为mapper接口调用的方法,如selectOne、selectList等等。通过一定量代码让该方法执行起来,从而完成数据库查询工作。

动态代理这一章节对mapper接口的代理实现进行简要叙述,至于一定量代码如何实现method,在第四章进行介绍,再次强调一下,建议对MybatisPlusAutoConfiguration有一定的了解再进行第四章的阅读!

------------------------------------------MybatisMapperRegistry---------------------------------------------
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
        final MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
    

-----------------------------------------MybatisMapperProxyFactory-----------------------------------------

    public T newInstance(SqlSession sqlSession) {
        final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
    protected T newInstance(MybatisMapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }
    
-----------------------------------------MybatisMapperProxy------------------------------------------------
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ...
        final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }

4.MybatisMapperProxy的invoke方法

未完待续...(2021.12.20更新)


接一下来的一章主要讲解MP与数据库的交互,也就是selectOne究竟是怎么查询一个的

首先,还是回顾一下我们现在手中拥有的主要资源,1.mapper接口的代理对象;2.SqlSessionTemplate对象;3.MybatisConfiguration和4.MybatisMapperRegistry。接着,简要分析它们的关系,“4”在进行工厂bean的工厂bean(名词解释点击这里)建立时通过对mapperXML文件进行解析得到,并存储在“3”中。通过将“3”传入MybatisSqlSessionFactoryBuilder().build()方法建立SqlSessionFactory,即SqlSession的工厂bean,从而生产“2”。根据上文对MybatisMapperRegistry的源码分析可知,在MybatisMapperRegistry中有一个hashmap存储着类型为MybatisMapperProxyFactory的工厂bean,在进行 @Autowired时,通过工厂bean生产“1”。

当MP与数据库发生交互时,意味着程序开始调用mapper接口的方法。其实,也可以通过debug进行阶段区分,上述所讲内容如果进行断点调试会影响项目的启动,停滞在以下界面。而进入invoke方法进行断点调试,意味着我们来到方法实现阶段,单纯的项目启动不会调用mapper接口的方法,因此项目可以顺利来到swagger的API调试页面。

image.png

讲了这么多,接下来让我们进入invoke方法,开始从数据库获取数据吧。通过invoke进入mapperMethod.execute()方法(这里作简单说明:由于项目配置springsecurity,在进行mapper方法调用时,会进行权限过滤,查询当前用户的权限信息,因此以下示例以AdminMapper的selectOne方法进行讲解)。 command对象的Name属性是selectOne的命名空间+sql语句id,通过源码也可以看出来mapper方法的调用通过SqlSession(此处是SqlSessionTemplate)进行实现。进入SqlSessionTemplate后,发现其selectOne方法是通过代理对象进行实现,而进一步执行代码时,来到SqlSessionTemplate中实现的一个invoke方法,不过不要紧张,这里仅仅是对method方法(这里的method已经更换为SqlSession下的selectOne)的一个实现,没有增强功能,也不存在其它复杂的逻辑(留个坑吧,感觉没这么简单)。继续debug调试,果然来到了DefaultSqlSession(我就说怎么SqlSessionTemplate怎么代码这么少,原来还是通过Default实现)在Default中几番跳跃,来到selectList方法(其实selectOne都是通过selectList进行查询,然后增加两个if判断,将0个查询结果和多个查询结果的情况进行特殊处理),其实到达这里就差不多了,源码中,这个query函数代表对数据库的进一步查询操作,内部的封装太过复杂(哎,要不也不会这么流行了~),仅展示最熟悉的jdbc操作ps.execute()。ps中的sql语句如下图所示。

----------------------------------------MybatisMapperMethod-------------------------------------
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        ...
        result = sqlSession.selectOne(command.getName(), param);
        ...
    }
 --------------------------------------SqlSessionTemplate--------------------------------    
    public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.selectOne(statement, parameter);
    } 
    
    private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
        ...
        
 -------------------------------------DefaultSqlSession----------------------------------        
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      ...
      
 -----------------------------------PreparedStatementHandler----------------------------------      
      public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.handleResultSets(ps);
  }
    

image.png

再顺便回顾一下通过jdbc进行数据库查询的操作过程:

  1. 建立数据库连接
  2. 预编译sql语句,返回PreparedStatement的实例
  3. 填充占位符
  4. 执行sql语句
  5. 资源关闭

总结

兜兜转转,终于来到结尾,这几天对源码的跟踪很让人抓狂,循环往复,无限套娃,恐怖如斯。不过,通过学习也可以发现,MP中的源码主要是对方法的封装太多,导致debug跳来跳去,而且各个类之间的属性相互包含,初学者很难提纲挈领,抓住首尾。这里,最后再将我对MP运行顺序进行分析,抛砖引玉,希望得到大佬的指点。

首先,分为三个部分,第一个部分是mapperXML文件的解析与存储;第二个部分是mapper接口代理对象的实现;第三个部分是mapper方法的实现。

未完待续...