Mybatis之原理详解和Mapper接口怎么注入Spring容器

1 Mybatis原理

MyBatis是目前非常流行的ORM框架,它的功能很强大,然而其实现却比较简单、优雅。本文主要讲述MyBatis的架构设计思路,并且讨论MyBatis的几个核心部件,然后结合一个select查询实例,深入代码,来探究MyBatis的实现
Mybatis中文说明文档

1.1 不使用mybatis

1.1.1 原生态jdbc

package com.sxt;
import java.sql.*;
public class JdbcDemo {
    public static void main(String[] args) throws Exception {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //1、加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");
            //2、通过驱动管理类获取数据库链接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
            //3、定义sql语句 ?表示占位符
            String sql = "select * from tb_user where username = ?";
            //4、通过连接获取声明statement
            preparedStatement = connection.prepareStatement(sql);
            //5、设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
            preparedStatement.setString(1, "王五");
            //6、向数据库发出sql执行查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            //7、遍历查询结果集
            while (resultSet.next()) {
                System.out.println(resultSet.getString("id") + "" + resultSet.getString("username"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //8、释放资源
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

1.1.2 SpringBoot 3.2 JdbcClient

SpringBoot 3.2引入了新的 JdbcClient 用于数据库操作,JdbcClientJdbcTemplate进行了封装,采用了 fluent API 的风格,可以进行链式调用。
自此,spring自带的数据库操作有了4种方式:JdbcTemplate、JdbcClient、SpringDataJdbc、SpringDataJpa

implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

在Service中直接注入JdbcClient即可:

@Component
public class DbService {
    @Autowired
    private JdbcClient jdbcClient;
}
1.1.2.1 查询操作
public MyData findDataById(Long id) {
        return jdbcClient.sql("select * from my_data where id = ?")
                .params(id)
                .query(MyData.class)
                .single();
    }

按照自定义查询条件查数据:

public List<MyData> findDataByName(String name) {
        return jdbcClient.sql("select * from my_data where name = ?")
                .params(name)
                .query(MyData.class)
                .list();
    }

以上两种查询方式,查询条件中的变量使用的是占位符,JdbcClient也支持按照参数名进行查询:

public Integer insertDataWithNamedParam(MyData myData) {
        Integer rowsAffected = jdbcClient.sql("insert into my_data values(:id,:name) ")
                .param("id", myData.id())
                .param("name", myData.name())
                .update();
        return rowsAffected;
    }

当参数比较多时,可以将参数放到一个Map中,用Map进行查询:

public List<MyData> findDataByParamMap(Map<String, ?> paramMap) {
        return jdbcClient.sql("select * from my_data where name = :name")
                .params(paramMap)
                .query(MyData.class)
                .list();
    }

当查询返回的结果不能简单的映射到一个类时,可以编写RowMapper,适用于SQL语句比较复杂的场景:

public List<MyData> findDataWithRowMapper() {
        return jdbcClient.sql("select * from my_data")
                .query((rs, rowNum) -> new MyData(rs.getLong("id"), rs.getString("name")))
                .list();
    }

同时也支持查询记录数:

public Integer countByName(String name) {
        return jdbcClient.sql("select count(*) from my_data where name = ?")
                .params(name)
                .query(Integer.class)
                .single();
    }
1.1.2.2 插入数据

通过占位符参数插入数据:

public Integer insertDataWithParam(MyData myData) {
        Integer rowsAffected = jdbcClient.sql("insert into my_data values(?,?) ")
                .param(myData.id())
                .param(myData.name())
                .update();
        return rowsAffected;
    }

通过命名参数插入数据:

public Integer insertDataWithNamedParam(MyData myData) {
        Integer rowsAffected = jdbcClient.sql("insert into my_data values(:id,:name) ")
                .param("id", myData.id())
                .param("name", myData.name())
                .update();
        return rowsAffected;
    }

直接插入整个对象:

public Integer insertDataWithObject(MyData myData) {
        Integer rowsAffected = jdbcClient.sql("insert into my_data values(:id,:name) ")
                .paramSource(myData)
                .update();
        return rowsAffected;
    }

1.2 mybatis核心组件

Mybatis里面的核心对象还是比较多,如下

Mybatis核心对象解释
ConfigurationMyBatis所有的配置信息都维持在Configuration对象之中
SqlSessionFactorySqlSession工厂专门创建SqlSession
SqlSession作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
ExecutorMyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合
ParameterHandler负责对用户传递的参数转换成JDBC Statement 所需要的参数
ResultSetHandler负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
TypeHandler负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatementMappedStatement维护了一条mapper.xml文件里面 select 、update、delete、insert节点的封装
SqlSource负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql表示动态生成的SQL语句以及相应的参数信息

1.3 原理图

mybatis应用程序通过SqlSessionFactoryBuildermybatis-config.xml配置文件中构建出SqlSessionFactory,然后,SqlSessionFactory的实例直接开启一个SqlSession,再通过SqlSession实例获得Mapper对象并运行Mapper映射的SQL语句,完成对数据库的CRUD和事务提交,之后关闭SqlSession。如下图所示:
大致流程原理图:
在这里插入图片描述
Mybatis执行原理
在这里插入图片描述

其中的类关系图示:
在这里插入图片描述

1.4 原理讲解

在这里插入图片描述

1.4.1 配置文件解析

配置文件解析过程大致如下所示:
图片

事实上,MyBatis内部对于配置文件解析的过程可以概括如下:

  • 加载配置文件MyBatis首先加载主配置文件(通常是mybatis-config.xml),并创建一个Configuration对象来表示整个MyBatis配置。
  • 解析主配置文件MyBatis使用XML解析器解析主配置文件,该文件包含了关于数据源、插件、类型别名、缓存等全局配置信息。这些配置会被存储在Configuration对象中。

其中参与配置文件解析的都继承与BaseBuilder

  • XMLStatementBuilder :这个类用于解析映射文件中的 <select>、<insert>、<update> 和 <delete> 等标签,构建与 SQL 语句相关的对象(如 MappedStatement),包括 SQL 语句的解析、参数映射、结果映射等。
  • XMLMapperBuilderXMLMapperBuilder 用于解析映射文件(通常是 Mapper.xml 文件),负责构建与映射文件相关的对象,包括映射文件的解析、SQL 语句的构建、参数映射、结果映射、缓存配置等。
  • XMLConfigBuilderXMLConfigBuilder 用于解析主配置文件(通常是 mybatis-config.xml 文件),负责构建与全局配置相关的对象,包括数据源配置、类型别名配置、插件配置、缓存配置等。

总结来看,对于MyBatis的加载过程来说,其在处理配置文件信息时,首先,会传递配置文件所在位置信息,然后再调用框架提供的SqlSessionFactorybuild方法便会根据传入路径信息去加载相关的配置文件,并进行解析。而解析的内容会存放到的configuration之中,进而方便后续组件的使用。

1.4.1.1 解析XML

Mybatis在初始化SqlSessionFactoryBean时,会找到mapperLocations配置的路径下中所有的XML文件并进行解析,这里我们重点关注两部分:创建SqlSourceMapperStatement

1.4.1.1.1 创建SqlSource

Mybatis会把每个SQL标签封装成SqlSource对象,然后根据SQL语句的不同,又分为动态SQL和静态SQL。其中,静态SQL包含一段String类型的sql语句;而动态SQL则是由一个个SqlNode组成。
在这里插入图片描述

假如我们有这样一个SQL:

<select id="getUserById" resultType="user">
    select * from user 
    <where>
        <if test="uid!=null">
            and uid=#{uid}
        </if>
    </where>
</select>

它对应的SqlSource对象看起来应该是这样的:
在这里插入图片描述

1.4.1.1.2 创建MappedStatement

接下来,Mybatis会为XML中的每个SQL标签都生成一个MappedStatement对象,这里面有两个属性很重要:

  • id全限定类名+方法名组成的ID
  • sqlSource:当前SQL标签对应的SqlSource对象

创建完的MappedStatement对象会被添加到Configuration中,Configuration对象就是Mybatis中的大管家,基本所有的配置信息都维护在这里。当把所有的XML都解析完成之后,Configuration就包含了所有的SQL信息。
在这里插入图片描述
到目前为止,XML就解析完成了,当我们执行Mybatis方法的时候,就可以通过 全限定类名+方法名 找到MappedStatement对象,然后解析里面的SQL内容并进行执行即可。

1.4.2 代理构建

当配置文件解析,下一步就是通过SqlSessiongetMapper方法来构建一个接口对应的代理类,这一过程大致如下:
在这里插入图片描述

这一过程中涉及的组件主要包括MapperProxyFactoryMapperRegistryMapperProxy,总之这一过程的本质就是通过Jdk动态代理的方式返回一个实现接口的实例对象

1.4.2.1 Dao 接口代理

但是Dao接口并没有具体的实现类,那么在被调用时,最终又是怎样找到我们的SQL语句的呢?
首先,我们在Spring配置文件中,一般会这样配置:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
 <property name="basePackage" value="com.viewscenes.netsupervisor.dao" />
 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>

或者你的项目是基于SpringBoot的,那么肯定也见过这种:@MapperScan("com.xxx.dao"),它们的作用是一样的,就是将包路径下的所有类注册到 Spring Bean 中,并将它们的beanClass设置为MapperFactoryBeanMapperFactoryBean实现了FactoryBean接口,俗称工厂Bean。那么,当我们通过@Autowired注入这个Dao接口时,返回的对象就是MapperFactoryBean这个工厂Bean中的getObject()方法对象。
那么,这个方法干了些什么呢?简单来说,它就是通过JDK动态代理,返回了一个Dao接口的代理对象MapperProxy,当我们通过@Autowired注入Dao接口时,注入的就是这个代理对象,我们调用Dao接口中的方法时,则会调用到MapperProxy对象的invoke()方法。
那么,目前为止,我们通过Dao接口也有了代理实现,所以就可以执行到它里面的方法了。

1.4.3 sql执行

当配置文件解析完成,接口相应的代理类构建完毕后,下一步要做的就是sql的执行,这一过程逻辑大致如下所示:
图片

这一部分的底层逻辑就是原生JDBC操纵数据库的那一套逻辑,即

  • 创建SQL语句:即创建StatementPreparedStatementCallableStatement对象,分别用于执行不同类型的SQL语句。
  • 执行SQL查询:使用创建的StatementPreparedStatement对象来执行SQL查询。
  • 处理查询结果:通过ResultSet对象来处理查询的结果数据。
1.4.3.1 执行

如上所述,当我们调用Dao接口方法的时候,实际调用到代理对象的invoke()方法。在这里,实际上调用的就是SqlSession里面的东西了。

public class DefaultSqlSession implements SqlSession {
 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
   MappedStatement ms = configuration.getMappedStatement(statement);
   return executor.query(ms, wrapCollection(parameter), rowBounds,Executor.NO_RESULT_HANDLER);
  }
 }
}

看到以上代码,说明它就是通过statement(全限定类型+方法名)拿到MappedStatement对象,然后通过执行器Executor去执行具体SQL并返回。
在这里插入图片描述

1.4.4 总结

Mybatis 内部对于sql执行的大致步骤:

  • 创建 SqlSessionFactory:使用Mybatis首先需要创建一个 SqlSessionFactory 对象,这通常通过读取MyBatis 的主配置文件(mybatis-config.xml)并使用 SqlSessionFactoryBuilder 来实现。SqlSessionFactory 负责创建数据库连接和 SqlSession 对象。
  • 创建 SqlSession:通过 SqlSessionFactory 创建一个 SqlSession 对象。SqlSession 代表了与数据库的一次会话,它可以执行 SQL 操作并管理数据库连接。通常,每个线程都会创建自己的 SqlSession
  • 执行 SQL 方法:在 SqlSession 中,通过调用方法执行 SQL 语句。MyBatis 支持多种方式来执行 SQL,包括 selectOne()、selectList()、insert()、update()、delete() 等方法。
  • SQL 语句解析:MyBatis 会解析 SQL 语句,包括动态 SQL,参数映射和结果映射。这包括了将 Java 对象转化为 SQL 语句中的参数,以及将查询结果映射回Java对象。
  • 执行 SQLMyBatisSQL 语句发送到数据库,并执行相应的操作,如查询、插入、更新或删除。数据库返回结果或受影响的行数,这取决于SQL语句的类型。
  • 处理结果:MyBatis 最终会将SQL的执行结果映射为 Java 对象,然后返回给调用者。映射过程通常基于映射文件中的配置。结果集的处理包括将数据库查询结果映射为 Java 对象的属性值。

进一步,上述步骤可总结概括总结为如下的流程。
在这里插入图片描述

2 深究mapper接口的注入问题

经常用SpringMyBatis也挺久的了,但是一直比较好奇mapper接口是怎么加载到spring容器中的,因为要想注入spring容器中,都必须有实例的,这就不得不提一下SpringMyBatis的中间件MyBatis-Spring

2.1 MyBatis-Spring

2.1.1 MyBatis-Spring基础

当在使用MyBatis时,一般是编写一个Mapper接口和一个Mapper.xml文件,我们都知道接口是不能直接被实例化的,然而我们一般在service层中编写的注入属性都是Mapper接口,那么Spring是如何对该接口进行实例化的呢

一般而言,如果使用SpringMyBatis作为开发框架时,在搭建开发环境的时候,都会做一个SpringMyBatis的整合,使用到的就是MyBatis-Spring这个中间件,MyBatis-Spring中间件把mapper接口和mapper.xml文件对应的代理类注册到Spring中,因此,在service层中就能根据类型注入,将对应mapper接口的代理类注入到service层中,这样才能够调用到对应的方法

2.1.2 MyBatis-Spring原理

在这里插入图片描述

2.1.3 讲解

Spring开发中,通常是在service层中通过依赖注入Dao层的实例,在MyBatis中,Mapper接口即对应着Dao实例,MyBatis-Spring中间件就是把MyBatis中的mapper.xmlmapper.java对应的Mapper接口注册到Spring容器中,使得service层可以直接通过以来注入获取到Mapper接口

2.1.3.1 注册

Spring中所有的Mapper接口都会被注册为MapperFactoryBean,所有的MapperFactoryBean会共享一个SqlSessionFactory,该SqlSessionFactorySqlSessionFactoryBean创建
sqlSessionFactoryconfiguration属性中存的是一个Configuration对象,configuratiaon对象中的mapperRegistry属性中存储了一个MapperRegistry对象,MapperRegistry对象中的knownMappers属性是一个keymapper.java文件对应接口的类型,valueMapperProxyFactory的对象。

2.1.3.2 获取

当从Spring中获取Mapper接口时,将会调用对应的MapperFactoryBeangetObject方法,该方法返回值即为对应的MapperProxyFactory创建的MapperProxy动态代理

2.1.4 整体流程图

在这里插入图片描述

2.2 总结

2.2.1 定位

注解方式 根据MapperScan里的内容找到basePackages

2.2.2 加载

MapperScannerRegistrarregisterBeanDefinitions方法通过ClassPathMapperScannerdoScan方法进行扫描basePackages
ClassPathMapperScanner继承springClassPathBeanDefinitionScanner,通过调用ClassPathBeanDefinitionScannerdoScan获得BeanDefinitionHolder,获取BeanDefinitionHolder之后通过processBeanDefinitions方法来把BeanDefinition对应的beanClass修改为MapperFactoryBeanbeanclass

2.2.3 注入

在注入mapper形成的bean中会根据MapperFactoryBean中的getObject获取对应的bean变量
MapperFactoryBean.getObject会调用SqlSessionTemplategetMapper方法获取mapper对象
SqlSessionTemplate调用Configuration.getMapper获取对象
Configuration调用MapperRegistry.getMapper方法
MapperRegistry根据mapperProxyFactory来生成对mappper的代理对象,该代理对象内部拥有mapperInterface以及SqlSessionTemplate对象

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值