从BeanFactory入手,讲透Spring整合Mybatis的底层原理


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


BeanFactory入手,逐步分析Mybatis-starter工作原理,讲透SpringBootMybaits整合的原理。


前言

在专栏Mybatis源码分析中笔者以学习Mybaits时接触的四行代码为例详细的分析Mybatis的内部工作原理,感兴趣的同学可前往专栏Mybatis源码分析进行查看。

入门Mybatis的四行代码

java 复制代码
  //<1> 加载配置文件
  InputStream is = Resources.getResourceAsStream("mybatis.xml");
  //<2> 创建sessionFactory对象
  sessionFactory = new SqlSessionFactoryBuilder().build(is);
  //<3> 获取sqlSession对象信息
  SqlSession session = factoy.openSqlSession()
  //<4> 构建映射器的代理对象
  UserMapper mapper = session.getMapper(UserMapper.class);
  // .....调用相关方法信息

而本次我们主要探究Mybatis-starter的工作原理,即探究分析清楚Spring整合Mybatis背后故事


橄榄FactoryBean

FactoryBean------Spring留给外部框架的扩展点

FactoryBeanSpring框架中的一种特殊Bean。具体来看,FactoryBean的特殊之处在于它可以向容器中注册两个Bean,一个是它本身,一个是FactoryBean.getObject()方法返回值所代表的Bean

更通俗一点来讲, FactoryBean允许开发人员可以定制化地创建和配置Bean实例,从而使得Bean的创建过程更加灵活和可扩展。

下面我们便通过一个简单的例子来快速了解FactoryBean的使用:

java 复制代码
public class CustomerFactoryBean implements FactoryBean<UserService> {
     @Override
    public UserService getObject() throws Exception {
        return new UserService();
    }

}

上述代码最终会在Spring的容器中生成了两个Bean

  1. 一个是类型为FactoryBeancustomerFactoryBean
  2. 另一个则是类型为UserServicebean其名称为customerFactoryBean

进一步,如果通过如下代码获取customerFactoryBean,你会发现其最终的返回值的类型为UserService而不是FactoyBean类型。

java 复制代码
public class MainApplication {

    public static void main(String[] args) {
        // 获取bean
        Object customerFactoryBean = applicationContext.getBean("customerFactoryBean");
        System.out.println(customerFactoryBean);
    }
}

这其实是Spring内部关于FactoryBean的一个优化处理,如果要获取到FactoryBean则需要在bean名称前加上一个&。例如,applicationContext.getBean("&customerFactoryBean");


知道了FactoryBean后,接下来我们将分析Mybatis-starter中是如何通过使用FactoryBean来实现Spring整合Mybatis的原理。

但在开始分析之前,先来回顾下使用Spring整合Mybatis时的相关配置。

Spring整合Mybaits时的相关配置

Spring整合Mybatis时通常需要在applicationContext.xml配置如下内容:

xml 复制代码
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydatabase"/>
    <property name="username" value="username"/>
    <property name="password" value="password"/>
</bean>

<!-- 配置MyBatis的SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <!-- 配置Mapper所需的sql信息 -->
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

<!-- 配置MyBatis的Mapper扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.example.dao"/>
</bean>

不难发现,如上的配置文件主要做了如下工作:

  • 指明数据源信息。
  • 配置SqlSessionFactoryBean。并使用SqlSessionFactory引用数据源,同时配置Mapper的相关配置文件。
  • 配置MapperScannerConfigurer。其主要用于自动扫描并注册Mapper接口,属性basePackage属性用于指定了Mapper接口所在的包。

不难发现在上述配置文件中,用到了两个重要的类信息即SqlSessionFactoryMapperScannerConfigurer。这两个类在后续实现SpringMybatis整合中扮演着重要的角色。


开始分析之前,你一定要明白Spring整合Mybatis的核心逻辑就是:将使用MyBatis时需要手动创建的对象全部注入到Spring容器,以交由容器管理。

SqlSessionFactory的构建

SqlSessionFactory相关代码

java 复制代码
public class SqlSessionFactoryBean implements
        FactoryBean<SqlSessionFactory>, InitializingBean, 
        ApplicationListener<ApplicationEvent> {
    // ...省略其他代码

    public SqlSessionFactory getObject() throws Exception {
        if (this.sqlSessionFactory == null) {
          afterPropertiesSet();
        }
        return this.sqlSessionFactory;
    }
}

