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() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcTy