个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈
Ext1:本文源码解析基于 mybatis-spring-boot-starter 2.1.1,即 mybatis 3.5.3 版本。
Ext2:本文主要是对源码的讲解,着重点会是在源码上。
一、从 MybatisAutoConfiguration 说开去,mapper 文件是怎么扫描的?
我们知道配置 SqlSessionFactory 是我们集成 Mybatis 时需要用到的常客,SqlSessionFactory 顾名思义是用来创建 SqlSession 对象的,SqlSession 对象的重要程度不言而喻。源码中提到,SqlSession 是 Mybatis 运行最重要的一个接口,通过此接口,我们可以进行我们的操作指令,获取 mapper,管理事务等操作。
官网 给出了一个简单的配置demo,通过 SqlSessionFactoryBean 进行 sqlSessionFactory 的创建。
@Bean
public SqlSessionFactory sqlSessionFactory() {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
return factoryBean.getObject();
}
我们可以拿到这个 SqlSessionBean 来进行我们一些定制化操作,比如 mybatis插件,自定义的返回处理等等。如果我们不显式声明 SqlSessionFactory,则会使用 mybatis-spring-boot-autoconfigure 下的这个 bean 的注册:

我们可以看到在 mybatis 里的很多定制化常客,都出现在了这里。比如,配置 mapper 文件位置的配置,我们用以下的小段代码来看的话:
-- 来自代码 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#sqlSessionFactory --
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
它的实现实际上非常简单:拿到我们所有的 mapperLocations 这个数组,解析成 Resource 数组。
-- 来自代码 org.mybatis.spring.boot.autoconfigure.MybatisProperties#resolveMapperLocations --
public Resource[] resolveMapperLocations() {
return Stream.of(Optional.ofNullable(this.mapperLocations).orElse(new String[0]))
.flatMap(location -> Stream.of(getResources(location))).toArray(Resource[]::new);
}
private Resource[] getResources(String location) {
try {
return resourceResolver.getResources(location);
} catch (IOException e) {
return new Resource[0];
}
}
-- application.yml中的配置 --
mybatis:
mapper-locations: classpath*:com/anur/mybatisdemo/test/mapper/*.xml
*/
纵览一下这几者的关系,SqlSessionFactory 是根据配置 Configuration 与 sqlSessionFactoryBuilder 共同创建的,如果在 spring 项目中,则会由 SqlSessionFactoryBean 来替代 SqlSessionFactoryBuilder 进行创建。

二、SqlSessionFactory 的初始化与 XMLMapperBuilder
其实上面扯了那么多,只是想引入一下 XMLMapperBuilder。我们知道,我们的配置(比如Spring中的 application.yml),最后会被解析成 Configuration,而 mapper.xml 文件正是依据我们的配置来进行读取的,读取到的 xml 将被读取成 Resource文件,最后在 SqlSessionFactoryBean 初始化完毕后、也就是在创建 SqlSessionFactory 之前:会通过 XMLMapperBuilder 完成 xml 文件的解析。

XMLMapperBuilder 在完成初始化后,调用 org.apache.ibatis.builder.xml.XMLMapperBuilder#parse 来进行真正的 mapper 文件解析:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
configurationElement 就是对我们 xml 文件的解析,通过parser.evalNode("/mapper") 拿到我们编写的 xml 的 <mapper>标签进行初步的解析,源码如下:可以看到许多熟悉的身影,比如 namespace、resultMap、select|insert|update|delete之类的。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
给一个简单的 xml 看一下 mapper 标签里面的内容方便理解,就是 <mapper xxxxxxxxx> </mapper> 之间那一大段内容, mybaits 封装的这套 XNode 可以使得我们访问 xml 像访问 map 一样轻松:
<?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.anur.mybatisdemo.test.TrackerConfigMapper">
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
select *
from tracker_config
where in_use = 1
<if test="followerId != null">and user_id = #{followerId}</if>
</select>
<select id="getFollower" resultType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
select *
from tracker_config
where in_use = 1
<if test="followerId != null">and user_id = #{followerId}</if>
limit 1
</select>
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<result column="user_d" property="userId"/>
<result column="in_use" property="inUse"/>
<association property="config" resultMap="customMap"/>
</resultMap>
</mapper>
三、ResultMap 是如何解析的
方才说到,configurationElement() 方法负责对 xml 文件进行解析,我们拿几个主要的元素出来讲讲,比如 resultMap:
resultMapElements(context.evalNodes("/mapper/resultMap")); 就是解析 resultMap 的入口,同样的,先拿到 resultMap 这个 XML 节点,进入到 resultMapElements 这个方法,resultMapElements 负责解析 xml,最后,将解析的结果交给 ResultMapResolver 处理。
我们先忽略 ResultMapResolver,简单看看 resultMapElement 中做了什么,对应的源码如下,大体可分为两类解析:
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) { // 循环解析子标签
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
-
一种是对
resultMap本身属性的解析,也就是getStringAttribute,例如当前resultMap的type是什么,它是否开启autoMapping,id是什么之类的。 -
一种则是对子标签的解析,子标签的解析,则分为
constructor、discriminator、以及其他字段的解析。
如图所示: 
3.1 ResultMap 中的重要成员:typeHandler
在 mybatis 对 mysql 返回的结果集 resultSet 进行解析时,typeHandler 有着举足轻重的作用。mysql 的 JdbcType 有很多,比如 BLOB, VARCHAR, DATE 等等,而我们的 java 类型( mybatis 称之为 javaType,或者 javaTypeClass)也很多,还包括我们很多的 自定义的 TypeHandler,这里就不赘述了。
那么必然存在一个问题,如何将它们一一对应上?毫无疑问,JdbcType 可以被解析为多个 javaTypeClass ,如 VARCHAR 可以对应解析成我们的 JSON JAVA BEAN ,也可以解析为 String 等等;同样, String 类型也可以由多个 JdbcType 解析而来,比如 DATE 类型可以经过一定规则的解析,成为 String 类型的时间。
答案就在 org.apache.ibatis.type.TypeHandlerRegistry。
public TypeHandlerRegistry() {
re

本文详细介绍了Mybatis中mapper.xml文件的解析过程,从MybatisAutoConfiguration开始,讲解SqlSessionFactory的初始化,XMLMapperBuilder的角色,ResultMap的解析,以及sql语句与MappedStatement的关联。通过源码分析,揭示了mapper.xml中配置的ResultMap、SqlSessionFactory、SqlSource和MappedStatement的生成机制,帮助读者理解Mybatis的工作原理。
最低0.47元/天 解锁文章
1559

被折叠的 条评论
为什么被折叠?



