SpringBoot集成Mybatis源码剖析及Mapper工作原理

本文通过源码解析,揭示了SpringBoot如何简化Mybatis配置,重点介绍了Mapper接口的自动扫描、SqlSessionFactory和SqlSessionTemplate的构建,以及MapperProxy的工作机制。
摘要由CSDN通过智能技术生成

Cc同学记得以前开发项目主要是以SSM或是SSH为主。Spring由于其繁琐的配置,一度被人认为“配置地狱”,各种XML、Annotation配置,让人眼花缭乱,而且如果出错了也很难找出原因。Cc同学记得刚自学Spring的时候就怕配置出错,因为一旦出错就要各种搜索。而Spring Boot的问世,让开发者看到了曙光,SpringBoot让创建基于Spring的应用更加快捷简易。 大部分Spring Boot Application只要一些极简的配置,即可“一键运行”。
这篇文章主要通过源码剖析Mybatis在Spring Boot环境下是如何进行自动配置的以及Mapper具体的工作原理。

1.准备工作-搭建一个极简的测试项目

通过实际运行项目来一步一步解读源码。
新建Spring Boot项目,引入 mybatis-spring-boot-starter和数据库驱动依赖;

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

数据库新建测试表,项目内生成对应的实体及Mapper接口和对应的Mapper.xml。这里就不多赘述了。

2.找到入口–MybatisAutoConfiguration

Spring Boot启动是自动配置原理
根据Spring Boot的启动原理,我们只需要找到mybatis-spring-boot-autoconfigure下的META-INF/spring.factories文件,即可可以看到自动配置类指向了MybatisAutoConfiguration,话不多说直接跟进。
spring.factories文件
简单看一下MybatisAutoConfiguration里面的几个重要的点。省略了方法体。

//构建SqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}

//构建SqlSessionTemplate
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {}

//注入MapperScan
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {}

//扫描Mapper接口
@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {}

3.Mapper的前世今生

//通过@Import(AutoConfiguredMapperScannerRegistrar.class)来加载AutoConfiguredMapperScannerRegistrar
@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
//省略代码部分。。。。。。
}


//实现了ImportBeanDefinitionRegistrar,所以直接看registerBeanDefinitions方法内部
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
  private BeanFactory beanFactory;
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //需要扫描的包
    List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
    //省略代码部分。。。。。。
    //动态注入bean,将MapperScannerConfigurer注入进IOC容器
    registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
  }
  @Override
  public void setBeanFactory(BeanFactory beanFactory) {
    this.beanFactory = beanFactory;
  }
}

打开MapperScannerConfigurer,可以看到它实现了BeanDefinitionRegistryPostProcessor接口,根据Spring的机制,直接查看它的postProcessBeanDefinitionRegistry方法。
在这里插入图片描述

  //当该类被IOC容器加载时,会调用该方法
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }
	//创建扫描器
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    //省略代码部分。。。。。。
    //扫描所配置包下所有的带有@Mapper注解的接口
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

当我继续步入方法内部,实际上进入了ClassPathMapperScanner的doScan方法
在这里插入图片描述
然后调用processBeanDefinitions()方法,直接进入该方法内部

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    AbstractBeanDefinition definition;
    BeanDefinitionRegistry registry = getRegistry();
    //循环刚刚扫描的mapper集合
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (AbstractBeanDefinition) holder.getBeanDefinition();
      //这里是最重要的一步,我们刚刚扫描出来的mapper BeanDefinition实际上是mapper接口本身
      //这里将Class的类型改为了MapperFactoryBean.class
      definition.setBeanClass(this.mapperFactoryBeanClass);
    }
  }

到此一步Mapper的初步工作就完成了,将我们定义好的Mapper接口扫描并注入IOC容器,且将原本的Class类型修改为了MapperFactoryBean.class类型,为什么要将Mapper接口修改为MapperFactoryBean?接下来实际运行看一下为什么以及mapper的工作原理

4.Mapper的工作原理

