MyBatis Mapper在Spring中的扫描和接口代理

我们使用SpringBoot+Mybatis构建Web应用时时,往往只需要在Application启动类上标注一个@MapperScan注解,Spring就会自动在@MapperScan定义的basePackage中扫描Mapper接口并将对应的bean注册到ApplicationContext中,本着知其然知其所以然的原则,下面我们就通过源码聊一下MyBatis Mapper的扫描和代理原理。

研究思路

Mybatis对于我们还是一个黑箱,我们知道的信息就只有@MapperScan注解还有Spring暴露的Mapper,一开始我们可以尝试debug Mapper的任意方法,但发现堆栈信息毫无用处。接着我们可以尝试在@MapperScan的核心属性上打一个断点(在接口方法上打断点代码运行会比较慢),我们首先要排除SpringBoot对@MapperScan属性的引用,因为MyBatis是可以通过Mybatis-Spring集成的。跳过SpringBoot的调用栈,我们可以看到较为关键堆栈,一般来说从最上面的方法开始往下找比较容易找到我们想要的信息,而且要有意识的跳过常用类的方法调用。因为我们的目的在于分析mybatis包于Spring的集成,我们需要重点搜索org.mybatis前缀的方法调用(我们研究一些Spring集成时,利用包的前缀能过滤大多数的方法调用栈)。
在这里插入图片描述
根据上面的原则,我们可以轻松找到org.mybatis.spring.annotaion.MapperScannerRegistrar,接下来就是在MapperScannerRegistrar打上断点,单步调试就ok了。

结果

MyBatis关键类汇总:

  • MapperScannerRegistrar
  • MapperScannerConfigurer
  • ClassPathMapperScanner

MapperScannerRegistrar实现ImportBeanDefinitionRegistrarResourceLoaderAware两个接口。这里的核心接口是ImportBeanDefinitionRegistrar,文档描述道开发者可以在处理@Configuration注解的配置类时额外添加的BeanDefinition。换句话说,我们可以通过实现这个接口,实现bean的动态添加。这个接口有两个方法:

  • registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry,BeanNameGenerator)
  • registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)

AnnotationMetadata:被@Import注解的类的注解集合;
BeanDefinitionRegistry:BeanDefinition的注册表;
BeanNameGenerator:用于被导入bean的命名策略;

了解了这个接口的方法,我们回到MyBatis的实现MapperScannerRegistrar

void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {

    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
    builder.addPropertyValue...
    ...
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
  }

从上面精简的代码可以看到,MyBatis注册了一个MapperScannerConfigurer的配置类,并且添加了一系列的属性值,需要注意的是addPropertyValue方法并不会在创建bean时注入属性值,需要自行应用到实例。

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,BeanDefinitionRegistryPostProcessor文档描述道该接口用于在常规的BeanDefinition(比如@Configuration、@Component、@Bean等注解定义的Bean)被注册完成后,在实例化这些bean之前添加或者修改BeanDefinition。

MyBatis的MapperScannerConfigurer实现了接口方法postProcessBeanDefinitionRegistry

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    ...
    scanner.registerFilters();
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

先前在MapperScannerRegistrar中,我们添加了很多PropertyValue到BeanDefinition中,processPropertyPlaceHolders方法就是将这些PropertyValue应用到MapperScannerConfigurer实例中。

在上面代码中,postProcessBeanDefinitionRegistry实例化了一个ClassPathMapperScanner,该类继承了ClassPathBeanDefinitionScannerClassPathBeanDefinitionScanner是用于Spring扫描包内的bean,该类定义了良好结构,我们可以通过重写该类的一些方法筛选需要的bean。在筛选bean过程中有两个方法起着重要作用。

  • isCandidateComponent(MetadataReader)
  • isCandidateComponent(AnnotatedBeanDefinition)

    这两个方法对包下的类进行筛选,isCandidateComponent(MetadataReader)通过注册的TypeFilter进行过滤;isCandidateComponent(AnnotatedBeanDefinition)一般需要重写,默认的的实现是选择满足下面三个条件之一的类:

  • isIndependent() && isConcrete()
  • isIndependent() && isAbstract() && hasAnnotatedMethods(Lookup.class.getName())

这两个条件都有一个前提就是候选bean的类型必须是为独立的类,而独立的类只能这两种情况之一:静态内部类或者普通外部非抽象类。bean类型其实可以为抽象类,但是必须存在@Lookup注解的方法,该类的所有抽象方法会被Spring重写。

在MyBatis中,其扫描bean过滤bean的条件通常是isInterface() && isIndependent(),所以我们定义的Mapper都为接口,但这不是根本原因,根源还是MyBatis使用的是JDK动态代理,这种代理模式只支持接口代理。

MyBatis使用ClassPathMapperScanner扫描bean,扫描完成可以得到一个候选BeanDefinition的集合,也就是Mapper的集合,MyBatis将集合中的BeanDefinition的BeanClass(Spring会根据该类型来实例化bean)修改为实现了FactoryBeanMapperFactoryBeanFactoryBean是Spring创建Bean的工厂方法,当一个BeanDefiniton的Class为FactoryBean的实例时,Spring会调用其getObject方法获取bean并将其注册到ApplicatinContext

MapperFactoryBean定义了bean实例化的逻辑,代码如下:

public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
}

通过追踪getMapper方法发现,其最后实例的创建是通过MapperProxyFactory创建的:

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

MapperProxy是最终的代理逻辑–Mapper接口的方法调用最终被代理映射为MapperMethod的调用。MapperMethod是MyBatis根据注解或者xml文件解析的方法,这个解析过程跟本文主题无关就不多赘述了。

我的GitHub代码中对MyBatis的代理过程进行了简单的模仿,可以参考:https://github.com/delin10/FinalSample/tree/master/mybatis-plus-sample/src/main/java/nil/ed/sample/mybatisplus/proxy

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子的木木

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值