Mybatis基本原理

简述Mybatis基本原理

Mybatis快速开始

1、引入pom依赖

2、Mybatis配置文件

3、编写映射文件

4、编写代码调用 SqlSession 执行sql语句

5、关闭SqlSession

Spring-boot是如何加载Mybatis的?

熟悉 SpringBoot 启动过程的朋友知道,SpringBoot 会去加载mybatis-spring-boot-autoconfigure-x.x.x.jar下 META-INF 中的spring.factories文件:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

MybatisAutoConfiguration()

1
2
3
4
5
6
7
8
public class MybatisAutoConfiguration implements InitializingBean {
//...
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// ...
}
}

可以发现,SqlSessionFactory 方法是我们的入口处。也就是说 Spring-boot 是通过 启动 SqlSessionFactory 加载了Mybatis框架。

准备工作

本博文使用 Mybatis 3.5.5 版本。

创建 database table

1
2
3
4
5
6
7
8
9
10
11
12
13
 create table user_info(
-> id int not null primary key AUTO_INCREMENT,
-> name char(50) not null,
-> age char(50) not null
-> );
desc user_info;
+-------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | char(50) | NO | | NULL | |
| age | char(50) | NO | | NULL | |
+-------+----------+------+-----+---------+----------------+

User Object

1
2
3
4
5
6
@Data
public class User {
private Long id;
private String name;
private Integer age;
}

mapper:UserMapper 为根据id查询用户信息

1
2
3
public interface UserMapper {
User selectUserById(Integer id);
}

UserMapper.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wsdsg.spring.boot.analyze.mapper.UserMapper">
<select id="selectUserById" resultType="com.wsdsg.spring.boot.analyze.entity.User"
parameterType="java.lang.Integer">
select * from user_info where id = #{id}
</select>
</mapper>

mybaitis的主配置文件 mybatis-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="dbconfig.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--resource-->
<mapper resource="UserMapper.xml"/>

<!--class-->
<!-- <mapper class="com.wsdsg.spring.boot.analyze.mapper.UserMapper"/>-->
<!--url-->
<!--<mapper url="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml"/>-->
<!--package-->
<!--<package name="com.wsdsg.spring.boot.analyze.mapper" />-->

</mappers>
</configuration>

数据库连接的属性文件 *.property:

1
2
3
4
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class TestMybatis implements CommandLineRunner{

@Override
public void run(String... args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
/*UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUserById(1);
System.out.println(user);*/
Object o = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById", 1);
System.out.println("我是第一次查询的"+o);
System.out.println("-------------------------------我是分割线---------------------");
Object z = session.selectOne("com.wsdsg.spring.boot.analyze.mapper.UserMapper.selectUserById", 1);
System.out.println("我是第二次查询的"+z);
/*User user = new User();
user.setAge(15);
user.setName("achuan");
int insert = session.insert("com.wsdsg.spring.boot.analyze.mapper.UserMapper.addOneUser", user);
session.commit();
System.out.println(insert);*/
} finally {
session.close();
}
}
}

MyBatis重要组件和运行流程图

  • Configuration MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中
  • SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
  • Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  • StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等
  • ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所对应的数据类型
  • ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
  • TypeHandler 负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换
  • MappedStatement MappedStatement维护一条<select|update|delete|insert>节点的封装
  • SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
  • BoundSql 表示动态生成的SQL语句以及相应的参数信息
graph TB
	SqlSession[SqlSession: Mybatis顶层API 通过会话访问提供crud能力] --> Executor
	Executor[Executor: Mybatis的执行核心 负责SQL语言的生产执行和缓存维护] --> StatementHandler
	StatementHandler[StatementHandler: 负责JDBC的Statement交互 包括设置statement参数和将JDBC返回结果转换为List] --> ParameterHandler
	StatementHandler --> ResultSetHandler
	ParameterHandler[ParameterHandler: 设置Statement参数] --> TypeHandler
	ResultSetHandler[ResultSetHandler: 将JDBC返回结果转换为List] --> TypeHandler
	TypeHandler[TypeHandler: 负责JDBC Type和Java Type之间的转换 / 设置参数 / 取出查询结果中的特定列] --> Stratement
	Stratement[JDBC Stratement: PreparedStatement / SimpleStatement / CallableStatement]
	Stratement --> ResultSet[ResultSet: 查询结果继]
	ResultSet --> TypeHandler