将断点打到我们的Mapper方法上,直接运行测试项目
在这里插入图片描述
可以看到,我们Spring给我注入的是MapperProxy类型,上面我们说过Mybatis将Mapper接口类型改为MapperFactoryBean,而这是拿到的确实MapperProxy这是为啥呢?我们去看一下MapperFactoryBean的源码。
在这里插入图片描述
可以看到实现了FactoryBean接口,而FactoryBean注入的时候是通过getObject去获取对象。
在这里插入图片描述
最后实际调用链调用的是MapperProxyFactory中的newInstance方法,实际上是通过JDK动态代理生成了Mapper接口的代理类,这里是巧妙的地方,就是JDK动态代理接口是必须得,而实现类是可有可无的,Mybatis就是对用户定义的Mapper的接口生成代理类MapperProxy之后,实际的逻辑都是通过MapperProxy内部实现的,没有用到实现类。
在这里插入图片描述
继续刚才的步骤继续debug,步入代码内部
在这里插入图片描述
调用cachedInvoker()去缓存中获取MapperMethodInvoker,从中获取MapperMethod,这个也是MyBatis的一级缓存,在内存中存储使用过的SQL语句
在这里插入图片描述
从而执行MapperMethod的execute方法
在这里插入图片描述
MapperMethod是Mybatis非常核心的一个组件
首先看一下它的构造方法

  private final SqlCommand command;
  private final MethodSignature method;
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

SqlCommand是MapperMethod的一个静态内部类封装了SQL类型 insert update delete select
MethodSignature也是一个静态内部类封装了方法的参数信息 返回类型信息等

接下来看一下execute方法

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

继续debug我们的代码
在这里插入图片描述
可以看到command中的name是咱们的测试方法名 getTest,type是SELECT
继续下一步
在这里插入图片描述
我们讲一下这两行代码

	Object param = method.convertArgsToSqlCommandParam(args);
	result = sqlSession.selectOne(command.getName(), param);

在这里插入图片描述
可以看到args是我们传进来的参数数组
在这里插入图片描述
这里主要工作就是组织好参数对应的值及顺序
接下来就是执行SqlTemplate的方法
在这里插入图片描述
进入selectOne方法内部
在这里插入图片描述
这里的this.sqlSessionProxy实际上SqlSession的代理类,这个可以在SqlSessionTemplate的构造方法中可以找到答案
在这里插入图片描述
所以继续跟进代码,进入SqlSessionTemplate的内部类的SqlSessionInterceptor中也就没有疑问了吧
在这里插入图片描述
实际上调用了DefaultSqlSession的selectOne方法
在这里插入图片描述
进入DefaultSqlSession的selectOne方法内部
在这里插入图片描述
继续进入selectList方法
在这里插入图片描述
第一步首先获取MappedStatement对象,MappedStatement对象对应Mapper.xml配置文件中的一个select/update/insert/delete节点,描述的就是一条SQL语句的信息。
在这里插入图片描述
简单看一下它的内部属性,继续执行
![在这里插入图片描述](https://img-blog.csdnimg.cn/717796f866604f39a71fa6d8ca6e4dc1.png
Executor是Mybatis执行器的顶层接口,它定义了对数据库的操作,这里我们看到Executor用的是它其中的一个实现类CachingExecutor,它为 Executor 对象增加了二级缓存的相关功能,它封装了一个用于执行数据库操作的 Executor 对象,以及一个用于管理缓存的 TransactionalCacheManager 对象。
在这里插入图片描述
进入CachingExecutor的query方法
在这里插入图片描述
可以看到第一步是得到待执行的SQL语句信息,以及二级缓存需要的key值,进入query方法
在这里插入图片描述
首先判断是否有缓存,第一次执行肯定是没有缓存,继续跟进
在这里插入图片描述
这里实际上是进入了BaseExecutor的queryFromDatabase方法,这里有一部分的缓存处理
继而进入doQuery方法,实际调用的是SimpleExcutor的doQuery方法
在这里插入图片描述
这里获取了StatementHandler,然后调用它的query方法
StatementHandler是四大组件中最重要的一个对象,负责操作 Statement 对象与数据库进行交流,在工作时还会使用 ParameterHandler 和 ResultSetHandler 对参数进行映射,对结果进行实体类的绑定。
在这里插入图片描述
调用了PreparedStatement的execute方法,到达这一步大家应该彻底懂了吧,Mybatis对JDBC的操作的进一步封装,最后一步就是对结果的映射,这里就不细讲了,小伙伴有兴趣可以自己去探索。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值