1:场景分析
在我们使用SpringBoot+MyBatis的时候,我们一般是先引入依赖,然后配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.coco.pojo
当然还要在启动类上加上一个注解
这时候,就可以编写一个接口,然后调用这个方法就可以执行配置文件中对应的SQL语句了
那么底层原理到底是怎么实现的呢??
2:万事开头难
分析一个框架源码的时候最难的就是不知道该从哪开始,我是这样想的,既然我们只要写一个这样的接口,那么就可以调用对应的SQL语句,那么肯定是在哪个环节对这个接口做一些特殊的处理
我们在启动类上加了一个注解,而且注解中的包路径正是我们接口的路径,这时候我们就有点眉目了。
进入 @MapperScan(“com.coco.mapper”) 这个注解中
我们看到除了注解的基本三个注解之外,还有一个注解就是 @Import({MapperScannerRegistrar.class}),很多小伙伴可能不知道这个注解有什么用,我们先解释一下
3: SpringBoot中@Import注解的作用
在SpringBoot中当我们要声明一个Bean的时候,我们可以在该类上加上 @Service,@Compont等,或者是在配置类中加上 @Bean 这个注解,除此之外还有一种方法,就是 @Import
@Import注解中会标明一个类,而且在SpringBoot启动的时候会处理也就是会实例化这个Bean,也就是会对这个Bean做一些处理
4: MapperScannerRegistrar.class的作用
即然知道了 @Import注解的作用,那现在我们进入到这个类中看看,这个类实现了ImportBeanDefinitionRegistrar 这个接口
这个接口有什么用呢??简单的来说就是MyBatis通过这个入口可以让Spring扫描到某些Bean,并且这些Bean会被Spring所管理,也就是说这些Bean会被Spring进行初始化。
所以我们自定义的Mapper接口会被Spring扫描到,然后会被Spring进行加载
ImportBeanDefinitionRegistrar 这个接口就代表着当把Bean生成了对应的BeanDefinition的时候,就会调用这个接口的方法,我们看下这个接口中定义的方法
这个方法做什么的呢??
Spring在加载Bean的时候,首先会将Bean生成一个个的对应的BeanDefinition,后续就会通过这些一个个的BeanDefinition来进行初始化,也就是生成对应的Bean。
简而言之:Spring会通过MyBatis提供的 @MapperScan(“com.coco.mapper”) 这个注解会扫描我们自定义的Mapper接口,然后Spring就会为这些Mapper接口生成对应的BeanDefinition
5: debug模式进入源码
然后debug模式启动SpringBoot项目,当然前提是整合了MyBatis哈,这个方法我进行了截取,其实只需要关注下面这几行代码就行
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//获取到MapperScan注解
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 获取MapperScan注解中basePackages的属性值
for (String pkg : annoAttrs.getStringArray("basePackages")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
// 真正开始处理这个包路径下的接口,也就是我们的Mapper接口
scanner.doScan(StringUtils.toStringArray(basePackages));
}
这里就可以获取到我们自定义mapper接口的包的全路径了
6: 开始处理Mapper接口
我们进入到上面的 scanner.doScan(StringUtils.toStringArray(basePackages)); 这个方法
然后进入 Set beanDefinitions = super.doScan(basePackages); 发现好多代码,其实这个方法的返回值是一个 BeanDefinitionHolder 的集合,而 BeanDefinitionHolder 就是bean的名称和该bean的BeanDefinition的组成
其实到这里我们应该能明白,这个方法的作用就是:扫描我们自定义的Mapper接口,然后为每一个接口生成一个对应的BeanDefinition,然后将其返回
我们debug到这一步可以看到返回值,也证实了我们之前说的
7: 拿到BeanDefinition之后的处理
现在我们来看下 processBeanDefinitions(beanDefinitions); 这个方法,因为之前我们已经拿到了Mapper接口的BeanDefinition了,所以接下来就要进一步的处理
这个方法的代码依旧很多,我这里就不贴出来了,这里我先说一下这个方法是干什么的。
Spring在初始化Bean之前,我们是可以改变Bean的BeanDefinition的属性值的,而这个方法做的事情就是这个,经过这个方法处理之后,我们之前得到的BeanDefiniton会发生一些改变。我这里贴出二张图进行对比一下
这是之前的:
这是经过该方法处理之后的:
可以发现该Bean的 beanClass 属性变了,已经不在是我们自定义的Bean的class了
改变之后有什么问题呢??
Spring在初始化Bean的时候,会拿到该Bean的BenDefinition,然后就是根据 beanClass这个属性值初始化Bean,本来我们Mapper接口初始化之后应该就是我们自己定义的Bean,也就是我们执行a.getClass的值应该是 com.xxx.a 这种形式的
但是现在变了,也就是说我们自定义的Mapper接口在被Spring初始化之后,再执行a.getClass会变成org.mybatis.spring.mapper.MapperFactoryBean
8: 初始化Bean
经过上面的步骤之后,我们是拿到了Mapper接口的BeanDefinition,现在Spring就要开始初始化这些Bean了
因为此时就涉及到了Spring的源码了,我这里就不细说了
大致的流程:
1: Spring在初始化bean的时候,会根据Bean的scope属性进行初始化,而我们自定义的Mapper接口由于BeanDefinition的beanClass属性被修改了,所以在初始化的时候,经过一系列的判断最终会由MyBatis中的 MapperProxy 生成一个代理类,底层是通过jdk动态代理实现的
2: 然后当我们调用Mapper接口方法的时候就会执行invoke方法,因为是jdk动态代理生成的代理类。
3: 这时候,MyBatis是可以拿到该方法所在的类和该类的全路径的,比如我们在 com.coco.mapper 包下自定义了一个TestMapper接口,然后里面有一个 test() 方法,这时候我们可以通过一系列的方法得到一个值,该值就是: com.coco.mapper.TestMapper.test, 也就是该Mapper接口的全路径+方法名
4: MyBatis在解析xml配置文件的时候,有一个 namespace的属性,它的值就是Mapper接口的全路径名,然后加上 id 的值,MyBatis底层会将所有的这种路径全都保存在一个Map中,然后执行接口方法的时候就会根据第3步生成的值去匹配,就能拿到对应的SQL语句了
<?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.coco.mapper.TestMapper">
<select id="test">
select * from test
</select>
</mapper>