流程图

graph LR
	mybatis --> Build
	Build --> xml[XML File] --> sqlSessionFactory
	Build --> Java[Java Code] --> sqlSessionFactory
	sqlSessionFactory --> SqlSession
	SqlSession --调用XML文件中定义的SQL语句--> SqlSession.getMapper
    SqlSession --调用Java Code中定义的SQL语句--> SqlSession.getMapper

Mybatis基本机制

0 配置加载

获取mybatis主配置文件的输入流对象。

1
2
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

1 解析mapper文件

入口代码

1
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

1.1 解析主配置文件 parser.parse()

SqlSessionFactoryBuilder().build()

会创建一个XMLConfigBuilder对象,这个对象的作用就是解析主配置文件

可以看出主配置文件 mybatis-config.xml 的最外层节点是 <configuration> 标签,mybatis的初始化就是把这个标签以及他的所有子标签进行解析,把解析好的数据封装在 Configuration 这个类中。

1
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

调用 parser.parse() 进行解析

1
var5 = this.build(parser.parse());

parser.parse() 内容,XMLConfigBuilder 维护一个parsed属性默认为false,这个方法一开始就判断这个主配置文件是否已经被解析,如果解析过了就抛异常。由于这里是工厂模式,所以每次只会解析一次主配置文件。后面即使创建新的链接,也无需再次解析主配置文件。

1
2
3
4
5
6
7
8
9
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}

this.parseConfiguration()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void parseConfiguration(XNode root) {
try {
this.propertiesElement(root.evalNode("properties"));
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(settings);
this.loadCustomLogImpl(settings);
this.typeAliasesElement(root.evalNode("typeAliases"));
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(settings);
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}

可以看出这个方法是对 <configuration> 的所有子标签挨个解析。

  • settings属性配置,在settings会配置缓存,日志之类的。
  • typeAliases是配置别名。
  • environments是配置数据库链接和事务。

这些子节点会被一个个解析并且把解析后的数据封装在 Configuration 这个类中,可以看到parser.parse() 方法返回的就是由 mybatis-config.xml 文件解析出来的 Configuration 对象。

总结:解析主配置文件把主配置文件里的所有信息封装到Configuration这个对象中

1.2 mappers解析 mapperElement()

在这里我们重点分析的解析mappers这个子标签,这个标签里面还会有一个个的mapper标签去映射mapper所对应的mapper.xml,比如这次配置的 UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--
properties/environments 配置
...
-->

<mappers>
<!--resource-->
<mapper resource="UserMapper.xml"/>

<!--class-->
<!-- <mapper class="com.wsdsg.spring.boot.analyze.mapper.UserMapper"/>-->

<!--url-->
<!--<mapper url="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml"/>-->

<!--package-->
<!--<package name="com.wsdsg.spring.boot.analyze.mapper" />-->

</mappers>
</configuration>

this.mapperElement 源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();

while(true) {
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String resource;
if ("package".equals(child.getName())) {
resource = child.getStringAttribute("name");
this.configuration.addMappers(resource);
} else {
resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
XMLMapperBuilder mapperParser;
InputStream inputStream;
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
inputStream = Resources.getUrlAsStream(url);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
mapperParser.parse();
} else {
if (resource != null || url != null || mapperClass == null) {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}

Class<?> mapperInterface = Resources.classForName(mapperClass);
this.configuration.addMapper(mapperInterface);
}
}
}
return;
}
}
}
  1. 这个方法一开始是一个循环,为什么要循环呢?因为一个mappers节点下面可能会有很多mapper节点。在应用中肯定不止一个mapper.xml。所以会去遍历每一个mapper节点去解析该节点所映射的xml文件。

  2. 循环下面是一个if..else判断。它先判断mappers下面的子节点是不是package节点。因为在实际开发中有很多的xml文件,不可能每一个xml文件都用一个mapper节点去映射,我们干脆会用一个package节点去映射一个包下面的所有的xml,这是多文件映射

  3. 如果不是package节点那肯定就是mapper节点做单文件映射。看下面的三行代码,发现单文件映射有3种方式。

    1. 使用mapper节点的resource属性直接映射xml文件。<mapper resource="UserMapper.xml"/>

    2. 使用mapper节点url属性映射磁盘内的某个xml文件。

      <mapper url="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml"/>

    3. 使用mapper节点的class属性直接映射某个mapper接口。可以回头看看我的主配置文件的mappers节点。

      <mapper class="com.wsdsg.spring.boot.analyze.mapper.UserMapper"/>

总结:解析主配置文件把主配置文件里的所有信息封装到 Configuration 这个对象中。

1.3 以resource方式解析

实际上映射xml的方式看源码可以得出有4种方式,先看单文件映射的resource方式。

1
2
3
4
5
6
7
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
// 方法执行单个 resource 文件解析
mapperParser.parse(); <========== 1.3.1
}
  1. 第一行代码的意思是实例化一个错误上下文对象。这个对象是干什么用的呢?使用mybatis的过程中如果出现错误会不会提示这个错误在哪个xml中,还提示这个错误在xml中的哪个sql中。这个对象的作用就是把错误信息封装起来,如果出现错误就会调用这个对象的toString方法。这个resource参数就是String类型的xml的名字,在这次的项目中是UserMapper.xml
  2. 第二行读取这个xml获取输入流对象。
  3. 然后创建一个mapper的xml文件解析器,类似XMLConfigBuilder,解析Mapper.xml文件。

1.3.1 mapperParser.parse()

mapperParser.parse() 方法执行单个 resource 文件解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
// 将单个 mapper.xml 文件中的多个sql语句转换成多个 MappedStatement 对象
this.configurationElement(this.parser.evalNode("/mapper")); <========== 1.3.2
// 将解析了的 <mapper> 进行缓存快照
this.configuration.addLoadedResource(this.resource); <========== 1.3.6
// 将单个 mapper.xml 文件的解析结果进行缓存
this.bindMapperForNamespace(); <========== 1.3.7
}

this.parsePendingResultMaps();
this.parsePendingCacheRefs();
this.parsePendingStatements();
}

1.3.2 configurationElement()

configurationElement() 方法将单个 mapper.xml 文件中的多个sql语句转换成多个 MappedStatement 对象

一开始就判断这个 xml 是否被解析过了。因为 configuration 对象会维护一个String类型的set集合loadedResources,这个集合中存放了所有已经被解析过的xml的名字。

this.configurationElement()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace != null && !namespace.isEmpty()) {
this.builderAssistant.setCurrentNamespace(namespace);
this.cacheRefElement(context.evalNode("cache-ref"));
this.cacheElement(context.evalNode("cache"));
this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
this.resultMapElements(context.evalNodes("/mapper/resultMap"));
this.sqlElement(context.evalNodes("/mapper/sql"));
// 一个一个地解析 mapper.xml 所有节点数据
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); <========== 1.3.3
} else {
throw new BuilderException("Mapper's namespace cannot be empty");
}
} catch (Exception var3) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);
}
}

1.3.3 buildStatementFromContext()

这个方法就是一个一个地解析 mapper.xml 所有节点数据。比如解析namespace,resultMap等等。重点是最后一句 this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

1
2
3
4
5
6
7
private void buildStatementFromContext(List<XNode> list) {
if (this.configuration.getDatabaseId() != null) {
this.buildStatementFromContext(list, this.configuration.getDatabaseId());
}

this.buildStatementFromContext(list, (String)null);
}

没什么好说的,继续进入 this.buildStatementFromContext,注意,这里的方法名和上面的方法名是一样的,都是 buildStatementFromContext,这里是体现了Java中的多态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
Iterator var3 = list.iterator();

while(var3.hasNext()) {
XNode context = (XNode)var3.next();
XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);
try {
// xml的会话解析器 负责解析每个sql语句(select、insert、update、delete)
statementParser.parseStatementNode(); <========== 1.3.4
} catch (IncompleteElementException var7) {
this.configuration.addIncompleteStatement(statementParser);
}
}

}
  1. 这个方法一开始是一个循环,遍历一个list,这个list里装的是xml中的所有sql节点,比如 selectinsertupdatedelete ,每一个sql是一个节点。循环解析每一个sql节点。
  2. 创建一个xml的会话解析器去解析每个节点。

1.3.4 parseStatementNode()

进入 XMLStatementBuilder.parseStatementNode(),xml的会话解析器,负责解析每个sql语句(select、insert、update、delete)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public void parseStatementNode() {
String id = this.context.getStringAttribute("id");
String databaseId = this.context.getStringAttribute("databaseId");
if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
String nodeName = this.context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = this.context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = this.context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = this.context.getBooleanAttribute("resultOrdered", false);
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);
includeParser.applyIncludes(this.context.getNode());
String parameterType = this.context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = this.resolveClass(parameterType);
String lang = this.context.getStringAttribute("lang");
LanguageDriver langDriver = this.getLanguageDriver(lang);
this.processSelectKeyNodes(id, parameterTypeClass, langDriver);
String keyStatementId = id + "!selectKey";
keyStatementId = this.builderAssistant.applyCurrentNamespace(keyStatementId, true);
Object keyGenerator;
if (this.configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(this.context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = this.context.getIntAttribute("fetchSize");
Integer timeout = this.context.getIntAttribute("timeout");
String parameterMap = this.context.getStringAttribute("parameterMap");
String resultType = this.context.getStringAttribute("resultType");
Class<?> resultTypeClass = this.resolveClass(resultType);
String resultMap = this.context.getStringAttribute("resultMap");
String resultSetType = this.context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = this.resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = this.configuration.getDefaultResultSetType();
}

String keyProperty = this.context.getStringAttribute("keyProperty");
String keyColumn = this.context.getStringAttribute("keyColumn");
String resultSets = this.context.getStringAttribute("resultSets");
// 将 ` MappedStatement` 对象逐一缓存
this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets <========== 1.3.5
}
}

这个方法很长,大致意思就是解析这个sql标签里的所有数据,并把所有数据通过 addMappedStatement 这个方法封装在MappedStatement这个对象中。MappedStatement维护一条 <select|update|delete|insert> 节点的封装,比如这个sql标签的id ,sql语句,入参,出参,等等。牢记一个sql的标签对应一个MappedStatement对象。

1.3.5 addMapperStatement()

addMapperStatement() 方法将 MappedStatement 对象逐一缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap, Class<?> parameterType,
String resultMap, Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (this.unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
} else {
id = this.applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
org.apache.ibatis.mapping.MappedStatement.Builder statementBuilder = (new org.apache.ibatis.mapping.MappedStatement.Builder(this.configuration, id, sqlSource, sqlCommandType))
.resource(this.resource).
fetchSize(fetchSize).
timeout(timeout).
statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(this.getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired((Boolean)this.valueOrDefault(flushCache, !isSelect))
.useCache((Boolean)this.valueOrDefault(useCache, isSelect))
.cache(this.currentCache);
ParameterMap statementParameterMap = this.getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}

MappedStatement statement = statementBuilder.build(); <===========
this.configuration.addMappedStatement(statement);
return statement;
}
}

MappedStatement statement = statementBuilder.build(); 通过解析出的参数构建了一个MapperStatement对象

this.configuration.addMappedStatement(statement); 把解析出来的 MapperStatement 装到Configuration维护的Map集合中。key值是这个sql标签的id值,我们这里应该就是selectUserById,value值就是我们解析出来的MapperStatement对象。

本质:其实Mybatis解析xml的目的就是把每个xml中的每个增删改查的sql标签解析成一个个MapperStatement,并把解析出来的这些对象装到Configuration的Map中备用。这里Configuration的Map就是一个catch。

1.3.6 addLoadedResource()

得到所有 Sql 语句的 MapperStatement 对象之后,回到 mapperParser.parse()1.3.1 mapperParser.parse()) 。

this.configuration.addLoadedResource(this.resource); 这一行是将解析了的 <mapper> 进行缓存快照,防止重复处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
// 将单个 mapper.xml 文件中的多个sql语句转换成多个 MappedStatement 对象
this.configurationElement(this.parser.evalNode("/mapper")); <========== 1.3.2
// 将解析了的 <mapper> 进行缓存快照
this.configuration.addLoadedResource(this.resource); <========== 1.3.6
// 将单个 mapper.xml 文件的解析结果进行缓存
this.bindMapperForNamespace(); <========== 1.3.7
}

this.parsePendingResultMaps();
this.parsePendingCacheRefs();
this.parsePendingStatements();
}

1.3.7 bindMapperForNamespace()

将单个 mapper.xml 文件的解析结果进行缓存。

mapperParser.parse() 第三行 bindMapperForNamespace()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void bindMapperForNamespace() {
// value 通过动态代理生产的class对象的代理对象。
String namespace = this.builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class boundType = null;

try {
// 通过反射生产的mapper的class对象。
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException var4) {
}

if (boundType != null && !this.configuration.hasMapper(boundType)) {
// 保存value
this.configuration.addLoadedResource("namespace:" + namespace);
// 保存key
this.configuration.addMapper(boundType); <========== 1.3.8
}
}

}
  1. 一开始获取名称空间,名称空间一般是我们mapper.xml 的全限定名(一个类的全限定名是将类全名的.全部替换为/,例如com/enjoy/learn/core/oop/method/TestClass),它通过反射获取这个mapper的class对象。

  2. if判断,Configuration中也维护了一个Map对象

    • key:通过反射生产的mapper的class对象
    • value:通过动态代理生产的class对象的代理对象
  3. 因为Map中还没有装我们生产的mapper对象,进入if中,

    configuration.addLoadedResource:它先把名称空间存到我们刚才存xml名字的set集合configuration对象的loadedResources属性)中。

    configuration.addMapper:然后再把反射生产的mapper的class对象存到Mapperconfiguration对象的mapperRegistry属性)中。

1.3.8 addMapper()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (this.hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}

boolean loadCompleted = false;

try {
this.knownMappers.put(type, new MapperProxyFactory(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
this.knownMappers.remove(type);
}

}
}
}

可以看出 mapperRegistry 这个类维护的Map的名字是 knownMappers,就是注册过的mapper。我们看他的put,key是我们生成的mapper的class对象,value是通过动态代理生成的mapper的代理对象。

到此mybatis根据主配置文件初始化就完成了,那说了这么久到底做了什么呢?我们总结一下。通过XmlConfigBuilder 解析主配置文件,然后通过 XmlMapperBuild 解析mappers下映射的所有xml文件(循环解析)。把每个xml中的各个sql解析成一个个 MapperStatement 对象装在 Configuration 维护的一个Map集合中,key值是id,value是mapperstatement对象-----然后把解析过的xml的名字和名称空间装在set集合中,通过名称空间反射生成的mapper的class对象以及class对象的代理对象装在 Configuration 对象维护的 mapperRegistry 中的Map中。

1.4 以URL方式解析

我们用resource引入xml的方法是先解析xml ,把各个sql标签解析成mapperstatement对象装进集合,然后再把mapper接口的class对象以及代理对象装进集合,但是引入xml的方式有4种,其中单文件引入方式还有url方式和class方式,看源码可以知道url方式和resource方法一样,都是直接引入一个 mapper.xml ,在其基础上处理,没有区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--
properties/environments 配置
...
-->

<mappers>
<!--resource-->
<mapper resource="UserMapper.xml"/>

<!--url-->
<mapper url="D:\coder_soft\idea_workspace\ecard_bus\spring-boot-analyze\target\classes\UserMapper.xml"/>
</mappers>
</configuration>

this.mapperElement 源码中,处理 urlresource 的代码一摸一样:

1
2
3
4
5
6
7
8
9
10
11
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
inputStream = Resources.getUrlAsStream(url);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
mapperParser.parse();
}

1.5 以Class方式解析

class方式是引入一个mapper接口,同resource和url的处理方法相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
<!--
properties/environments 配置
...
-->
<mappers>
<mapper class="com.wsdsg.spring.boot.analyze.mapper.UserMapper"/>
</mappers>
</configuration>

this.mapperElement 源码中,处理 class 文件解析的代码如下,先反射生产mapper接口的class对象,再调用Configuration的addMpper方法。

1
2
3
4
5
6
7
8
else {
if (resource != null || url != null || mapperClass == null) {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
// 反射生产mapper接口的class对象
Class<?> mapperInterface = Resources.classForName(mapperClass);
this.configuration.addMapper(mapperInterface);
}

this.configuration.addMapper() 方法如下,和 1.3.8 addMapper() 一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public <T> void addMapper(Class<T> type) {
this.mapperRegistry.addMapper(type);
}

public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (this.hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}

boolean loadCompleted = false;

try {
this.knownMappers.put(type, new MapperProxyFactory(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
this.knownMappers.remove(type);
}

}
}

}

生产mapper的class对象后,再通过动态代理生产代理对象然后装进集合。

那我们接口对象生成了,但是还没解析xml呢,别急我们进入parser.parse()这个方法

