Mybatis与Spring结合深探——MapperFactoryBean的奥秘

2023-12-15 00:15

本文主要是介绍Mybatis与Spring结合深探——MapperFactoryBean的奥秘,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • MapperFactoryBean的工作原理
    • 底层实现剖析
      • MapperFactoryBean的checkDaoConfig()方法
        • 总结
      • MapperFactoryBean的getObject()方法
    • 思考联想
    • 后续

系列相关相关文章
究竟FactoryBean是什么?深入理解Spring的工厂神器
超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?
Mybatis与Spring结合深探——MapperFactoryBean的奥秘
后续TODO:MapperScannerConfigurer

前言

在这里插入图片描述

在没有Spring单独使用Mybatis的时候,我在之前的文章超硬核解析Mybatis动态代理原理!只有接口没实现也能跑? 讲解到了调用链路new SqlSessionFactoryBuilder().build(xml)-->XMLConfigBuilder#parse-->>XMLConfigBuilder#parseConfiguration--->XMLConfigBuilder#mapperElement-->XMLMapperBuilder#mapperParser.parse()-->XMLMapperBuilder#configurationElement-->XMLMapperBuilder#bindMapperForNamespace-->Configuration#MapperRegistry#addMappper()

在SqlSessionFactoryBuilder().build方法 中最终调用Configuration对象的addMappper()方法(实际上是委托给MapperRegistry的addMapper)添加对应的MapperProxyFactory代理工厂类,最终通过这个工厂类生成对应的代理对象MapperProxy 。

也就是MapperRegistry内部维护一个映射关系,每个接口对应一个MapperProxyFactory(生成动态代理工厂类)

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

这样便于在后面调用MapperRegistry的getMapper()时,直接从Map中获取某个接口对应的动态代理工厂类,然后再利用工厂类针对其接口生成真正的动态代理类。


如果想了解什么是FactoryBean是什么,可以查看前文究竟FactoryBean是什么?深入理解Spring的工厂神器

更详细的内容可以查看我之前的文章:超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?


而我们现在在Spring框架中整合Mybatis时,我们通常会使用MapperFactoryBean来生成Mapper的代理实例,也就是不需要再通过new SqlSessionFactoryBuilder().build(xml)的方式去注册动态代理接口。这是一种更简单且易于配置的方式,可让我们以Spring的形式操作Mybatis的持久层。本文将深入探索MapperFactoryBean的工作原理,并说明如何将Mybatis和Spring框架结合起来,以构建一个响应迅速而又易于维护的数据访问层。

MapperFactoryBean的工作原理

当应用启动时,Spring容器会为每个MapperFactoryBean生成一个相应的Bean实例。这个过程包含了几个关键步骤:

  • Bean的定义:在Spring配置文件中定义MapperFactoryBean,这包括指定其sqlSessionFactorysqlSessionTemplate
  • Bean的实例化:Spring容器将调用MapperFactoryBeangetObject()方法,这个方法内部又会调用Mybatis的SqlSession.getMapper()
  • 生成Mapper代理:正如前面提到的,Mybatis使用动态代理技术生成代理对象。这个过程由Mybatis内部的MapperProxyFactory完成。
  • Bean的使用:最终创建的Mapper被注入到其他组件中,这样,业务代码就可以通过普通的Java方法调用来执行SQL操作了。

下面我们看看实际的配置代码示例:

<!-- Mybatis的SqlSessionFactory配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dataSource" /><property name="mapperLocations" value="classpath*:mapper/*.xml" />
</bean><!-- Mapper接口对应的FactoryBean配置 -->
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"><property name="mapperInterface" value="com.example.mapper.UserMapper" /><property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

当然,在Spring的Java配置中,我们通常用注解来代替上述XML配置,得益于Spring的@MapperScan,可以大幅简化这个配置:

@Configuration
@MapperScan("com.example.mapper")
public class AppConfig {@Beanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();sessionFactory.setDataSource(dataSource);// ...其他配置...return sessionFactory.getObject();}
}

底层实现剖析

MapperFactoryBean的checkDaoConfig()方法

MapperFactoryBean本身extend自SqlSessionDaoSupport,SqlSessionDaoSupport又extend自DaoSupport接口,DaoSupport接口实现了InitializingBean接口,在对象初始化的时候会调用它的afterPropertiesSet方法,该方法中首先调用了checkDaoConfig()方法,MapperFactoryBean重载的checkDaoConfig()如下面所示—>这里最终会将调用configuration.addMapper(this.mapperInterface)(实际也是委托给MapperRegistry)

微信公众号:bugstack虫洞栈 & MapperFactoryBean类图

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {private Class<T> mapperInterface;public void setMapperInterface(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}protected void checkDaoConfig() {super.checkDaoConfig();Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");Configuration configuration = this.getSqlSession().getConfiguration();if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {try {configuration.addMapper(this.mapperInterface);} catch (Exception var6) {this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);throw new IllegalArgumentException(var6);} finally {ErrorContext.instance().reset();}}}@Overridepublic T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}// ...
}

checkDaoConfig方法中,会检查mapperInterface是否已设置,符合Spring管理Bean生命周期的要求。

接着通过configuration.addMapper(this.mapperInterface)方法重点关注,最终实现是在MapperRegistry中:
到这里以后,跟我之前文章超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?解析的步骤又是一样的了

new SqlSessionFactoryBuilder().build(xml)-->XMLConfigBuilder#parse-->>XMLConfigBuilder#parseConfiguration--->XMLConfigBuilder#mapperElement-->XMLMapperBuilder#mapperParser.parse()-->XMLMapperBuilder#configurationElement-->XMLMapperBuilder#bindMapperForNamespace-->Configuration#MapperRegistry#addMappper()

  public <T> void addMapper(Class<T> type) {if (type.isInterface()) {if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {knownMappers.put(type, new MapperProxyFactory<T>(type));// It's important that the type is added before the parser is run// otherwise the binding may automatically be attempted by the// mapper parser. If the type is already known, it won't try.MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}}

parser.parse()完成了对mapper对应xml的解析成MappedStatement,并添加到了configuration对象中,这里的configuration也就是我们上面提到的new Configuration()创建的那个对象(非常重要)。

总结

mapper接口的定义在bean加载阶段会被替换成MapperFactoryBean类型,在spring容器初始化的时候会给我们生成MapperFactoryBean类型的对象,在该对象生成的过程中调用afterPropertiesSet()方法,为我们生成了一个MapperProxyFactory类型的对象存放于Configuration里的MapperRegistry对象中,同时解析了mapper接口对应的xml文件,把每一个方法解析成一个MappedStatement对象,存放于Configuration里mappedStatements这个Map集合中。

MapperFactoryBean的getObject()方法

MapperFactoryBean实现了FactoryBean接口,实现了FactoryBean接口的类型在调用getBean(beanName)既通过名称获取对象时,返回的对象不是本身类型的对象,而是通过实现接口中的getObject()方法返回的对象。

这里需要对spring的声明周期有一定的了解:下面是简化版的MapperFactoryBean的链路调用

getObject()--->doGetBean()--->getObjectForBeanInstance()--->getObjectFromFactoryBean()--->doGetObjectFromFactoryBean--->MapperFactoryBean.getObject()方法

protected <T> T doGetBean(final String name, final Object[] args) {Object sharedInstance = getSingleton(name);if (sharedInstance != null) {// 如果是 FactoryBean,则需要调用 FactoryBean#getObjectreturn (T) getObjectForBeanInstance(sharedInstance, name);}BeanDefinition beanDefinition = getBeanDefinition(name);//这里如果是MapperFactoryBean对象,初始化完成以后会进入下面的判断Object bean = createBean(name, beanDefinition, args);//这里如果是MapperFactoryBean对象,初始化完成以后会进入下面的判断return (T) getObjectForBeanInstance(bean, name);}private Object getObjectForBeanInstance(Object beanInstance, String beanName) {if (!(beanInstance instanceof FactoryBean)) {return beanInstance;}Object object = getCachedObjectForFactoryBean(beanName);if (object == null) {FactoryBean<?> factoryBean = (FactoryBean<?>) beanInstance;//这里如果是MapperFactoryBean对象,初始化完成以后会进入这里的逻辑object = getObjectFromFactoryBean(factoryBean, beanName);}return object;}

FactoryBeanRegistrySupport的方法getObjectFromFactoryBean--->doGetObjectFromFactoryBean()--->factory.getObject()方法

    protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName) {if (factory.isSingleton()) {Object object = this.factoryBeanObjectCache.get(beanName);if (object == null) {//这里如果是MapperFactoryBean对象,初始化完成以后会进入这里的逻辑object = doGetObjectFromFactoryBean(factory, beanName);this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));}return (object != NULL_OBJECT ? object : null);} else {return doGetObjectFromFactoryBean(factory, beanName);}}private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName){try {//最终调用MapperFactoryBean的getObject方法获取实际的对象return factory.getObject();} catch (Exception e) {throw new BeansException("FactoryBean threw exception on object[" + beanName + "] creation", e);}}

MapperFactoryBean的getObject()方法

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

走到这里,是不是也跟我之前文章超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?解析的步骤又是一样的了

getMapper方法的大致调用逻辑链是: SqlSession#getMapper() ——> Configuration#getMapper() ——> MapperRegistry#getMapper() ——> MapperProxyFactory#newInstance() ——> Proxy#newProxyInstance()–>MapperProxy#invoke–>MapperMethod#execute

思考联想

  • 在之前的文章中超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?我们通过new SqlSessionFactoryBuilder().build(xml)最终调用委托给Configuration#MapperRegistry#addMappper() 方法进行mapper接口的注册,而在Spring结合mybatis的过程中,我们通过MapperFactoryBean的checkDaoConfig()最终调用委托给Configuration#MapperRegistry#addMappper() 方法进行mapper接口的注册方法实现,本质上是一样的。

  • 在之前的文章中超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?,我们又通过SqlSessionFactory的openSession()新建一个SqlSession,然后通过session#getMapper()最终调用委托给MapperRegistry#getMapper()——> MapperProxyFactory#newInstance() 方法实现代理对象的生成,而在Spring结合mybatis的过程中,我们通过MapperFactoryBean的getObject()调用this.getSqlSession().getMapper(this.mapperInterface)最终也是委托给MapperRegistry#getMapper()——> MapperProxyFactory#newInstance() 方法实现代理对象的生成,本质也是一样的道理。

后续

刚刚上面的例子我们可以发现:每配置一个mapper,都需要写一个对应的MapperFactoryBean,如果mapper多了这样是很繁琐的。

<!-- Mapper接口对应的FactoryBean配置 -->
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"><property name="mapperInterface" value="com.example.mapper.UserMapper" /><property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

为了解决这个问题,我们可以使用MapperScannerConfigurer,让它扫描特定的包,自动帮我们成批的创建映射器。这样一来,就能大大减少配置的工作量。具体的实现原理我们后面再进行讲解。

<!-- 配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 --><bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><!-- 注入sqlSessionFactory --><property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/><!-- 给出需要扫描Dao接口包 --><property name="basePackage" value="com.joe.dao"/></bean>

这篇关于Mybatis与Spring结合深探——MapperFactoryBean的奥秘的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/494410

相关文章

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

java图像识别工具类(ImageRecognitionUtils)使用实例详解

《java图像识别工具类(ImageRecognitionUtils)使用实例详解》:本文主要介绍如何在Java中使用OpenCV进行图像识别,包括图像加载、预处理、分类、人脸检测和特征提取等步骤... 目录前言1. 图像识别的背景与作用2. 设计目标3. 项目依赖4. 设计与实现 ImageRecogni

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

Java访问修饰符public、private、protected及默认访问权限详解

《Java访问修饰符public、private、protected及默认访问权限详解》:本文主要介绍Java访问修饰符public、private、protected及默认访问权限的相关资料,每... 目录前言1. public 访问修饰符特点:示例:适用场景:2. private 访问修饰符特点:示例:

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.