禅与计算机程序设计艺术Java 核心技术编程语言爱好者

spring-mybatis:为什么我只写一个接口就能被注入了

2022-11-01  本文已影响0人  pq217

前言

mybatis是我们常用的持久层框架,包括mybaits-plus,在使用它们的过程中一直有一个困惑:我只写了个接口(当然也写了xml),都没有实现类,为什么就可以在spring中被注入了,而且可以直接使用它进行数据库的增删查改

我们都知道一个接口是不能被初始化的,那么注入的到底是什么类的什么对象,这个对象是如何创建的,又是如何被加入bean容器的

这种感受在我接触了spring-data时更加强烈,比如spring-data-jpa和spring-data-elasticsearch,这些东西甚至连xml都不用写了,直接写接口就可以用

今天就以mybatis为突破口,彻底看明白是如何实现的

猜想

我习惯在看源码前先去自己思考一下,如果我来实现这个功能,该如何去做,所以先猜想一下这个牛逼功能的实现思路

感觉考虑到这三点疑惑基本就解除了,至于这个对象如何操作的数据库:有了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的方法)打一个断点走一下

doCreateBean

往下继续走


doCreateBean2

走到这一步就发现instanceWrapper就已经有值了,是从factoryBeanInstanceCache中获取到的,它是一个bean的包装器,基本证明了使用了FactoryBean猜想

而instanceWrapper有值就会短路后续的createBeanInstance(实例化bean)操作,因为bean已经被实例化过了并存储在factoryBeanInstanceCache缓存中

而仔细一看instanceWrapper包装的对象是MapperFactoryBean,说明被实例化过的是FactoryBean本身而不是BookRepository的对象

这符合FactoryBean的创建逻辑:doCreateBean返回的是FactoryBean,而上一层doGetBean会调用FactoryBean的getObject方法实际获取生产的bean

doGetBean

那么FactoryBean的实例对象是什么时候创建出来的,为什么会在factoryBeanInstanceCache缓存中?这是因为spring只要遇到getBeanByType的获取bean方式就会实例化所有的FactoryBean,因为只有这样才能通过FactoryBean.getObjectType方法判断生产的type是不是查询的type,当然如果没有提前被创建依然会通过createBeanInstance方法创建出FactoryBean的对象

到此,证明了mybaits使用了FactoryBean的猜想,对应的FactoryBean实现:MapperFactoryBean

注册所有mapper的bean定义

接下来证实这个FactoryBean的bean定义是如何被加入BeanFactory的

这个问题简单,打开@MapperScan注解的定义就一目了然了

@MapperScan

再看看ImportBeanDefinitionRegistrar的实现,它是如何注册bean定义的

ImportBeanDefinitionRegistrar
讲真,刚看到这里有点蒙,我以为这里的逻辑是扫描包下所有的mapper,依次注入创建目标为MapperFactoryBean的bean定义,结果这里的代码只注册了MapperScannerConfigurer一个bean定义

那么就继续看看MapperScannerConfigurer是个什么东西

MapperScannerConfigurer
原来是这样,MapperScannerConfigurer是一个bean定义注册后置处理器BeanDefinitionRegistryPostProcessor

所以mybaits是通过@Import+ImportBeanDefinitionRegistrar向bean容器加入了一个bean定义注册型后置处理器(MapperScannerConfigurer),然后实际给每个mapper加入到bean容器是通过MapperScannerConfigurer来实现的

那就继续来看一下MapperScannerConfigurer的postProcessBeanDefinitionRegistry方法实现

MapperScannerConfigurer
看到scanner就感觉找对地了,在spring源码中scanner就代表扫描器,扫描到所有带有@Component的类然后注册bean定义,相比mybaits也是这个套路

前面代码都是初始化扫描器,看看scanner.scan方法做了什么,scan最终会走向doScan

ClassPathMapperScanner.doScan
可以看到调用了父类的doScan方法,而ClassPathMapperScanner的父类就是ClassPathBeanDefinitionScanner,是spring的一个类,他可以实现扫描包,并把包下的类/接口注册为bean定义
ClassPathBeanDefinitionScanner.doScan
也就是mybaits借助了spring的类来实现扫描,这一步挺巧的,毕竟spring实现了,直接用就可以,负责扫描包下的类转换为bean定义还是一个很繁琐的工作

但是问题也来了,通过上一章分析,mybaits给每个mapper注册的bean定义是一个FactoryBean,spring扫描并注册bean定义可不会加什么FactoryBean,调试registerBeanDefinition(definitionHolder, this.registry)这一步如下

ClassPathBeanDefinitionScanner.registerBeanDefinition
可以看到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()方法

MapperFactoryBean.getObject
继续跟会走到MapperRegistry.getMapper
MapperRegistry.getMapper
这时已经看到动态代理的字样了,继续进入mapperProxyFactory.newInstance
MapperProxyFactory.newInstance
证明了猜想,mybaits就是根据mapper的接口生成动态代理,代理对象的执行逻辑就不看了,无非就是根据方法对应xml的sql去数据库执行并返回

总结

最后总结一下:

上一篇下一篇

猜你喜欢

热点阅读