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
用于数据库操作,JdbcClient
对JdbcTemplate
进行了封装,采用了 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核心对象 | 解释 |
---|---|
Configuration | MyBatis所有的配置信息都维持在Configuration对象之中 |
SqlSessionFactory | SqlSession工厂专门创建SqlSession |
SqlSession | 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能 |
Executor | MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护 |
StatementHandler | 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合 |
ParameterHandler | 负责对用户传递的参数转换成JDBC Statement 所需要的参数 |
ResultSetHandler | 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合 |
TypeHandler | 负责java数据类型和jdbc数据类型之间的映射和转换 |
MappedStatement | MappedStatement维护了一条mapper.xml文件里面 select 、update、delete、insert节点的封装 |
SqlSource | 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回 |
BoundSql | 表示动态生成的SQL语句以及相应的参数信息 |
1.3 原理图
mybatis
应用程序通过SqlSessionFactoryBuilder
从mybatis-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
语句的解析、参数映射、结果映射等。XMLMapperBuilder
:XMLMapperBuilder
用于解析映射文件(通常是Mapper.xml
文件),负责构建与映射文件相关的对象,包括映射文件的解析、SQL
语句的构建、参数映射、结果映射、缓存配置等。XMLConfigBuilder
:XMLConfigBuilder
用于解析主配置文件(通常是mybatis-config.xml
文件),负责构建与全局配置相关的对象,包括数据源配置、类型别名配置、插件配置、缓存配置等。
总结来看,对于MyBatis
的加载过程来说,其在处理配置文件信息时,首先,会传递配置文件所在位置信息,然后再调用框架提供的SqlSessionFactory
的build
方法便会根据传入路径信息去加载相关的配置文件,并进行解析。而解析的内容会存放到的configuration
之中,进而方便后续组件的使用。
1.4.1.1 解析XML
Mybatis
在初始化SqlSessionFactoryBean
时,会找到mapperLocations
配置的路径下中所有的XML
文件并进行解析,这里我们重点关注两部分:创建SqlSource
和MapperStatement
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 代理构建
当配置文件解析,下一步就是通过SqlSession
的getMapper
方法来构建一个接口对应的代理类,这一过程大致如下:
这一过程中涉及的组件主要包括MapperProxyFactory
、MapperRegistry
、MapperProxy
,总之这一过程的本质就是通过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
设置为MapperFactoryBean
,MapperFactoryBean
实现了FactoryBean
接口,俗称工厂Bean
。那么,当我们通过@Autowired
注入这个Dao
接口时,返回的对象就是MapperFactoryBean
这个工厂Bean
中的getObject()
方法对象。
那么,这个方法干了些什么呢?简单来说,它就是通过JDK
动态代理,返回了一个Dao
接口的代理对象MapperProxy
,当我们通过@Autowired
注入Dao
接口时,注入的就是这个代理对象,我们调用Dao
接口中的方法时,则会调用到MapperProxy
对象的invoke()
方法。
那么,目前为止,我们通过Dao
接口也有了代理实现,所以就可以执行到它里面的方法了。
1.4.3 sql执行
当配置文件解析完成,接口相应的代理类构建完毕后,下一步要做的就是sql的执行,这一过程逻辑大致如下所示:
这一部分的底层逻辑就是原生JDBC操纵数据库的那一套逻辑,即
创建SQL语句
:即创建Statement
、PreparedStatement
或CallableStatement
对象,分别用于执行不同类型的SQL语句。执行SQL查询
:使用创建的Statement
或PreparedStatement
对象来执行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
对象。- 执行
SQL
:MyBatis
将SQL
语句发送到数据库,并执行相应的操作,如查询、插入、更新或删除。数据库返回结果或受影响的行数,这取决于SQL
语句的类型。 - 处理结果:
MyBatis
最终会将SQL
的执行结果映射为Java
对象,然后返回给调用者。映射过程通常基于映射文件中的配置。结果集的处理包括将数据库查询结果映射为Java
对象的属性值。
进一步,上述步骤可总结概括总结为如下的流程。
2 深究mapper接口的注入问题
经常用Spring
和MyBatis
也挺久的了,但是一直比较好奇mapper
接口是怎么加载到spring
容器中的,因为要想注入spring
容器中,都必须有实例的,这就不得不提一下Spring
和MyBatis
的中间件MyBatis-Spring
2.1 MyBatis-Spring
2.1.1 MyBatis-Spring基础
当在使用MyBatis
时,一般是编写一个Mapper
接口和一个Mapper.xml
文件,我们都知道接口是不能直接被实例化的,然而我们一般在service
层中编写的注入属性都是Mapper
接口,那么Spring
是如何对该接口进行实例化的呢
一般而言,如果使用Spring
和MyBatis
作为开发框架时,在搭建开发环境的时候,都会做一个Spring
与MyBatis
的整合,使用到的就是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.xml
和mapper.java
对应的Mapper接口
注册到Spring
容器中,使得service
层可以直接通过以来注入获取到Mapper
接口
2.1.3.1 注册
在Spring
中所有的Mapper
接口都会被注册为MapperFactoryBean
,所有的MapperFactoryBean
会共享一个SqlSessionFactory
,该SqlSessionFactory
由SqlSessionFactoryBean
创建
在sqlSessionFactory
的configuration
属性中存的是一个Configuration
对象,configuratiaon
对象中的mapperRegistry
属性中存储了一个MapperRegistry
对象,MapperRegistry
对象中的knownMappers
属性是一个key
为mapper.java
文件对应接口的类型,value
为MapperProxyFactory
的对象。
2.1.3.2 获取
当从Spring
中获取Mapper
接口时,将会调用对应的MapperFactoryBean
的getObject
方法,该方法返回值即为对应的MapperProxyFactory
创建的MapperProxy
动态代理
2.1.4 整体流程图
2.2 总结
2.2.1 定位
注解方式 根据MapperScan
里的内容找到basePackages
2.2.2 加载
MapperScannerRegistrar
里registerBeanDefinitions
方法通过ClassPathMapperScanner
的doScan
方法进行扫描basePackages
ClassPathMapperScanner
继承spring
中ClassPathBeanDefinitionScanner
,通过调用ClassPathBeanDefinitionScanner
中doScan
获得BeanDefinitionHolder
,获取BeanDefinitionHolder
之后通过processBeanDefinitions
方法来把BeanDefinition
对应的beanClass
修改为MapperFactoryBean
的beanclass
2.2.3 注入
在注入mapper
形成的bean
中会根据MapperFactoryBean
中的getObject
获取对应的bean
变量
MapperFactoryBean.getObject
会调用SqlSessionTemplate
的getMapper
方法获取mapper
对象
SqlSessionTemplate
调用Configuration.getMapper
获取对象
Configuration
调用MapperRegistry.getMapper
方法
MapperRegistry
根据mapperProxyFactory
来生成对mappper
的代理对象,该代理对象内部拥有mapperInterface
以及SqlSessionTemplate
对象