经过之前对于FactoryBean的介绍,不难发现SqlSessionFactoryBeanSpring中用于配置MyBatisSqlSessionFactory的工厂类。

具体来看,其会向Spring容器中注册一个SqlSessionFactorybean对象。进一步,sqlSessionFactory会通过方法afterPropertiesSet()来完成初始化。 具体逻辑如下:

java 复制代码
public void afterPropertiesSet() throws Exception {
 // 省略其他逻辑
  this.sqlSessionFactory = buildSqlSessionFactory();
}

由于buildSqlSessionFactory()方法逻辑很长,此处就不带着大家逐行分析了。但你可以稍微思考下buildSqlSessionFactory()内部会完后那些逻辑呢?

在之前的Mybatis流程分析(二):配置处理,构建SqlSession工厂中我们就曾对new SqlSessionFactoryBuilder().build(is);背后的工作流程进行过详细的分析,其内部无非就会做两件事

  • 解析MyBatis配置文件,构建全局配置类Configuration对象;
  • 通过new关键字构建一个DefaultSqlSessionFactory对象,并且将Configuration进行传入。

而这其实恰好也是buildSqlSessionFactory()的关键逻辑。事实上,只要你在读buildSqlSessionFactory()方法时把握住如上两点,读懂一个小小的buildSqlSessionFactory()方法根本不在话下。

(Ps:在笔者看来,当你熟悉了Mybatis背后的工作原理后。像buildSqlSessionFactory()这些方法其实点进去看一眼就能明白其所作的逻辑,根本无需耗费太多精力去钻研其细节!我相信如果你有看过专栏之前文章的话,回头再看Mybatis源码,一定会倍感轻松~~)

至此,相信你已经明白了SqlSessionFactoryBean的作用了。其无非就是实解析配置文件并构建得到SqlSessionFactory,并最终将SqlSessionFactory放入Spring容器中。


扫描路下的类信息并构建Bean

如果我们不引入Spring,即原生使用Mybatis开发时,通常会首先构建SqlSessionFactory然后来获取SqlSeession会话,进而通过SqlSeession来构建Mapper的代理对象,从而完成对数据的操纵。

但当引入Spring后,其内部又将如何来完成SqlSeessionMapper的构建呢?其关键就在于MapperScannerConfigurer对象。接下来,我们便来分析MapperScannerConfigurer的具体细节。

java 复制代码
public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, 
    InitializingBean, 
    ApplicationContextAware, 
    BeanNameAware {

  // 省略内部其他相关方法
}

其实MapperScannerConfigurer内部有很多方法,当你看到 MapperScannerConfigurer中诸多的方法你可能会有点蒙圈,不知道该分析类中的那个方法。如果你有这个问题那么说明你对于Spring框架还是不熟悉,那不妨关注笔者的新专栏Spring打怪升级之路笔者正在从零分析Spring框架的相关知识点,并立志于构建一个smart-spring

回到文章,不难发现MapperScannerConfigurer会实现一个名为BeanDefinitionRegistryPostProcessor的接口。而BeanDefinitionRegistryPostProcessorSpring框架中的一个扩展点,其可以向容器中动态注册新的Bean

进一步,MapperScannerConfigurerpostProcessBeanDefinitionRegistry方法逻辑如下:

java 复制代码
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {

    // 省略其他无关逻辑
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
   
    // 委托给ClassPathBeanDefinitionScanner进行扫描得到BeanDefinition
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, 
            ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

其中,postProcessBeanDefinitionRegistry将对bean的扫描交给ClassPathMapperScanner,用以扫描指定包并得到BeanDefinition。这部分的调用逻辑具体如下所示:

这里你要注意,这里扫描对应basePackage所得到的并不是像容器中注入一个接口所对应的实例对象!此处向Spring容器中注入bean的类型为mapperFactoryBean

看到这里你可能会有点乱,我们来简单梳理一下。之前谈的SqlSessionFactoryBean是一个FactoryBean,其作用在于向容器中注入一个SqlSessionFactory;之后,而后讨论的MapperScannerConfigurer其主要作用就是为了扫描basepackage下我们所定义的接口信息,进而封装成一个bean放入容器。

而后出现的MapperFactoryBean则是MapperScannerConfigurer扫描路径下所有类型后得到bean所对应类型。换言之,我们Dao层所定义的接口,最终放入容器后bean所对应的类型为MapperFactoryBean。 如下这张图准确的反映了三者之间的关系。


窥探MapperFactoryBean的秘密

经过上述的讨论,相信你已经清楚了。每个Mapper接口在容器中的bean实际是一个MapperFactoryBean对象。通过名字不难发现,MapperFactoryBean实际上还是一个FactoryBean。既然是FactoryBean那么其一定会重写getObject() 方法。接下来,我们便看看MapperFactoryBean中的getObject() 会完成那些逻辑。

MapperFactoryBean

java 复制代码
public class MapperFactoryBean<T> extends SqlSessionDaoSupport    implements FactoryBean<T> {
    @Override
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }

    @Override
    public Class<T> getObjectType() {
        return this.mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        // 返回true是为了让Mapper接口是一个单例的
        return true;
    }
}