1.5.1 parser.parse()

  • 开始会判断这个mapper对应的xml是否存在于装已经解析过的xml的set集合中。
  • 如果没有缓存,进入if中 重点来了,loadXmlResource() 这个方法看名字就知道是加载xml资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void parse() {
String resource = this.type.toString();
if (!this.configuration.isResourceLoaded(resource)) {
this.loadXmlResource(); <======== 1.5.2
this.configuration.addLoadedResource(resource);
this.assistant.setCurrentNamespace(this.type.getName());
this.parseCache();
this.parseCacheRef();
Method[] var2 = this.type.getMethods();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
Method method = var2[var4];
if (this.canHaveStatement(method)) {
if (this.getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent() && method.getAnnotation(ResultMap.class) == null) {
this.parseResultMap(method);
}

try {
this.parseStatement(method);
} catch (IncompleteElementException var7) {
this.configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
}

this.parsePendingMethods();
}

1.5.2 loadXmlResource()

根据class name拼接,xml文件名称,调用 XMLMapperBuilder.parse() 来解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void loadXmlResource() {
if (!this.configuration.isResourceLoaded("namespace:" + this.type.getName())) {
String xmlResource = this.type.getName().replace('.', '/') + ".xml";
InputStream inputStream = this.type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
try {
inputStream = Resources.getResourceAsStream(this.type.getClassLoader(), xmlResource);
} catch (IOException var4) {
}
}

if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, this.assistant.getConfiguration(), xmlResource, this.configuration.getSqlFragments(), this.type.getName());
xmlParser.parse(); <======== 1.5.3
}
}

}

1.5.3 XMLMapperBuilder.parse()

代码如下:

1
2
3
4
5
6
7
8
9
10
11
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
this.configurationElement(this.parser.evalNode("/mapper"));
this.configuration.addLoadedResource(this.resource);
this.bindMapperForNamespace();
}

this.parsePendingResultMaps();
this.parsePendingCacheRefs();
this.parsePendingStatements();
}

可以发现,1.5.3 XMLMapperBuilder.parse() 和 1.3.1mapperParser.parse() 一摸一样,也就是执行了1.3.1 到 1.3.8 的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
// 将单个 mapper.xml 文件中的多个sql语句转换成多个 MappedStatement 对象
this.configurationElement(this.parser.evalNode("/mapper")); <========== 1.3.2
// 将解析了的 <mapper> 进行缓存快照
this.configuration.addLoadedResource(this.resource); <========== 1.3.6
// 将单个 mapper.xml 文件的解析结果进行缓存
this.bindMapperForNamespace(); <========== 1.3.7
}

this.parsePendingResultMaps();
this.parsePendingCacheRefs();
this.parsePendingStatements();
}

resource/url方法

resource和url是直接引入xml,那我们就先解析xml,然后通过xml的名称空间反射生成mapper的class对象,再通过动态代理生产class对象的代理对象。

class方法

class方式填写的是mapper接口的全限定名(一个类的全限定名是将类全名的.全部替换为/,例如com/enjoy/learn/core/oop/method/TestClass),就是上面的那个名称空间,所以先生成class对象和代理对象,然后通过拼接字符串就是全限定名+“.xml”获取xml的名称,然后再解析xml。

1.6 多文件解析

mapperElement()方法中,关于多文件解析的入口代码如下:

1
2
3
4
if ("package".equals(child.getName())) {
resource = child.getStringAttribute("name");
this.configuration.addMappers(resource); <======= 1.6.1
}

它首先或得xml所在的包名,然后调用configuration的addMappers对象,是不是有点眼熟,

单文件映射是addMapper,

多文件映射是addMappers。

1.6.1 mapperElement()

分两个文件,三个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void addMappers(String packageName) {
this.mapperRegistry.addMappers(packageName);
}

public void addMappers(String packageName) {
this.addMappers(packageName, Object.class);
}

public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
resolverUtil.find(new IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
Iterator var5 = mapperSet.iterator();

while(var5.hasNext()) {
Class<?> mapperClass = (Class)var5.next();
this.addMapper(mapperClass);
}

}
  • 通过 ResolverUtil 这个解析工具类找出该包下的所有 mapper 的名称。
  • 通过反射生产mapper的class对象装进集合中。
  • 循环调用 addMapper(mapperClass) 这个方法,这就和单文件映射的class类型一样了,把mapper接口的class对象作为参数传进去,然后生产代理对象装进集合然后再解析xml。

