spring-mybatis:为什么我只写一个接口就能被注入了
前言
mybatis是我们常用的持久层框架,包括mybaits-plus,在使用它们的过程中一直有一个困惑:我只写了个接口(当然也写了xml),都没有实现类,为什么就可以在spring中被注入了,而且可以直接使用它进行数据库的增删查改
我们都知道一个接口是不能被初始化的,那么注入的到底是什么类的什么对象,这个对象是如何创建的,又是如何被加入bean容器的
这种感受在我接触了spring-data时更加强烈,比如spring-data-jpa和spring-data-elasticsearch,这些东西甚至连xml都不用写了,直接写接口就可以用
今天就以mybatis为突破口,彻底看明白是如何实现的
猜想
我习惯在看源码前先去自己思考一下,如果我来实现这个功能,该如何去做,所以先猜想一下这个牛逼功能的实现思路
-
首先一个接口肯定是不能被创建并调其用方法的,第三方框架也不可能写接口的实现类,所以猜测是根据接口来生成一个实现该接口的对象,这个jdk动态代理技术可以做到
-
这个对象加入到了spring容器,那么一定是注册了bean定义到bean工厂,由于我们的接口并没有类似
@Repository
,@Component
的注解,而只有mybaits的@Mapper
注解,猜想是mybaits自己去扫描的注解,并注册了bean定义,使用经典的@Import+ImportBeanDefinitionRegistrar方式即可实现 -
考虑到普通的bean定义都是指定了targetType,即创建的bean的class,后续通过反射创建bean,而我们的接口并没有实现类,所以注册的bean定义应该是一个更加灵活的FactoryBean定义,而FactoryBean的getObject的过程应该就是根据接口生成代理对象
感觉考虑到这三点疑惑基本就解除了,至于这个对象如何操作的数据库:有了sql和数据库连接,想必谁都可以操作数据库吧,只是写的好坏的问题了
有了思路,感觉这个也不算什么黑科技了,自己也能仿写, 接下来跟进源码证实一下猜想
测试项目
基于mybaits搭建一个简单的springboot测试项目,主要为了排除其他框架的干扰,给自己搭建一个纯洁的学习环境
依赖单纯一点,spring版本2.3.2.RELEASE
<dependencies>
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--mybatis-spring-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
配置文件application.yml,数据库提前建好
#配置数据库
spring:
datasource:
url: jdbc:mysql://dev.135.com:3306/mytest
username: root
password: root
#指定xml路径
mybatis:
mapper-locations: classpath:mapper/*.xml
创建一个book表,存储字段id,title,content,写一个实体映射
public class Book {
private Long id;
private String title;
private String content;
// 省略getter setter
}
创建仓库和xml,写一个简单的查询sql
@Mapper
public interface BookRepository {
List<Book> all();
}
<?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.pq.mybaits.delve.repository.BookRepository">
<select id="all" resultType="com.pq.mybaits.delve.entity.Book">
select * from book
</select>
</mapper>
写一个spring启动类
@SpringBootApplication
@MapperScan("com.pq.mybaits.delve.repository") // 指定mapper的包
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
BookRepository bookRepository = context.getBean(BookRepository.class);
test(bookRepository);
}
public static void test(BookRepository bookRepository) {
// 搜索
List<Book> books = bookRepository.all();
System.out.println(books);
}
}
在数据库book表存点值,运行代码,正常打印表中的数据列表,测试项目搭建完成!
源码
FactoryBean
通过调试,一点点找线索,首先既然可以get到BookRepository.class的bean,那么一定有一个bean的创建过程,给BeanFactory的doCreateBean
(spring创建bean的方法)打一个断点走一下
往下继续走
doCreateBean2
走到这一步就发现instanceWrapper就已经有值了,是从factoryBeanInstanceCache中获取到的,它是一个bean的包装器,基本证明了使用了FactoryBean猜想
而instanceWrapper有值就会短路后续的createBeanInstance
(实例化bean)操作,因为bean已经被实例化过了并存储在factoryBeanInstanceCache缓存中
而仔细一看instanceWrapper包装的对象是MapperFactoryBean
,说明被实例化过的是FactoryBean本身而不是BookRepository的对象
这符合FactoryBean的创建逻辑:doCreateBean返回的是FactoryBean,而上一层doGetBean会调用FactoryBean的getObject方法实际获取生产的bean
那么FactoryBean的实例对象是什么时候创建出来的,为什么会在factoryBeanInstanceCache缓存中?这是因为spring只要遇到getBeanByType的获取bean方式就会实例化所有的FactoryBean,因为只有这样才能通过FactoryBean.getObjectType
方法判断生产的type是不是查询的type,当然如果没有提前被创建依然会通过createBeanInstance方法创建出FactoryBean的对象
到此,证明了mybaits使用了FactoryBean的猜想,对应的FactoryBean实现:MapperFactoryBean
注册所有mapper的bean定义
接下来证实这个FactoryBean的bean定义是如何被加入BeanFactory的
这个问题简单,打开@MapperScan
注解的定义就一目了然了
再看看ImportBeanDefinitionRegistrar的实现,它是如何注册bean定义的
讲真,刚看到这里有点蒙,我以为这里的逻辑是扫描包下所有的mapper,依次注入创建目标为MapperFactoryBean的bean定义,结果这里的代码只注册了
MapperScannerConfigurer
一个bean定义
那么就继续看看MapperScannerConfigurer是个什么东西
原来是这样,MapperScannerConfigurer是一个bean定义注册后置处理器
BeanDefinitionRegistryPostProcessor
所以mybaits是通过@Import+ImportBeanDefinitionRegistrar向bean容器加入了一个bean定义注册型后置处理器(MapperScannerConfigurer),然后实际给每个mapper加入到bean容器是通过MapperScannerConfigurer来实现的
那就继续来看一下MapperScannerConfigurer的postProcessBeanDefinitionRegistry
方法实现
看到scanner就感觉找对地了,在spring源码中scanner就代表扫描器,扫描到所有带有@Component的类然后注册bean定义,相比mybaits也是这个套路
前面代码都是初始化扫描器,看看scanner.scan
方法做了什么,scan最终会走向doScan
可以看到调用了父类的doScan方法,而ClassPathMapperScanner的父类就是ClassPathBeanDefinitionScanner,是spring的一个类,他可以实现扫描包,并把包下的类/接口注册为bean定义
ClassPathBeanDefinitionScanner.doScan
也就是mybaits借助了spring的类来实现扫描,这一步挺巧的,毕竟spring实现了,直接用就可以,负责扫描包下的类转换为bean定义还是一个很繁琐的工作
但是问题也来了,通过上一章分析,mybaits给每个mapper注册的bean定义是一个FactoryBean,spring扫描并注册bean定义可不会加什么FactoryBean,调试registerBeanDefinition(definitionHolder, this.registry)
这一步如下
可以看到beanClass还是我们的接口:BookRepository.class ,这样肯定是不行的,所以mybaits才会调用完spring的doScan之后加入了自己的处理,即
processBeanDefinitions
ClassPathMapperScanner.doScan
看一下processBeanDefinitions的内部
processBeanDefinitions
这一步就关键了,通过
definition.setBeanClass(this.mapperFactoryBeanClass)
方法把bean定义的beanClass属性修改为MapperFactoryBean
,与上一章对接上了
动态代理
到此我们知道mybaits通过扫描@MapperScan指定的路径,把所有的mapper注册为bean定义,并且bean定义生产出的bean是MapperFactoryBean.getObject()
的结果,那么MapperFactoryBean.getObject()到底反回了一个什么对象呐?刚刚猜想是一个接口的动态代理,接下来证实一下
MapperFactoryBean.getObject()方法
继续跟会走到
MapperRegistry.getMapper
MapperRegistry.getMapper
这时已经看到动态代理的字样了,继续进入
mapperProxyFactory.newInstance
MapperProxyFactory.newInstance
证明了猜想,mybaits就是根据mapper的接口生成动态代理,代理对象的执行逻辑就不看了,无非就是根据方法对应xml的sql去数据库执行并返回
总结
最后总结一下:
- mybaits使用@MapperScan注解,通过内部的@Import+ImportBeanDefinitionRegistrar+BeanDefinitionRegistryPostProcessor方式向容器注入bean定义
- 注入的bean定义是通过spring的扫描器,扫描到@MapperScan的value路径下的所有mapper接口
- mybaits在注册了bean定义后,立即把所有mapper的bean定义的beanClass修改为MapperFactoryBean,使这些bean定义变成FactoryBean类型的bean定义
- 调用spring.getBean(xxxMapper.class)时,通过MapperFactoryBean的getObject方法获取该mapper接口的代理对象,代理对象执行每个方法的逻辑就是执行该方法对应xml的sql语句