不难发现,MapperFactoryBean中的getObject() 方法的返回值为Mapper接口在容器中的bean

如果你问我为什么这么笃定,我的答案就是 getSqlSession(). getMapper(this.mapperInterface);!这行代码。

(注:有关Mybatis根据Mapper接口生成代理对象的逻辑可参考专栏Mybatis源码分析相关文章!)

这里还要强调一点,MapperFactoryBeangetSqlSession()所返回的SqlSession的实际类型为SqlSessionTemplate。进一步,SqlSessionTemplate中会持有一个sqlSessionProxy对象。而这个sqlSessionProxySqlSession的动态代理对象。

因此,每次调用到sqlSessionProxy的方法时,都会从Spring事务管理器中获取SqlSession,并最终调用从Spring事务管理器中获取到的SqlSession对象,进而调用其内部对应方法。

至于这里为什么要这么绕,笔者考虑可能出于如下几个原因:

具体原因如下:

  1. 事务管理SqlSessionTemplate在执行数据库操作时会自动参与Spring事务管理,确保数据库操作与Spring事务同步。这是非常重要的,因为在Spring应用中,你通常会使用Spring的事务管理功能来管理数据库事务。而SqlSessionTemplate会根据Spring事务的状态来创建和管理SqlSession对象。

  2. 线程安全性SqlSessionTemplate确保每个线程都拥有自己的SqlSession实例,从而避免多线程并发访问SqlSession时可能出现的问题。

总之,MapperFactoryBean内部使用SqlSessionTemplate来获取SqlSession,可以确保MyBatisSpring的集成更加稳定和易于管理。这使得你可以更容易地将MyBatisSpring集成,并充分利用Spring框架提供的事务管理和异常处理机制。

(Ps:这里可能比较绕,但你只需知道其底层是通过SqlSession会话获取对应Mapper接口代理对象即可)

总结

至此,其实我们也就分析清楚了SpringMybatis整合的背后的秘密,其本质无非就是FactoryBean的灵活应用,从而使得原先通过手动创建的逻辑交给容器来自动扫描。

写在最后

或许,分析源码的过程相对枯燥,在当下这个快节奏的时代下,这种行为似乎并不讨喜,甚至有些人觉得源码只是面试时会用到,平时工作熟练掌握crud,同时汇报时把ppt做好就可以了,何必耗费多余的精力去分析源码呢?这个问题其实很难去回答,但笔者的建议就是去做让自己开心的事就行了。

于笔者而言看源码、分析源码、写作、分享是让笔者感到开心的一件事,写作之初笔者曾过分纠结于文章阅读量和点赞量,为此也专门发过沸点来询问广大掘友的意见,得到的答案无非就是题材选择有问题。对于这个问题笔者认真思考过,也考虑过更换方向,但后来笔者后期还是会选择坚持,因为笔者更想通过文章的形式来回顾自己学编程一路而来的努力和付出,愿我们彼此的坚持最终能迎来明日的曙光,就这样吧!

再补充一点,笔者专栏的文章是成体系的,从头开始读效果更佳哦!当然觉得文章还可以的话,不妨点赞+收藏+关注,不错过之后的每一次更新。

相关推荐
2401_857439697 分钟前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧6668 分钟前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索10 分钟前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
芒果披萨15 分钟前
Filter和Listener
java·filter
qq_49244844620 分钟前
Java实现App自动化(Appium Demo)
java
阿华的代码王国28 分钟前
【SpringMVC】——Cookie和Session机制
java·后端·spring·cookie·session·会话
德育处主任Pro1 小时前
『Django』APIView基于类的用法
后端·python·django
找了一圈尾巴1 小时前
前后端交互通用排序策略
java·交互
哎呦没3 小时前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
2401_857600953 小时前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化