到此mybatis的初始化就完成了。

2 获取SqlSession对象

2.1 DefaultSqlSessionFactory Object

获取DefaultSqlSessionFactory Object,根据主配置文件的流对象构建一个会话工厂对象。

1
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); <===== 第一层

其实我们点进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 第一层 
public SqlSessionFactory build(InputStream inputStream) {
return this.build((InputStream)inputStream, (String)null, (Properties)null); <===== 第二层
}

// 第二层
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse()); <===== 第三层
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}

会发现最后返回的是 DefaultSqlSessionFactory 对象,获得该对象。

1
2
3
4
// 第三层
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

2.2 SqlSession Object

获取会话对象SqlSession Object,走的代码如下,直接open一个session,SqlSession是与数据库互动的顶级api,所有的增删改查都要调用session。

1
SqlSession session = sqlSessionFactory.openSession();   <===== 进入openSession

进入openSession()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface SqlSessionFactory {

SqlSession openSession();

SqlSession openSession(boolean autoCommit);

SqlSession openSession(Connection connection);

SqlSession openSession(TransactionIsolationLevel level);

SqlSession openSession(ExecutorType execType);

SqlSession openSession(ExecutorType execType, boolean autoCommit);

SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

SqlSession openSession(ExecutorType execType, Connection connection);

Configuration getConfiguration();

}

这个一个接口,它的实现类就是,2.1 获得的 DefaultSqlSessionFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}


private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

我们看第二段代码:因为我们解析主配置文件把所有的节点信息都保存在了configuration对象中,它开始直接或得Environment节点的信息,这个节点配置了数据库连接和事务。之后通过Environment创建了一个事务工厂,然后通过事务工厂实例化了一个事务对象。 重点来了------> 最后他创建了一个执行器Executor ,我们知道session是与数据库交互的顶层api,session中会维护一个Executor 来负责sql生产和执行和查询缓存等。我们再来看看new这个执行器的时候的过程

执行器生成完后返回了一个DefaultSqlSession,这里面维护了Configuration和Executor。

graph TB
	subgraph step 0 配置加载
		a[获取mybatis主配置文件的输入流对象] --> b[InputStream Object]
	end
	subgraph step 1 构建会话工厂对象
		b --> c[创建XMLConfigBuilder对象]
		c --> d[解析主配置文件]
	end

Configuration 主要属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Configuration {
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel;
protected boolean cacheEnabled;
protected boolean callSettersOnNulls;
protected boolean useActualParamName;
protected boolean returnInstanceForEmptyRow;
protected boolean shrinkWhitespacesInSql;
protected String logPrefix;
protected Class<? extends Log> logImpl;
protected Class<? extends VFS> vfsImpl;
protected LocalCacheScope localCacheScope;
protected JdbcType jdbcTypeForNull;
protected Set<String> lazyLoadTriggerMethods;
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ResultSetType defaultResultSetType;
protected ExecutorType defaultExecutorType;
protected AutoMappingBehavior autoMappingBehavior;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior;
protected Properties variables;
protected ReflectorFactory reflectorFactory;
protected ObjectFactory objectFactory;
protected ObjectWrapperFactory objectWrapperFactory;
protected boolean lazyLoadingEnabled;
protected ProxyFactory proxyFactory;
protected String databaseId;
protected Class<?> configurationFactory;
protected final MapperRegistry mapperRegistry;
protected final InterceptorChain interceptorChain;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected final TypeAliasRegistry typeAliasRegistry;
protected final LanguageDriverRegistry languageRegistry;
protected final Map<String, MappedStatement> mappedStatements;
protected final Map<String, Cache> caches;
protected final Map<String, ResultMap> resultMaps;
protected final Map<String, ParameterMap> parameterMaps;
protected final Map<String, KeyGenerator> keyGenerators;
protected final Set<String> loadedResources;
protected final Map<String, XNode> sqlFragments;
protected final Collection<XMLStatementBuilder> incompleteStatements;
protected final Collection<CacheRefResolver> incompleteCacheRefs;
protected final Collection<ResultMapResolver> incompleteResultMaps;
protected final Collection<MethodResolver> incompleteMethods;
protected final Map<String, String> cacheRefMap;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!