前言
由于工作中第一次使用Mybatis,阅读完官方文档后,想了解一下Mybaits的主体代码设计和运行流程,通过在本地搭建Mybatis环境并Debug源码,最终以框架图的输出来总结本次源码阅读。
官方文档:
- Mybatis github 地址:GitHub - mybatis/mybatis-3: MyBatis SQL mapper framework for Java
- Mybatis 官方文档:mybatis – MyBatis 3 | 简介
源码阅读目标
- Mybatis的运行流程是怎么样的?
- 阅读完主体代码后画一个简易的框架图(如下)
一、 环境工作
pom文件依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>mybatis_study</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 最新release版本是3.5.15 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.15</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
</dependencies>
</project>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3336/test_db?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/BlogMapper.xml"/>
</mappers>
</configuration>
BlogMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mapper.BlogMapper">
<select id="selectBlogById" resultType="org.example.po.Blog">
select * from Blog where id = #{id}
</select>
<select id="selectBlogByTitle" resultType="org.example.po.Blog">
select * from Blog where title = #{title}
</select>
</mapper>
BlogMapper
package org.example.mapper;
import org.example.po.Blog;
/**
* @author caizelin
* @date 2024/1/13 18:09
*/
public interface BlogMapper {
Blog selectBlogById(Integer id);
Blog selectBlogByTitle(String title);
}
BlogPO
package org.example.po;
/**
* @author caizelin
* @date 2024/1/13 18:08
*/
public class Blog {
private Integer id;
private String title;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
Main
package org.example;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.example.mapper.BlogMapper;
import org.example.po.Blog;
import java.io.IOException;
import java.io.InputStream;
/**
* main()方法,分4步来debug源码
* @author caizelin
* @date 2024/1/13 17:59
*/
public class Main {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// step 1 : 构建SqlSession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// step 2: 创建SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// step 3: 通过sqlSession先获取到Mapper接口对象
BlogMapper mapper = session.getMapper(BlogMapper.class);
// step 4: 调用mapper的方法
Blog blog = mapper.selectBlogById(1);
}
}
}
二、 源码分析
1. SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
这段代码执行完成后,会优先解析mybatis-config.xml。然后转为配置对象Configuration,关注mapperRegistry和mappedStatements字段,这两个字段由具体的Mapper.xml解析出来的。
-
environment字段,dataSource数据放在在这里
-
mapperRegistry负责生成所有的Mapper的代理工厂的Map
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
}
- config: 由xml解析出来的全局的配置类Configuration
- knowMappers: 收集所有的Mapper接口的接口名-代理对象工厂类的Map
比如:BlogMapper Interface
key: BlogMapper接口全类名称 如org.example.mapper.BlogMapper
value: BlogMapper接口的代理对象工厂类,MapperProxyFactory<BlogMapper>
- mappedStatements负责解析所有的xml并生成:方法名称- xml配置解析对象(包含sql等信息)
第一次代码推断
推断代码段一:
BlogMapper mapper = session.getMapper(BlogMapper.class);
当从session.getMapper的时候,从configuration拿到mapperRegistry的knownMappers, 然后根据接口获取到对应的Mapper的代理类工厂,从工厂中创建出对应的代理类
推断代码段二:
Blog blog = mapper.selectBlogById(1);
代理类执行代码, 先从当前这个mapper中,根据方法名称,从mappedStatements中根据方法名,拿到xml配置解析出来的对象MappedStatement,然后取出MappedStatement.sqlSource.sql和入参来做SQL的拼接,得到select * from Blog where id = 1; 然后调用JDBC API 查询DB。
也就是说,这两行代码的核心是:
根据Mapper接口得到代理类,代理类中,完成SQL拼接,然后执行SQL,响应封装并返回。
2. SqlSession
SqlSession session = sqlSessionFactory.openSession()
SqlSession本身是个接口, 从注释信息来看,通过这个接口来执行命令,获取Mapper代理对象,并且管理transactions(一个session连接)
SqlSession的默认实现是DefaultSqlSession
也就是说,DefaultSqlSession内组合来Executor,通过Executor来调用JDBC来完成CRUD
第二次代码推断
走到了这里,这里就是根据statement得到MappedStatement,然后调用executor去查DB。根据第一次代码推断,代理类去执行方法然后返回数据,也就是说,代理类Blog blog = mapper.selectBlogById(1) 是怎么走到SqlSession.selectList(),所以,代理类的核心工作逻辑大概就是:根据selectBlogById判断要走到SqlSession的哪个方法,然后调用SqlSession的方法执行代码
3. 获取代理类逻辑
Debug源代码
4. 代理类执行方法
代理类执行方法的时候,在invoker的方法中,调用cachedInvoker.invoke(),而cachedInvoker的时候,会传入当前方法method,当前Mapper接口,以及sqlSession中的configuration(全局配置) 生成一个MapperMethod 对象。这个对象里面有两个字段需要关注,command和method方法签名
调用cachedInvoker.invoke()
cachedInvoker.invoke()即执行MapperMethod.execute(sqlSession, args)方法,在这里最终会进入到SELET中,
上图的写错了,是进入到SELET中,然后调用了sqlSession.selectOne()方法去执行数据查询
小结:这个代理类的作用仅仅是分析出目前所执行的方法要去调用sqlSession的哪个方法,然后传入method名称和参数,调用sqlSession.selectOne()去执行代码。
5. SqlSession最终执行方法
Eexecutor会从DB拿数据
执行doQuery方法,在这里执行prepareStatement
执行repareStatement
获取数据库连接
返回Statement,这里的SQL已经拼接好了
执行handler.query方法
三、总结
流程大概走完了,我在这分为了3层,配置层,代理层,核心层
配置层:
- 通过xml解析,得到Configuration配置类(全局配置),这个类中,有两个核心字段需要关注,分别是mapperRegistry(为每个mapper.xml注册代理类工厂,放在knowMappers子属性中)和mappedStatements(扫描每个mapper.xml 得到 方法名-MappedStatement(xml中该方法sql的配置映射对象))
- 构建SqlSessionFactory, 使用默认实现DefaultSqlSessionFactory,该对象组合了Configuration作为其属性
核心层:
- 使用SqlSessionFactory构建SqlSession,SqlSession接口可以执行命令,获取mapper代理类,并管理连接
- SqlSession调用Executor进行SQL的执行
- Executro调用 JDBC API 和DB交互,封装并返回数据
代理层:
- 基于核心层的SqlSession获取mapper代理类,调用代理类执行方法
- 代理类本身是MapperProxy类型,invoke的时候,会使用方法名,全局配置Configuration, mapper的接口类型构造MapperMethod对象,该对象在构建过程中,会解析入参并归类当前的方法属于哪个SqlCommandType(SQL的命令类型,INSERT,UPDATE,DELETE,SELECT等,并封装到属性SqlComand中。
- 代理类构造完MapperMethod后,会执行MapperMethod.executor()方法,该方法会根据当前MapperMethod对象的SqlCommandType,去调用核心层的sqlSession的对应方法去完成命令的执行,然后核心层继续往下执行得到响应并返回给代理类数据